aboutsummaryrefslogtreecommitdiff
path: root/src/client/util
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/util')
-rw-r--r--src/client/util/DictationManager.ts78
-rw-r--r--src/client/util/DocumentManager.ts4
-rw-r--r--src/client/util/DragManager.ts38
-rw-r--r--src/client/util/ParagraphNodeSpec.ts133
-rw-r--r--src/client/util/ProsemirrorExampleTransfer.ts213
-rw-r--r--src/client/util/RichTextRules.ts114
-rw-r--r--src/client/util/RichTextSchema.tsx484
-rw-r--r--src/client/util/SelectionManager.ts1
-rw-r--r--src/client/util/TooltipLinkingMenu.tsx20
-rw-r--r--src/client/util/TooltipTextMenu.tsx89
-rw-r--r--src/client/util/clamp.js15
-rw-r--r--src/client/util/convertToCSSPTValue.js43
-rw-r--r--src/client/util/prosemirrorPatches.js139
-rw-r--r--src/client/util/toCSSLineSpacing.js64
14 files changed, 1174 insertions, 261 deletions
diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
index 9c61fe125..fb3c15cea 100644
--- a/src/client/util/DictationManager.ts
+++ b/src/client/util/DictationManager.ts
@@ -2,9 +2,10 @@ import { SelectionManager } from "./SelectionManager";
import { DocumentView } from "../views/nodes/DocumentView";
import { UndoManager } from "./UndoManager";
import * as interpreter from "words-to-numbers";
+import { DocumentType } from "../documents/DocumentTypes";
import { Doc } from "../../new_fields/Doc";
import { List } from "../../new_fields/List";
-import { Docs, DocumentType } from "../documents/Documents";
+import { Docs } from "../documents/Documents";
import { CollectionViewType } from "../views/collections/CollectionBaseView";
import { Cast, CastCtor } from "../../new_fields/Types";
import { listSpec } from "../../new_fields/Schema";
@@ -12,6 +13,7 @@ import { AudioField, ImageField } from "../../new_fields/URLField";
import { HistogramField } from "../northstar/dash-fields/HistogramField";
import { MainView } from "../views/MainView";
import { Utils } from "../../Utils";
+import { RichTextField } from "../../new_fields/RichTextField";
/**
* This namespace provides a singleton instance of a manager that
@@ -43,7 +45,7 @@ export namespace DictationManager {
export namespace Controls {
- const infringe = "unable to process: dictation manager still involved in previous session";
+ export const Infringed = "unable to process: dictation manager still involved in previous session";
const intraSession = ". ";
const interSession = " ... ";
@@ -62,35 +64,45 @@ export namespace DictationManager {
export type ListeningUIStatus = { interim: boolean } | false;
export interface ListeningOptions {
+ useOverlay: boolean;
language: string;
continuous: ContinuityArgs;
delimiters: DelimiterArgs;
interimHandler: InterimResultHandler;
tryExecute: boolean;
+ terminators: string[];
}
export const listen = async (options?: Partial<ListeningOptions>) => {
let results: string | undefined;
let main = MainView.Instance;
- main.dictationOverlayVisible = true;
- main.isListening = { interim: false };
+ let overlay = options !== undefined && options.useOverlay;
+ if (overlay) {
+ main.dictationOverlayVisible = true;
+ main.isListening = { interim: false };
+ }
try {
results = await listenImpl(options);
if (results) {
Utils.CopyText(results);
- main.isListening = false;
- let execute = options && options.tryExecute;
- main.dictatedPhrase = execute ? results.toLowerCase() : results;
- main.dictationSuccess = execute ? await DictationManager.Commands.execute(results) : true;
+ if (overlay) {
+ main.isListening = false;
+ let execute = options && options.tryExecute;
+ main.dictatedPhrase = execute ? results.toLowerCase() : results;
+ main.dictationSuccess = execute ? await DictationManager.Commands.execute(results) : true;
+ }
+ options && options.tryExecute && await DictationManager.Commands.execute(results);
}
} catch (e) {
- main.isListening = false;
- main.dictatedPhrase = results = `dictation error: ${"error" in e ? e.error : "unknown error"}`;
- main.dictationSuccess = false;
+ if (overlay) {
+ main.isListening = false;
+ main.dictatedPhrase = results = `dictation error: ${"error" in e ? e.error : "unknown error"}`;
+ main.dictationSuccess = false;
+ }
} finally {
- main.initiateDictationFade();
+ overlay && main.initiateDictationFade();
}
return results;
@@ -98,7 +110,7 @@ export namespace DictationManager {
const listenImpl = (options?: Partial<ListeningOptions>) => {
if (isListening) {
- return infringe;
+ return Infringed;
}
isListening = true;
@@ -126,6 +138,12 @@ export namespace DictationManager {
recognizer.onresult = (e: SpeechRecognitionEvent) => {
current = synthesize(e, intra);
+ let matchedTerminator: string | undefined;
+ if (options && options.terminators && (matchedTerminator = options.terminators.find(end => current ? current.trim().toLowerCase().endsWith(end.toLowerCase()) : false))) {
+ current = matchedTerminator;
+ recognizer.abort();
+ return complete();
+ }
handler && handler(current);
isManuallyStopped && complete();
};
@@ -161,13 +179,13 @@ export namespace DictationManager {
}
isManuallyStopped = true;
salvageSession ? recognizer.stop() : recognizer.abort();
- let main = MainView.Instance;
- if (main.dictationOverlayVisible) {
- main.cancelDictationFade();
- main.dictationOverlayVisible = false;
- main.dictationSuccess = undefined;
- setTimeout(() => main.dictatedPhrase = placeholder, 500);
- }
+ // let main = MainView.Instance;
+ // if (main.dictationOverlayVisible) {
+ // main.cancelDictationFade();
+ // main.dictationOverlayVisible = false;
+ // main.dictationSuccess = undefined;
+ // setTimeout(() => main.dictatedPhrase = placeholder, 500);
+ // }
};
const synthesize = (e: SpeechRecognitionEvent, delimiter?: string) => {
@@ -299,11 +317,20 @@ export namespace DictationManager {
}
}],
- ["promote", {
+ ["new outline", {
action: (target: DocumentView) => {
- console.log(target);
- },
- restrictTo: [DocumentType.TEXT]
+ let newBox = Docs.Create.TextDocument({ width: 400, height: 200, title: "My Outline" });
+ newBox.autoHeight = true;
+ let proto = newBox.proto!;
+ proto.page = -1;
+ let prompt = "Press alt + r to start dictating here...";
+ let head = 3;
+ let anchor = head + prompt.length;
+ let proseMirrorState = `{"doc":{"type":"doc","content":[{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"type":"text","text":"${prompt}"}]}]}]}]},"selection":{"type":"text","anchor":${anchor},"head":${head}}}`;
+ proto.data = new RichTextField(proseMirrorState);
+ proto.backgroundColor = "#eeffff";
+ target.props.addDocTab(newBox, proto, "onRight");
+ }
}]
]);
@@ -317,6 +344,9 @@ export namespace DictationManager {
let what = matches[2];
let dataDoc = Doc.GetProto(target.props.Document);
let fieldKey = "data";
+ if (isNaN(count)) {
+ return;
+ }
for (let i = 0; i < count; i++) {
let created: Doc | undefined;
switch (what) {
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 7f526b247..ec731da84 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -9,6 +9,7 @@ import { CollectionView } from '../views/collections/CollectionView';
import { DocumentView } from '../views/nodes/DocumentView';
import { LinkManager } from './LinkManager';
import { undoBatch, UndoManager } from './UndoManager';
+import { Scripting } from './Scripting';
export class DocumentManager {
@@ -202,4 +203,5 @@ export class DocumentManager {
return 1;
}
}
-} \ No newline at end of file
+}
+Scripting.addGlobal(function focus(doc: any) { DocumentManager.Instance.getDocumentViews(Doc.GetProto(doc)).map(view => view.props.focus(doc, true)); }); \ No newline at end of file
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 0b6d9b5e5..4c9c9c17c 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -1,5 +1,5 @@
import { action, runInAction } from "mobx";
-import { Doc } from "../../new_fields/Doc";
+import { Doc, Field } from "../../new_fields/Doc";
import { Cast, StrCast } from "../../new_fields/Types";
import { URLField } from "../../new_fields/URLField";
import { emptyFunction } from "../../Utils";
@@ -9,8 +9,11 @@ import { DocumentManager } from "./DocumentManager";
import { LinkManager } from "./LinkManager";
import { SelectionManager } from "./SelectionManager";
import { SchemaHeaderField } from "../../new_fields/SchemaHeaderField";
-import { DocumentDecorations } from "../views/DocumentDecorations";
-import { NumberLiteralType } from "typescript";
+import { Docs } from "../documents/Documents";
+import { CompileScript } from "./Scripting";
+import { ScriptField } from "../../new_fields/ScriptField";
+import { List } from "../../new_fields/List";
+import { PrefetchProxy } from "../../new_fields/Proxy";
export type dropActionType = "alias" | "copy" | undefined;
export function SetupDrag(
@@ -142,6 +145,8 @@ export namespace DragManager {
withoutShiftDrag?: boolean;
+ finishDrag?: (dropData: { [id: string]: any }) => void;
+
offsetX?: number;
offsetY?: number;
@@ -211,6 +216,7 @@ export namespace DragManager {
dropAction: dropActionType;
userDropAction: dropActionType;
moveDocument?: MoveFunction;
+ applyAsTemplate?: boolean;
[id: string]: any;
}
@@ -235,7 +241,7 @@ export namespace DragManager {
export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) {
runInAction(() => StartDragFunctions.map(func => func()));
- StartDrag(eles, dragData, downX, downY, options,
+ StartDrag(eles, dragData, downX, downY, options, options && options.finishDrag ? options.finishDrag :
(dropData: { [id: string]: any }) => {
(dropData.droppedDocuments = dragData.userDropAction === "alias" || (!dragData.userDropAction && dragData.dropAction === "alias") ?
dragData.draggedDocuments.map(d => Doc.MakeAlias(d)) :
@@ -246,6 +252,28 @@ export namespace DragManager {
});
}
+ export function StartButtonDrag(eles: HTMLElement[], script: string, title: string, vars: { [name: string]: Field }, params: string[], initialize?: (button: Doc) => void, downX: number, downY: number, options?: DragOptions) {
+ let dragData = new DragManager.DocumentDragData([], [undefined]);
+ runInAction(() => StartDragFunctions.map(func => func()));
+ StartDrag(eles, dragData, downX, downY, options, options && options.finishDrag ? options.finishDrag :
+ (dropData: { [id: string]: any }) => {
+ let bd = Docs.Create.ButtonDocument({ width: 150, height: 50, title: title });
+ let compiled = CompileScript(script, {
+ params: { doc: Doc.name },
+ typecheck: false,
+ editable: true
+ });
+ if (compiled.compiled) {
+ let scriptField = new ScriptField(compiled);
+ bd.onClick = scriptField;
+ }
+ params.map(p => Object.keys(vars).indexOf(p) !== -1 && (Doc.GetProto(bd)[p] = new PrefetchProxy(vars[p] as Doc)));
+ initialize && initialize(bd);
+ bd.buttonParams = new List<string>(params);
+ dropData.droppedDocuments = [bd];
+ });
+ }
+
export function StartLinkedDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) {
dragData.moveDocument = moveLinkedDocument;
@@ -481,7 +509,7 @@ export namespace DragManager {
// if (parent && dragEle) parent.appendChild(dragEle);
});
if (target) {
- if (finishDrag) finishDrag(dragData);
+ finishDrag && finishDrag(dragData);
target.dispatchEvent(
new CustomEvent<DropEvent>("dashOnDrop", {
diff --git a/src/client/util/ParagraphNodeSpec.ts b/src/client/util/ParagraphNodeSpec.ts
new file mode 100644
index 000000000..3a993e1ff
--- /dev/null
+++ b/src/client/util/ParagraphNodeSpec.ts
@@ -0,0 +1,133 @@
+import clamp from './clamp';
+import convertToCSSPTValue from './convertToCSSPTValue';
+import toCSSLineSpacing from './toCSSLineSpacing';
+import { Node, DOMOutputSpec } from 'prosemirror-model';
+
+//import type { NodeSpec } from './Types';
+type NodeSpec = {
+ attrs?: { [key: string]: any },
+ content?: string,
+ draggable?: boolean,
+ group?: string,
+ inline?: boolean,
+ name?: string,
+ parseDOM?: Array<any>,
+ toDOM?: (node: any) => DOMOutputSpec,
+};
+
+// This assumes that every 36pt maps to one indent level.
+export const INDENT_MARGIN_PT_SIZE = 36;
+export const MIN_INDENT_LEVEL = 0;
+export const MAX_INDENT_LEVEL = 7;
+export const ATTRIBUTE_INDENT = 'data-indent';
+
+export const EMPTY_CSS_VALUE = new Set(['', '0%', '0pt', '0px']);
+
+const ALIGN_PATTERN = /(left|right|center|justify)/;
+
+// https://github.com/ProseMirror/prosemirror-schema-basic/blob/master/src/schema-basic.js
+// :: NodeSpec A plain paragraph textblock. Represented in the DOM
+// as a `<p>` element.
+const ParagraphNodeSpec: NodeSpec = {
+ attrs: {
+ align: { default: null },
+ color: { default: null },
+ id: { default: null },
+ indent: { default: null },
+ lineSpacing: { default: null },
+ // TODO: Add UI to let user edit / clear padding.
+ paddingBottom: { default: null },
+ // TODO: Add UI to let user edit / clear padding.
+ paddingTop: { default: null },
+ },
+ content: 'inline*',
+ group: 'block',
+ parseDOM: [{ tag: 'p', getAttrs }],
+ toDOM,
+};
+
+function getAttrs(dom: HTMLElement): Object {
+ const {
+ lineHeight,
+ textAlign,
+ marginLeft,
+ paddingTop,
+ paddingBottom,
+ } = dom.style;
+
+ let align = dom.getAttribute('align') || textAlign || '';
+ align = ALIGN_PATTERN.test(align) ? align : "";
+
+ let indent = parseInt(dom.getAttribute(ATTRIBUTE_INDENT) || "", 10);
+
+ if (!indent && marginLeft) {
+ indent = convertMarginLeftToIndentValue(marginLeft);
+ }
+
+ indent = indent || MIN_INDENT_LEVEL;
+
+ const lineSpacing = lineHeight ? toCSSLineSpacing(lineHeight) : null;
+
+ const id = dom.getAttribute('id') || '';
+ return { align, indent, lineSpacing, paddingTop, paddingBottom, id };
+}
+
+function toDOM(node: Node): DOMOutputSpec {
+ const {
+ align,
+ indent,
+ lineSpacing,
+ paddingTop,
+ paddingBottom,
+ id,
+ } = node.attrs;
+ const attrs: { [key: string]: any } | null = {};
+
+ let style = '';
+ if (align && align !== 'left') {
+ style += `text-align: ${align};`;
+ }
+
+ if (lineSpacing) {
+ const cssLineSpacing = toCSSLineSpacing(lineSpacing);
+ style +=
+ `line-height: ${cssLineSpacing};` +
+ // This creates the local css variable `--czi-content-line-height`
+ // that its children may apply.
+ `--czi-content-line-height: ${cssLineSpacing}`;
+ }
+
+ if (paddingTop && !EMPTY_CSS_VALUE.has(paddingTop)) {
+ style += `padding-top: ${paddingTop};`;
+ }
+
+ if (paddingBottom && !EMPTY_CSS_VALUE.has(paddingBottom)) {
+ style += `padding-bottom: ${paddingBottom};`;
+ }
+
+ style && (attrs.style = style);
+
+ if (indent) {
+ attrs[ATTRIBUTE_INDENT] = String(indent);
+ }
+
+ if (id) {
+ attrs.id = id;
+ }
+
+ return ['p', attrs, 0];
+}
+
+export const toParagraphDOM = toDOM;
+export const getParagraphNodeAttrs = getAttrs;
+
+export function convertMarginLeftToIndentValue(marginLeft: string): number {
+ const ptValue = convertToCSSPTValue(marginLeft);
+ return clamp(
+ MIN_INDENT_LEVEL,
+ Math.floor(ptValue / INDENT_MARGIN_PT_SIZE),
+ MAX_INDENT_LEVEL
+ );
+}
+
+export default ParagraphNodeSpec; \ No newline at end of file
diff --git a/src/client/util/ProsemirrorExampleTransfer.ts b/src/client/util/ProsemirrorExampleTransfer.ts
index c38f84551..1d2d33800 100644
--- a/src/client/util/ProsemirrorExampleTransfer.ts
+++ b/src/client/util/ProsemirrorExampleTransfer.ts
@@ -1,14 +1,11 @@
-import { Schema, NodeType } from "prosemirror-model";
-import {
- wrapIn, setBlockType, chainCommands, toggleMark, exitCode,
- joinUp, joinDown, lift, selectParentNode
-} from "prosemirror-commands";
-import { wrapInList, splitListItem, liftListItem, sinkListItem } from "prosemirror-schema-list";
-import { undo, redo } from "prosemirror-history";
+import { chainCommands, exitCode, joinDown, joinUp, lift, selectParentNode, setBlockType, splitBlockKeepMarks, toggleMark, wrapIn } from "prosemirror-commands";
+import { redo, undo } from "prosemirror-history";
import { undoInputRule } from "prosemirror-inputrules";
-import { Transaction, EditorState } from "prosemirror-state";
+import { Schema } from "prosemirror-model";
+import { liftListItem, sinkListItem } from "./prosemirrorPatches.js";
+import { splitListItem, wrapInList, } from "prosemirror-schema-list";
+import { EditorState, Transaction, TextSelection, NodeSelection } from "prosemirror-state";
import { TooltipTextMenu } from "./TooltipTextMenu";
-import { Statement } from "../northstar/model/idea/idea";
const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false;
@@ -30,79 +27,161 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
bind("Shift-Mod-z", redo);
bind("Backspace", undoInputRule);
- if (!mac) {
- bind("Mod-y", redo);
- }
+ !mac && bind("Mod-y", redo);
bind("Alt-ArrowUp", joinUp);
bind("Alt-ArrowDown", joinDown);
bind("Mod-BracketLeft", lift);
bind("Escape", selectParentNode);
- if (type = schema.marks.strong) {
- bind("Mod-b", toggleMark(type));
- bind("Mod-B", toggleMark(type));
- }
- if (type = schema.marks.em) {
- bind("Mod-i", toggleMark(type));
- bind("Mod-I", toggleMark(type));
- }
- if (type = schema.marks.underline) {
- bind("Mod-u", toggleMark(type));
- bind("Mod-U", toggleMark(type));
- }
- if (type = schema.marks.code) {
- bind("Mod-`", toggleMark(type));
- }
+ bind("Mod-b", toggleMark(schema.marks.strong));
+ bind("Mod-B", toggleMark(schema.marks.strong));
- if (type = schema.nodes.bullet_list) {
- bind("Ctrl-.", wrapInList(type));
- }
- if (type = schema.nodes.ordered_list) {
- bind("Ctrl-n", wrapInList(type));
- }
- if (type = schema.nodes.blockquote) {
- bind("Ctrl->", wrapIn(type));
+ bind("Mod-e", toggleMark(schema.marks.em));
+ bind("Mod-E", toggleMark(schema.marks.em));
+
+ bind("Mod-u", toggleMark(schema.marks.underline));
+ bind("Mod-U", toggleMark(schema.marks.underline));
+
+ bind("Mod-`", toggleMark(schema.marks.code));
+
+ bind("Ctrl-.", wrapInList(schema.nodes.bullet_list));
+
+ bind("Ctrl-n", wrapInList(schema.nodes.ordered_list));
+
+ bind("Ctrl->", wrapIn(schema.nodes.blockquote));
+
+ // bind("^", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+ // let newNode = schema.nodes.footnote.create({});
+ // if (dispatch && state.selection.from === state.selection.to) {
+ // let tr = state.tr;
+ // tr.replaceSelectionWith(newNode); // replace insertion with a footnote.
+ // dispatch(tr.setSelection(new NodeSelection( // select the footnote node to open its display
+ // tr.doc.resolve( // get the location of the footnote node by subtracting the nodesize of the footnote from the current insertion point anchor (which will be immediately after the footnote node)
+ // tr.selection.anchor - tr.selection.$anchor.nodeBefore!.nodeSize))));
+ // return true;
+ // }
+ // return false;
+ // });
+
+
+ let cmd = chainCommands(exitCode, (state, dispatch) => {
+ if (dispatch) {
+ dispatch(state.tr.replaceSelectionWith(schema.nodes.hard_break.create()).scrollIntoView());
+ return true;
+ }
+ return false;
+ });
+ bind("Mod-Enter", cmd);
+ bind("Shift-Enter", cmd);
+ mac && bind("Ctrl-Enter", cmd);
+
+
+ bind("Shift-Ctrl-0", setBlockType(schema.nodes.paragraph));
+
+ bind("Shift-Ctrl-\\", setBlockType(schema.nodes.code_block));
+
+ for (let i = 1; i <= 6; i++) {
+ bind("Shift-Ctrl-" + i, setBlockType(schema.nodes.heading, { level: i }));
}
- if (type = schema.nodes.hard_break) {
- let br = type, cmd = chainCommands(exitCode, (state, dispatch) => {
- if (dispatch) {
- dispatch(state.tr.replaceSelectionWith(br.create()).scrollIntoView());
- return true;
+
+ let hr = schema.nodes.horizontal_rule;
+ bind("Mod-_", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+ dispatch(state.tr.replaceSelectionWith(hr.create()).scrollIntoView());
+ return true;
+ });
+
+ bind("Mod-s", TooltipTextMenu.insertStar);
+
+ let updateBullets = (tx2: Transaction) => {
+ tx2.doc.descendants((node: any, offset: any, index: any) => {
+ if (node.type === schema.nodes.ordered_list || node.type === schema.nodes.list_item) {
+ let path = (tx2.doc.resolve(offset) as any).path;
+ let depth = Array.from(path).reduce((p: number, c: any) => p + (c.hasOwnProperty("type") && (c as any).type === schema.nodes.ordered_list ? 1 : 0), 0);
+ if (node.type === schema.nodes.ordered_list) depth++;
+ tx2.setNodeMarkup(offset, node.type, { ...node.attrs, mapStyle: node.attrs.mapStyle, bulletStyle: depth }, node.marks);
}
- return false;
});
- bind("Mod-Enter", cmd);
- bind("Shift-Enter", cmd);
- if (mac) {
- bind("Ctrl-Enter", cmd);
+ };
+
+
+ bind("Tab", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+ var ref = state.selection;
+ var range = ref.$from.blockRange(ref.$to);
+ var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
+ if (!sinkListItem(schema.nodes.list_item)(state, (tx2: Transaction) => {
+ updateBullets(tx2);
+ marks && tx2.ensureMarks([...marks]);
+ marks && tx2.setStoredMarks([...marks]);
+ dispatch(tx2);
+ })) { // couldn't sink into an existing list, so wrap in a new one
+ let newstate = state.applyTransaction(state.tr.setSelection(TextSelection.create(state.doc, range!.start, range!.end)));
+ if (!wrapInList(schema.nodes.ordered_list)(newstate.state, (tx2: Transaction) => {
+ updateBullets(tx2);
+ // when promoting to a list, assume list will format things so don't copy the stored marks.
+ marks && tx2.ensureMarks([...marks]);
+ marks && tx2.setStoredMarks([...marks]);
+ dispatch(tx2);
+ })) {
+ console.log("bullet promote fail");
+ }
}
- }
- if (type = schema.nodes.list_item) {
- bind("Enter", splitListItem(type));
- bind("Shift-Tab", liftListItem(type));
- bind("Tab", sinkListItem(type));
- }
- if (type = schema.nodes.paragraph) {
- bind("Shift-Ctrl-0", setBlockType(type));
- }
- if (type = schema.nodes.code_block) {
- bind("Shift-Ctrl-\\", setBlockType(type));
- }
- if (type = schema.nodes.heading) {
- for (let i = 1; i <= 6; i++) {
- bind("Shift-Ctrl-" + i, setBlockType(type, { level: i }));
+ });
+
+ bind("Shift-Tab", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+ var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
+
+ if (!liftListItem(schema.nodes.list_item)(state.tr, (tx2: Transaction) => {
+ updateBullets(tx2);
+ marks && tx2.ensureMarks([...marks]);
+ marks && tx2.setStoredMarks([...marks]);
+ dispatch(tx2);
+ })) {
+ console.log("bullet demote fail");
}
+ });
+
+ let splitMetadata = (marks: any, tx: Transaction) => {
+ marks && tx.ensureMarks(marks.filter((val: any) => val.type !== schema.marks.metadata && val.type !== schema.marks.metadataKey && val.type !== schema.marks.metadataVal));
+ marks && tx.setStoredMarks(marks.filter((val: any) => val.type !== schema.marks.metadata && val.type !== schema.marks.metadataKey && val.type !== schema.marks.metadataVal));
+ return tx;
}
- if (type = schema.nodes.horizontal_rule) {
- let hr = type;
- bind("Mod-_", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
- dispatch(state.tr.replaceSelectionWith(hr.create()).scrollIntoView());
- return true;
+ bind("Enter", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+ var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
+ if (!splitListItem(schema.nodes.list_item)(state, (tx3: Transaction) => dispatch(tx3))) {
+ if (!splitBlockKeepMarks(state, (tx3: Transaction) => {
+ splitMetadata(marks, tx3);
+ if (!liftListItem(schema.nodes.list_item)(tx3, dispatch as ((tx: Transaction<Schema<any, any>>) => void))) {
+ dispatch(tx3);
+ }
+ })) {
+ return false;
+ }
+ }
+ return true;
+ });
+ bind("Space", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+ var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
+ dispatch(splitMetadata(marks, state.tr));
+ return false;
+ });
+ bind(":", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+ let range = state.selection.$from.blockRange(state.selection.$to, (node: any) => {
+ return !node.marks || !node.marks.find((m: any) => m.type === schema.marks.metadata);
});
- }
+ let path = (state.doc.resolve(state.selection.from - 1) as any).path;
+ let spaceSeparator = path[path.length - 3].childCount > 1 ? 0 : -1;
+ let textsel = TextSelection.create(state.doc, range!.end - path[path.length - 3].lastChild.nodeSize + spaceSeparator, range!.end);
+ let text = range ? state.doc.textBetween(textsel.from, textsel.to) : "";
+ let whitespace = text.length - 1;
+ for (; whitespace >= 0 && text[whitespace] !== " "; whitespace--) { }
+ if (text.endsWith(":")) {
+ dispatch(state.tr.addMark(textsel.from + whitespace + 1, textsel.to, schema.marks.metadata.create() as any).
+ addMark(textsel.from + whitespace + 1, textsel.to - 2, schema.marks.metadataKey.create() as any));
+ }
+ return false;
+ });
- bind("Mod-s", TooltipTextMenu.insertStar);
return keys;
}
diff --git a/src/client/util/RichTextRules.ts b/src/client/util/RichTextRules.ts
index 3b8396510..c727eec73 100644
--- a/src/client/util/RichTextRules.ts
+++ b/src/client/util/RichTextRules.ts
@@ -1,14 +1,10 @@
-import {
- inputRules,
- wrappingInputRule,
- textblockTypeInputRule,
- smartQuotes,
- emDash,
- ellipsis
-} from "prosemirror-inputrules";
-import { Schema, NodeSpec, MarkSpec, DOMOutputSpecArray, NodeType } from "prosemirror-model";
-
+import { textblockTypeInputRule, smartQuotes, emDash, ellipsis, InputRule } from "prosemirror-inputrules";
import { schema } from "./RichTextSchema";
+import { wrappingInputRule } from "./prosemirrorPatches";
+import { NodeSelection } from "prosemirror-state";
+import { NumCast, Cast } from "../../new_fields/Types";
+import { Doc } from "../../new_fields/Doc";
+import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
export const inpRules = {
rules: [
@@ -21,10 +17,29 @@ export const inpRules = {
// 1. ordered list
wrappingInputRule(
- /^(\d+)\.\s$/,
+ /^1\.\s$/,
+ schema.nodes.ordered_list,
+ () => {
+ return ({ mapStyle: "decimal", bulletStyle: 1 })
+ },
+ (match: any, node: any) => {
+ return node.childCount + node.attrs.order === +match[1];
+ },
+ (type: any) => ({ type: type, attrs: { mapStyle: "decimal", bulletStyle: 1 } })
+ ),
+ // a. alphabbetical list
+ wrappingInputRule(
+ /^a\.\s$/,
schema.nodes.ordered_list,
- match => ({ order: +match[1] }),
- (match, node) => node.childCount + node.attrs.order === +match[1]
+ // match => {
+ () => {
+ return ({ mapStyle: "alpha", bulletStyle: 1 })
+ // return ({ order: +match[1] })
+ },
+ (match: any, node: any) => {
+ return node.childCount + node.attrs.order === +match[1];
+ },
+ (type: any) => ({ type: type, attrs: { mapStyle: "alpha", bulletStyle: 1 } })
),
// * bullet list
@@ -35,9 +50,76 @@ export const inpRules = {
// # heading
textblockTypeInputRule(
- new RegExp("^(#{1,6})\\s$"),
+ new RegExp(/^(#{1,6})\s$/),
schema.nodes.heading,
- match => ({ level: match[1].length })
- )
+ match => {
+ return ({ level: match[1].length });
+ }
+ ),
+
+ new InputRule(
+ new RegExp(/^#([0-9]+)\s$/),
+ (state, match, start, end) => {
+ let size = Number(match[1]);
+ let ruleProvider = FormattedTextBox.InputBoxOverlay!.props.ruleProvider;
+ let heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading);
+ if (ruleProvider && heading) {
+ (Cast(FormattedTextBox.InputBoxOverlay!.props.Document, Doc) as Doc).heading = Number(match[1]);
+ return state.tr.deleteRange(start, end);
+ }
+ return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontSize.create({ fontSize: Number(match[1]) }))
+ }),
+ new InputRule(
+ new RegExp(/^\^\^\s$/),
+ (state, match, start, end) => {
+ let node = (state.doc.resolve(start) as any).nodeAfter;
+ let sm = state.storedMarks || undefined;
+ let ruleProvider = FormattedTextBox.InputBoxOverlay!.props.ruleProvider;
+ let heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading);
+ if (ruleProvider && heading) {
+ ruleProvider["ruleAlign_" + heading] = "center";
+ return node ? state.tr.deleteRange(start, end).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
+ }
+ return node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "center" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
+ state.tr;
+ }),
+ new InputRule(
+ new RegExp(/^\[\[\s$/),
+ (state, match, start, end) => {
+ let node = (state.doc.resolve(start) as any).nodeAfter;
+ let sm = state.storedMarks || undefined;
+ let ruleProvider = FormattedTextBox.InputBoxOverlay!.props.ruleProvider;
+ let heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading);
+ if (ruleProvider && heading) {
+ ruleProvider["ruleAlign_" + heading] = "left";
+ }
+ return node ? state.tr.deleteRange(start, end).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
+ }),
+ new InputRule(
+ new RegExp(/^\]\]\s$/),
+ (state, match, start, end) => {
+ let node = (state.doc.resolve(start) as any).nodeAfter;
+ let sm = state.storedMarks || undefined;
+ let ruleProvider = FormattedTextBox.InputBoxOverlay!.props.ruleProvider;
+ let heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading);
+ if (ruleProvider && heading) {
+ ruleProvider["ruleAlign_" + heading] = "right";
+ }
+ return node ? state.tr.deleteRange(start, end).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
+ }),
+ new InputRule(
+ new RegExp(/\^f\s$/),
+ (state, match, start, end) => {
+ let newNode = schema.nodes.footnote.create({});
+ let tr = state.tr;
+ tr.deleteRange(start, end).replaceSelectionWith(newNode); // replace insertion with a footnote.
+ return tr.setSelection(new NodeSelection( // select the footnote node to open its display
+ tr.doc.resolve( // get the location of the footnote node by subtracting the nodesize of the footnote from the current insertion point anchor (which will be immediately after the footnote node)
+ tr.selection.anchor - tr.selection.$anchor.nodeBefore!.nodeSize)));
+ }),
+ // let newNode = schema.nodes.footnote.create({});
+ // if (dispatch && state.selection.from === state.selection.to) {
+ // return true;
+ // }
]
};
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 6d2abfaa2..f027a4bf7 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -1,6 +1,18 @@
-import { DOMOutputSpecArray, MarkSpec, Node, NodeSpec, Schema, Slice } from "prosemirror-model";
+import { baseKeymap, toggleMark } from "prosemirror-commands";
+import { redo, undo } from "prosemirror-history";
+import { keymap } from "prosemirror-keymap";
+import { DOMOutputSpecArray, Fragment, MarkSpec, Node, NodeSpec, Schema, Slice } from "prosemirror-model";
import { bulletList, listItem, orderedList } from 'prosemirror-schema-list';
-import { TextSelection } from "prosemirror-state";
+import { EditorState, TextSelection } from "prosemirror-state";
+import { StepMap } from "prosemirror-transform";
+import { EditorView } from "prosemirror-view";
+import { Doc } from "../../new_fields/Doc";
+import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
+import { DocServer } from "../DocServer";
+import { Cast, NumCast } from "../../new_fields/Types";
+import { DocumentManager } from "./DocumentManager";
+import ParagraphNodeSpec from "./ParagraphNodeSpec";
+import { times } from "async";
const pDOM: DOMOutputSpecArray = ["p", 0], blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"],
preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0];
@@ -13,23 +25,32 @@ export const nodes: { [index: string]: NodeSpec } = {
content: "block+"
},
- // :: NodeSpec A plain paragraph textblock. Represented in the DOM
- // as a `<p>` element.
- paragraph: {
- content: "inline*",
- group: "block",
- parseDOM: [{ tag: "p" }],
- toDOM() { return pDOM; }
- },
- // starmine: {
- // inline: true,
- // attrs: { oldtext: { default: "" } },
- // group: "inline",
- // toDOM() { return ["star", "㊉"]; },
- // parseDOM: [{ tag: "star" }]
+ footnote: {
+ group: "inline",
+ content: "inline*",
+ inline: true,
+ attrs: {
+ visibility: { default: false }
+ },
+ // This makes the view treat the node as a leaf, even though it
+ // technically has content
+ atom: true,
+ toDOM: () => ["footnote", 0],
+ parseDOM: [{ tag: "footnote" }]
+ },
+
+ // // :: NodeSpec A plain paragraph textblock. Represented in the DOM
+ // // as a `<p>` element.
+ // paragraph: {
+ // content: "inline*",
+ // group: "block",
+ // parseDOM: [{ tag: "p" }],
+ // toDOM() { return pDOM; }
// },
+ paragraph: ParagraphNodeSpec,
+
// :: NodeSpec A blockquote (`<blockquote>`) wrapping one or more blocks.
blockquote: {
content: "block+",
@@ -87,8 +108,6 @@ export const nodes: { [index: string]: NodeSpec } = {
visibility: { default: false },
text: { default: undefined },
textslice: { default: undefined },
- textlen: { default: 0 }
-
},
group: "inline",
toDOM(node) {
@@ -105,7 +124,6 @@ export const nodes: { [index: string]: NodeSpec } = {
// }
// }]
},
-
// :: NodeSpec An inline image (`<img>`) node. Supports `src`,
// `alt`, and `href` attributes. The latter two default to the empty
// string.
@@ -113,9 +131,11 @@ export const nodes: { [index: string]: NodeSpec } = {
inline: true,
attrs: {
src: {},
- width: { default: "100px" },
+ width: { default: 100 },
alt: { default: null },
- title: { default: null }
+ title: { default: null },
+ float: { default: "left" },
+ docid: { default: "" }
},
group: "inline",
draggable: true,
@@ -129,7 +149,7 @@ export const nodes: { [index: string]: NodeSpec } = {
};
}
}],
- // TODO if we don't define toDom, something weird happens: dragging the image will not move it but clone it. Why?
+ // TODO if we don't define toDom, dragging the image crashes. Why?
toDOM(node) {
const attrs = { style: `width: ${node.attrs.width}` };
return ["img", { ...node.attrs, ...attrs }];
@@ -174,36 +194,54 @@ export const nodes: { [index: string]: NodeSpec } = {
ordered_list: {
...orderedList,
content: 'list_item+',
- group: 'block'
+ group: 'block',
+ attrs: {
+ bulletStyle: { default: 0 },
+ mapStyle: { default: "decimal" },
+ visibility: { default: true }
+ },
+ toDOM(node: Node<any>) {
+ const bs = node.attrs.bulletStyle;
+ const decMap = bs ? "decimal" + bs : "";
+ const multiMap = bs === 1 ? "decimal1" : bs === 2 ? "upper-alpha" : bs === 3 ? "lower-roman" : bs === 4 ? "lower-alpha" : "";
+ let map = node.attrs.mapStyle === "decimal" ? decMap : multiMap;
+ return node.attrs.visibility ? ['ol', { class: `${map}-ol`, style: `list-style: none;` }, 0] :
+ ['ol', { class: `${map}-ol`, style: `list-style: none;` }];
+ }
},
- //this doesn't currently work for some reason
+
bullet_list: {
...bulletList,
content: 'list_item+',
group: 'block',
// parseDOM: [{ tag: "ul" }, { style: 'list-style-type=disc' }],
- // toDOM() { return ulDOM }
+ toDOM(node: Node<any>) {
+ return ['ul', 0];
+ }
},
- //bullet_list: {
- // content: 'list_item+',
- // group: 'block',
- //active: blockActive(schema.nodes.bullet_list),
- //enable: wrapInList(schema.nodes.bullet_list),
- //run: wrapInList(schema.nodes.bullet_list),
- //select: state => true,
- // },
list_item: {
+ attrs: {
+ bulletStyle: { default: 0 },
+ mapStyle: { default: "decimal" },
+ visibility: { default: true }
+ },
...listItem,
- content: 'paragraph block*'
+ content: 'paragraph block*',
+ toDOM(node: any) {
+ const bs = node.attrs.bulletStyle;
+ const decMap = bs ? "decimal" + bs : "";
+ const multiMap = bs === 1 ? "decimal1" : bs === 2 ? "upper-alpha" : bs === 3 ? "lower-roman" : bs === 4 ? "lower-alpha" : "";
+ let map = node.attrs.mapStyle === "decimal" ? decMap : multiMap;
+ return node.attrs.visibility ? ["li", { class: `${map}` }, 0] : ["li", { class: `${map}` }, "..."];
+ //return ["li", { class: `${map}` }, 0];
+ }
},
-
};
const emDOM: DOMOutputSpecArray = ["em", 0];
const strongDOM: DOMOutputSpecArray = ["strong", 0];
const codeDOM: DOMOutputSpecArray = ["code", 0];
-const underlineDOM: DOMOutputSpecArray = ["underline", 0];
// :: Object [Specs](#model.MarkSpec) for the marks in the schema.
export const marks: { [index: string]: MarkSpec } = {
@@ -214,7 +252,8 @@ export const marks: { [index: string]: MarkSpec } = {
attrs: {
href: {},
location: { default: null },
- title: { default: null }
+ title: { default: null },
+ docref: { default: false }
},
inclusive: false,
parseDOM: [{
@@ -222,13 +261,17 @@ export const marks: { [index: string]: MarkSpec } = {
return { href: dom.getAttribute("href"), location: dom.getAttribute("location"), title: dom.getAttribute("title") };
}
}],
- toDOM(node: any) { return ["a", node.attrs, 0]; }
+ toDOM(node: any) {
+ return node.attrs.docref && node.attrs.title ?
+ ["div", ["span", `"`], ["span", 0], ["span", `"`], ["br"], ["a", { ...node.attrs, class: "prosemirror-attribution" }, node.attrs.title], ["br"]] :
+ ["a", { ...node.attrs }, 0];
+ }
},
// :: MarkSpec An emphasis mark. Rendered as an `<em>` element.
// Has parse rules that also match `<i>` and `font-style: italic`.
em: {
- parseDOM: [{ tag: "i" }, { tag: "em" }, { style: "font-style=italic" }],
+ parseDOM: [{ tag: "i" }, { tag: "em" }, { style: "font-style: italic" }],
toDOM() { return emDOM; }
},
@@ -241,16 +284,6 @@ export const marks: { [index: string]: MarkSpec } = {
toDOM() { return strongDOM; }
},
- underline: {
- parseDOM: [
- { tag: 'u' },
- { style: 'text-decoration=underline' }
- ],
- toDOM: () => ['span', {
- style: 'text-decoration:underline'
- }]
- },
-
strikethrough: {
parseDOM: [
{ tag: 'strike' },
@@ -280,15 +313,79 @@ export const marks: { [index: string]: MarkSpec } = {
toDOM: () => ['sup']
},
+ mbulletType: {
+ attrs: {
+ bulletType: { default: "decimal" }
+ },
+ toDOM(node: any) {
+ return ['span', {
+ style: `background: ${node.attrs.bulletType === "decimal" ? "yellow" : node.attrs.bulletType === "upper-alpha" ? "blue" : "green"}`
+ }];
+ }
+ },
+
+ metadata: {
+ toDOM() {
+ return ['span', { style: 'font-size:75%; background:rgba(100, 100, 100, 0.2); ' }];
+ }
+ },
+ metadataKey: {
+ toDOM() {
+ return ['span', { style: 'font-style:italic; ' }];
+ }
+ },
+ metadataVal: {
+ toDOM() {
+ return ['span'];
+ }
+ },
+
highlight: {
- parseDOM: [{ style: 'color: blue' }],
+ parseDOM: [
+ {
+ tag: "span",
+ getAttrs: (p: any) => {
+ if (typeof (p) !== "string") {
+ let style = getComputedStyle(p);
+ if (style.textDecoration === "underline") return null;
+ if (p.parentElement.outerHTML.indexOf("text-decoration: underline") !== -1 &&
+ p.parentElement.outerHTML.indexOf("text-decoration-style: dotted") !== -1)
+ return null;
+ }
+ return false;
+ }
+ },
+ ],
+ inclusive: true,
toDOM() {
return ['span', {
- style: 'color: blue'
+ style: 'text-decoration: underline; text-decoration-style: dotted; text-decoration-color: rgba(204, 206, 210, 0.92)'
}];
}
},
+ underline: {
+ parseDOM: [
+ {
+ tag: "span",
+ getAttrs: (p: any) => {
+ if (typeof (p) !== "string") {
+ let style = getComputedStyle(p);
+ if (style.textDecoration === "underline")
+ return null;
+ if (p.parentElement.outerHTML.indexOf("text-decoration-style:line") !== -1)
+ return null;
+ }
+ return false;
+ }
+ }
+ // { style: "text-decoration=underline" }
+ ],
+ toDOM: () => ['span', {
+ style: 'text-decoration:underline;text-decoration-style:line'
+ }]
+ },
+
search_highlight: {
parseDOM: [{ style: 'background: yellow' }],
toDOM() {
@@ -298,6 +395,27 @@ export const marks: { [index: string]: MarkSpec } = {
}
},
+ // the id of the user who entered the text
+ user_mark: {
+ attrs: {
+ userid: { default: "" },
+ hide_users: { default: [] },
+ opened: { default: true },
+ modified: { default: "when?" }
+ },
+ group: "inline",
+ toDOM(node: any) {
+ let hideUsers = node.attrs.hide_users;
+ let hidden = hideUsers.indexOf(node.attrs.userid) !== -1 || (hideUsers.length === 0 && node.attrs.userid !== Doc.CurrentUserEmail);
+ return hidden ?
+ (node.attrs.opened ?
+ ['span', { class: "userMarkOpen" }, 0] :
+ ['span', { class: "userMark" }, ['span', 0]]
+ ) :
+ ['span', 0];
+ }
+ },
+
// :: MarkSpec Code font mark. Represented as a `<code>` element.
code: {
@@ -305,6 +423,24 @@ export const marks: { [index: string]: MarkSpec } = {
toDOM() { return codeDOM; }
},
+ // pFontFamily: {
+ // attrs: {
+ // style: { default: 'font-family: "Times New Roman", Times, serif;' },
+ // },
+ // parseDOM: [{
+ // tag: "span", getAttrs(dom: any) {
+ // if (getComputedStyle(dom).font === "Times New Roman") return { style: `font-family: "Times New Roman", Times, serif;` };
+ // if (getComputedStyle(dom).font === "Arial, Helvetica") return { style: `font-family: Arial, Helvetica, sans-serif;` };
+ // if (getComputedStyle(dom).font === "Georgia") return { style: `font-family: Georgia, serif;` };
+ // if (getComputedStyle(dom).font === "Comic Sans") return { style: `font-family: "Comic Sans MS", cursive, sans-serif;` };
+ // if (getComputedStyle(dom).font === "Tahoma, Geneva") return { style: `font-family: Tahoma, Geneva, sans-serif;` };
+ // }
+ // }],
+ // toDOM: (node: any) => ['span', {
+ // style: node.attrs.style
+ // }]
+ // },
+
/* FONTS */
timesNewRoman: {
@@ -373,7 +509,6 @@ export const marks: { [index: string]: MarkSpec } = {
attrs: {
fontSize: { default: 10 }
},
- inclusive: false,
parseDOM: [{ style: 'font-size: 10px;' }],
toDOM: (node) => ['span', {
style: `font-size: ${node.attrs.fontSize}px;`
@@ -450,15 +585,12 @@ export const marks: { [index: string]: MarkSpec } = {
}]
},
};
-function getFontSize(element: any) {
- return parseFloat((getComputedStyle(element) as any).fontSize);
-}
export class ImageResizeView {
_handle: HTMLElement;
_img: HTMLElement;
_outer: HTMLElement;
- constructor(node: any, view: any, getPos: any) {
+ constructor(node: any, view: any, getPos: any, addDocTab: any) {
this._handle = document.createElement("span");
this._img = document.createElement("img");
this._outer = document.createElement("span");
@@ -466,6 +598,7 @@ export class ImageResizeView {
this._outer.style.width = node.attrs.width;
this._outer.style.display = "inline-block";
this._outer.style.overflow = "hidden";
+ (this._outer.style as any).float = node.attrs.float;
this._img.setAttribute("src", node.attrs.src);
this._img.style.width = "100%";
@@ -478,6 +611,33 @@ export class ImageResizeView {
this._handle.style.bottom = "-10px";
this._handle.style.right = "-10px";
let self = this;
+ this._img.onpointerdown = function (e: any) {
+ if (!view.isOverlay || e.ctrlKey) {
+ e.preventDefault();
+ e.stopPropagation();
+ DocServer.GetRefField(node.attrs.docid).then(async linkDoc => {
+ if (linkDoc instanceof Doc) {
+ let proto = Doc.GetProto(linkDoc);
+ let targetContext = await Cast(proto.targetContext, Doc);
+ let jumpToDoc = await Cast(linkDoc.anchor2, Doc);
+ if (jumpToDoc) {
+ if (DocumentManager.Instance.getDocumentView(jumpToDoc)) {
+
+ DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, undefined, undefined, NumCast((jumpToDoc === linkDoc.anchor2 ? linkDoc.anchor2Page : linkDoc.anchor1Page)));
+ return;
+ }
+ }
+ if (targetContext) {
+ DocumentManager.Instance.jumpToDocument(targetContext, e.ctrlKey, false, document => addDocTab(document, undefined, location ? location : "inTab"));
+ } else if (jumpToDoc) {
+ DocumentManager.Instance.jumpToDocument(jumpToDoc, e.ctrlKey, false, document => addDocTab(document, undefined, location ? location : "inTab"));
+ } else {
+ DocumentManager.Instance.jumpToDocument(linkDoc, e.ctrlKey, false, document => addDocTab(document, undefined, location ? location : "inTab"));
+ } e.ctrlKey
+ }
+ });
+ }
+ }
this._handle.onpointerdown = function (e: any) {
e.preventDefault();
e.stopPropagation();
@@ -487,15 +647,18 @@ export class ImageResizeView {
const currentX = e.pageX;
const diffInPx = currentX - startX;
self._outer.style.width = `${startWidth + diffInPx}`;
+ //Array.from(FormattedTextBox.InputBoxOverlay!.CurrentDiv.getElementsByTagName("img")).map((img: any) => img.opacity = "0.1");
+ FormattedTextBox.InputBoxOverlay!.CurrentDiv.style.opacity = "0";
};
const onpointerup = () => {
document.removeEventListener("pointermove", onpointermove);
document.removeEventListener("pointerup", onpointerup);
view.dispatch(
- view.state.tr.setNodeMarkup(getPos(), null,
- { src: node.attrs.src, width: self._outer.style.width })
- .setSelection(view.state.selection));
+ view.state.tr.setSelection(view.state.selection).setNodeMarkup(getPos(), null,
+ { ...node.attrs, width: self._outer.style.width })
+ );
+ FormattedTextBox.InputBoxOverlay!.CurrentDiv.style.opacity = "1";
};
document.addEventListener("pointermove", onpointermove);
@@ -520,72 +683,186 @@ export class ImageResizeView {
}
}
+export class OrderedListView {
+ update(node: any) {
+ return false; // if attr's of an ordered_list (e.g., bulletStyle) change, return false forces the dom node to be recreated which is necessary for the bullet labels to update
+ }
+}
+
+export class FootnoteView {
+ innerView: any;
+ outerView: any;
+ node: any;
+ dom: any;
+ getPos: any;
+
+ constructor(node: any, view: any, getPos: any) {
+ // We'll need these later
+ this.node = node;
+ this.outerView = view;
+ this.getPos = getPos;
+
+ // The node's representation in the editor (empty, for now)
+ this.dom = document.createElement("footnote");
+ this.dom.addEventListener("pointerup", this.toggle, true);
+ // These are used when the footnote is selected
+ this.innerView = null;
+ }
+ selectNode() {
+ const attrs = { ...this.node.attrs };
+ attrs.visibility = true;
+ this.dom.classList.add("ProseMirror-selectednode");
+ if (!this.innerView) this.open();
+ }
+
+ deselectNode() {
+ const attrs = { ...this.node.attrs };
+ attrs.visibility = false;
+ this.dom.classList.remove("ProseMirror-selectednode");
+ if (this.innerView) this.close();
+ }
+ open() {
+ if (!this.outerView.isOverlay) return;
+ // Append a tooltip to the outer node
+ let tooltip = this.dom.appendChild(document.createElement("div"));
+ tooltip.className = "footnote-tooltip";
+ // And put a sub-ProseMirror into that
+ this.innerView = new EditorView(tooltip, {
+ // You can use any node as an editor document
+ state: EditorState.create({
+ doc: this.node,
+ plugins: [keymap(baseKeymap),
+ keymap({
+ "Mod-z": () => undo(this.outerView.state, this.outerView.dispatch),
+ "Mod-y": () => redo(this.outerView.state, this.outerView.dispatch),
+ "Mod-b": toggleMark(schema.marks.strong)
+ })]
+ }),
+ // This is the magic part
+ dispatchTransaction: this.dispatchInner.bind(this),
+ handleDOMEvents: {
+ pointerdown: ((view: any, e: PointerEvent) => {
+ // Kludge to prevent issues due to the fact that the whole
+ // footnote is node-selected (and thus DOM-selected) when
+ // the parent editor is focused.
+ e.stopPropagation();
+ document.addEventListener("pointerup", this.ignore, true);
+ if (this.outerView.hasFocus()) this.innerView.focus();
+ }) as any
+ }
+
+ });
+ setTimeout(() => this.innerView && this.innerView.docView.setSelection(0, 0, this.innerView.root, true), 0);
+ }
+
+ ignore = (e: PointerEvent) => {
+ e.stopPropagation();
+ document.removeEventListener("pointerup", this.ignore, true);
+ }
+
+ toggle = () => {
+ if (this.innerView) this.close();
+ else {
+ this.open();
+ }
+ }
+ close() {
+ this.innerView && this.innerView.destroy();
+ this.innerView = null;
+ this.dom.textContent = "";
+ }
+ dispatchInner(tr: any) {
+ let { state, transactions } = this.innerView.state.applyTransaction(tr);
+ this.innerView.updateState(state);
+
+ if (!tr.getMeta("fromOutside")) {
+ let outerTr = this.outerView.state.tr, offsetMap = StepMap.offset(this.getPos() + 1)
+ for (let i = 0; i < transactions.length; i++) {
+ let steps = transactions[i].steps;
+ for (let j = 0; j < steps.length; j++) {
+ outerTr.step(steps[j].map(offsetMap));
+ }
+ }
+ if (outerTr.docChanged) this.outerView.dispatch(outerTr);
+ }
+ }
+ update(node: any) {
+ if (!node.sameMarkup(this.node)) return false;
+ this.node = node;
+ if (this.innerView) {
+ let state = this.innerView.state;
+ let start = node.content.findDiffStart(state.doc.content);
+ if (start !== null) {
+ let { a: endA, b: endB } = node.content.findDiffEnd(state.doc.content);
+ let overlap = start - Math.min(endA, endB);
+ if (overlap > 0) { endA += overlap; endB += overlap; }
+ this.innerView.dispatch(
+ state.tr
+ .replace(start, endB, node.slice(start, endA))
+ .setMeta("fromOutside", true));
+ }
+ }
+ return true;
+ }
+
+ destroy() {
+ if (this.innerView) this.close();
+ }
+
+ stopEvent(event: any) {
+ return this.innerView && this.innerView.dom.contains(event.target);
+ }
+
+ ignoreMutation() { return true; }
+}
+
export class SummarizedView {
- // TODO: highlight text that is summarized. to find end of region, walk along mark
_collapsed: HTMLElement;
_view: any;
constructor(node: any, view: any, getPos: any) {
this._collapsed = document.createElement("span");
- this._collapsed.textContent = node.attrs.visibility ? "㊀" : "㊉";
- this._collapsed.style.opacity = "0.5";
- this._collapsed.style.position = "relative";
- this._collapsed.style.width = "40px";
- this._collapsed.style.height = "20px";
- let self = this;
+ this._collapsed.className = this.className(node.attrs.visibility);
this._view = view;
const js = node.toJSON;
node.toJSON = function () {
-
return js.apply(this, arguments);
};
- this._collapsed.onpointerdown = function (e: any) {
- if (node.attrs.visibility) {
- // node.attrs.visibility = !node.attrs.visibility;
- let y = getPos();
- const attrs = { ...node.attrs };
- attrs.visibility = !attrs.visibility;
- let { from, to } = self.updateSummarizedText(y + 1, view.state.schema.marks.highlight);
- let length = to - from;
- let newSelection = TextSelection.create(view.state.doc, y + 1, y + 1 + length);
- // update attrs of node
- attrs.text = newSelection.content();
- attrs.textslice = newSelection.content().toJSON();
- view.dispatch(view.state.tr.setNodeMarkup(y, undefined, attrs));
- view.dispatch(view.state.tr.setSelection(newSelection).deleteSelection(view.state, () => { }));
- self._collapsed.textContent = "㊉";
- } else {
- // node.attrs.visibility = !node.attrs.visibility;
- let y = getPos();
- const attrs = { ...node.attrs };
- attrs.visibility = !attrs.visibility;
- view.dispatch(view.state.tr.setNodeMarkup(y, undefined, attrs));
- let mark = view.state.schema.mark(view.state.schema.marks.highlight);
- view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.doc, y + 1, y + 1)));
- const from = view.state.selection.from;
- let size = node.attrs.text.size;
- view.dispatch(view.state.tr.replaceSelection(node.attrs.text).addMark(from, from + size, mark).removeStoredMark(mark));
- self._collapsed.textContent = "㊀";
+
+ this._collapsed.onpointerdown = (e: any) => {
+ const visible = !node.attrs.visibility;
+ const attrs = { ...node.attrs, visibility: visible };
+ let textSelection = TextSelection.create(view.state.doc, getPos() + 1);
+ if (!visible) { // update summarized text and save in attrs
+ textSelection = this.updateSummarizedText(getPos() + 1);
+ attrs.text = textSelection.content();
+ attrs.textslice = attrs.text.toJSON();
}
+ view.dispatch(view.state.tr.
+ setSelection(textSelection). // select the current summarized text (or where it will be if its collapsed)
+ replaceSelection(!visible ? new Slice(Fragment.fromArray([]), 0, 0) : node.attrs.text). // collapse/expand it
+ setNodeMarkup(getPos(), undefined, attrs)); // update the attrs
e.preventDefault();
e.stopPropagation();
+ this._collapsed.className = this.className(visible);
};
(this as any).dom = this._collapsed;
-
- }
- selectNode() {
}
+ selectNode() { }
+
+ deselectNode() { }
+
+ className = (visible: boolean) => "formattedTextBox-summarizer" + (visible ? "" : "-collapsed");
- updateSummarizedText(start?: any, mark?: any) {
- let $start = this._view.state.doc.resolve(start);
+ updateSummarizedText(start?: any) {
+ let mark = this._view.state.schema.marks.highlight.create();
let endPos = start;
- let _mark = this._view.state.schema.mark(this._view.state.schema.marks.highlight);
let visited = new Set();
for (let i: number = start + 1; i < this._view.state.doc.nodeSize - 1; i++) {
let skip = false;
this._view.state.doc.nodesBetween(start, i, (node: Node, pos: number, parent: Node, index: number) => {
if (node.isLeaf && !visited.has(node) && !skip) {
- if (node.marks.includes(_mark)) {
+ if (node.marks.find((m: any) => m.type === mark.type)) {
visited.add(node);
endPos = i + node.nodeSize - 1;
}
@@ -593,10 +870,7 @@ export class SummarizedView {
}
});
}
- return { from: start, to: endPos };
- }
-
- deselectNode() {
+ return TextSelection.create(this._view.state.doc, start, endPos);
}
}
// :: Schema
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index ee623d082..9efef888d 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -4,7 +4,6 @@ import { DocumentView } from "../views/nodes/DocumentView";
import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
import { NumCast, StrCast } from "../../new_fields/Types";
import { InkingControl } from "../views/InkingControl";
-import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
export namespace SelectionManager {
diff --git a/src/client/util/TooltipLinkingMenu.tsx b/src/client/util/TooltipLinkingMenu.tsx
index 55e0eb909..e6d6c471f 100644
--- a/src/client/util/TooltipLinkingMenu.tsx
+++ b/src/client/util/TooltipLinkingMenu.tsx
@@ -1,23 +1,9 @@
-import { action, IReactionDisposer, reaction } from "mobx";
-import { Dropdown, DropdownSubmenu, MenuItem, MenuItemSpec, renderGrouped, icons, } from "prosemirror-menu"; //no import css
-import { baseKeymap, lift } from "prosemirror-commands";
-import { history, redo, undo } from "prosemirror-history";
-import { keymap } from "prosemirror-keymap";
-import { EditorState, Transaction, NodeSelection, TextSelection } from "prosemirror-state";
+import { EditorState } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
-import { schema } from "./RichTextSchema";
-import { Schema, NodeType, MarkType } from "prosemirror-model";
-import React = require("react");
+import { FieldViewProps } from "../views/nodes/FieldView";
import "./TooltipTextMenu.scss";
+import React = require("react");
const { toggleMark, setBlockType, wrapIn } = require("prosemirror-commands");
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { wrapInList, bulletList, liftListItem, listItem } from 'prosemirror-schema-list';
-import {
- faListUl,
-} from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { FieldViewProps } from "../views/nodes/FieldView";
-import { throwStatement } from "babel-types";
const SVG = "http://www.w3.org/2000/svg";
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index 4672dd246..84d045e6f 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -3,8 +3,8 @@ import { faListUl } from '@fortawesome/free-solid-svg-icons';
import { action, observable } from "mobx";
import { Dropdown, icons, MenuItem } from "prosemirror-menu"; //no import css
import { Mark, MarkType, Node as ProsNode, NodeType, ResolvedPos, Schema } from "prosemirror-model";
-import { liftListItem, wrapInList } from 'prosemirror-schema-list';
-import { EditorState, NodeSelection, TextSelection } from "prosemirror-state";
+import { wrapInList } from 'prosemirror-schema-list';
+import { EditorState, NodeSelection, TextSelection, Transaction } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { Doc, Field, Opt } from "../../new_fields/Doc";
import { Id } from "../../new_fields/FieldSymbols";
@@ -18,6 +18,7 @@ import { DragManager } from "./DragManager";
import { LinkManager } from "./LinkManager";
import { schema } from "./RichTextSchema";
import "./TooltipTextMenu.scss";
+import { Cast, NumCast } from '../../new_fields/Types';
const { toggleMark, setBlockType } = require("prosemirror-commands");
const { openPrompt, TextField } = require("./ProsemirrorCopy/prompt.js");
@@ -28,11 +29,11 @@ export class TooltipTextMenu {
private view: EditorView;
private fontStyles: MarkType[];
private fontSizes: MarkType[];
- private listTypes: NodeType[];
+ private listTypes: (NodeType | any)[];
private editorProps: FieldViewProps & FormattedTextBoxProps;
private fontSizeToNum: Map<MarkType, number>;
private fontStylesToName: Map<MarkType, string>;
- private listTypeToIcon: Map<NodeType, string>;
+ private listTypeToIcon: Map<NodeType | any, string>;
//private link: HTMLAnchorElement;
private wrapper: HTMLDivElement;
private extras: HTMLDivElement;
@@ -60,8 +61,6 @@ export class TooltipTextMenu {
@observable
private _storedMarks: Mark<any>[] | null | undefined;
- public HackToFixTextSelectionGlitch: boolean = false;
-
constructor(view: EditorView, editorProps: FieldViewProps & FormattedTextBoxProps) {
this.view = view;
@@ -165,13 +164,22 @@ export class TooltipTextMenu {
this.fontSizeToNum.set(schema.marks.p48, 48);
this.fontSizeToNum.set(schema.marks.p72, 72);
this.fontSizeToNum.set(schema.marks.pFontSize, 10);
- this.fontSizeToNum.set(schema.marks.pFontSize, 10);
+ // this.fontSizeToNum.set(schema.marks.pFontSize, 12);
+ // this.fontSizeToNum.set(schema.marks.pFontSize, 14);
+ // this.fontSizeToNum.set(schema.marks.pFontSize, 16);
+ // this.fontSizeToNum.set(schema.marks.pFontSize, 18);
+ // this.fontSizeToNum.set(schema.marks.pFontSize, 20);
+ // this.fontSizeToNum.set(schema.marks.pFontSize, 24);
+ // this.fontSizeToNum.set(schema.marks.pFontSize, 32);
+ // this.fontSizeToNum.set(schema.marks.pFontSize, 48);
+ // this.fontSizeToNum.set(schema.marks.pFontSize, 72);
this.fontSizes = Array.from(this.fontSizeToNum.keys());
//list types
this.listTypeToIcon = new Map();
this.listTypeToIcon.set(schema.nodes.bullet_list, ":");
- this.listTypeToIcon.set(schema.nodes.ordered_list, "1)");
+ this.listTypeToIcon.set(schema.nodes.ordered_list.create({ mapStyle: "decimal" }), "1.1");
+ this.listTypeToIcon.set(schema.nodes.ordered_list.create({ mapStyle: "multi" }), "1.A");
// this.listTypeToIcon.set(schema.nodes.bullet_list, "⬜");
this.listTypes = Array.from(this.listTypeToIcon.keys());
@@ -300,10 +308,9 @@ export class TooltipTextMenu {
{
handlers: {
dragComplete: action(() => {
- // let m = dragData.droppedDocuments;
let linkDoc = dragData.linkDocument;
let proto = Doc.GetProto(linkDoc);
- if (docView && docView.props.ContainingCollectionView) {
+ if (proto && docView && docView.props.ContainingCollectionView) {
proto.sourceContext = docView.props.ContainingCollectionView.props.Document;
}
linkDoc instanceof Doc && this.makeLink(Utils.prepend("/doc/" + linkDoc[Id]), ctrlKey ? "onRight" : "inTab");
@@ -315,8 +322,6 @@ export class TooltipTextMenu {
e.preventDefault();
};
this.linkEditor.appendChild(this.linkDrag);
- // this.linkEditor.appendChild(this.linkText);
- // this.linkEditor.appendChild(linkBtn);
this.tooltip.appendChild(this.linkEditor);
}
@@ -425,11 +430,13 @@ export class TooltipTextMenu {
}
public static insertStar(state: EditorState<any>, dispatch: any) {
- let newNode = schema.nodes.star.create({ visibility: false, text: state.selection.content(), textslice: state.selection.content().toJSON(), textlen: state.selection.to - state.selection.from });
- if (dispatch) {
- //console.log(newNode.attrs.text.toString());
- dispatch(state.tr.replaceSelectionWith(newNode));
- }
+ if (state.selection.empty) return false;
+ let mark = state.schema.marks.highlight.create();
+ let tr = state.tr;
+ tr.addMark(state.selection.from, state.selection.to, mark);
+ let content = tr.selection.content();
+ let newNode = schema.nodes.star.create({ visibility: false, text: content, textslice: content.toJSON() });
+ dispatch && dispatch(tr.replaceSelectionWith(newNode).removeMark(tr.selection.from - 1, tr.selection.from, mark));
return true;
}
@@ -489,10 +496,20 @@ export class TooltipTextMenu {
if (markType.name[0] === 'p') {
let size = this.fontSizeToNum.get(markType);
if (size) { this.updateFontSizeDropdown(String(size) + " pt"); }
+ let ruleProvider = this.editorProps.ruleProvider;
+ let heading = NumCast(this.editorProps.Document.heading);
+ if (ruleProvider && heading) {
+ ruleProvider["ruleSize_" + heading] = size;
+ }
}
else {
let fontName = this.fontStylesToName.get(markType);
if (fontName) { this.updateFontStyleDropdown(fontName); }
+ let ruleProvider = this.editorProps.ruleProvider;
+ let heading = NumCast(this.editorProps.Document.heading);
+ if (ruleProvider && heading) {
+ ruleProvider["ruleFont_" + heading] = fontName;
+ }
}
//actually apply font
return toggleMark(markType)(view.state, view.dispatch, view);
@@ -502,12 +519,37 @@ export class TooltipTextMenu {
}
}
+ updateBullets = (tx2: Transaction, style: string) => {
+ tx2.doc.descendants((node: any, offset: any, index: any) => {
+ if (node.type === schema.nodes.ordered_list || node.type === schema.nodes.list_item) {
+ let path = (tx2.doc.resolve(offset) as any).path;
+ let depth = Array.from(path).reduce((p: number, c: any) => p + (c.hasOwnProperty("type") && (c as any).type === schema.nodes.ordered_list ? 1 : 0), 0);
+ if (node.type === schema.nodes.ordered_list) depth++;
+ tx2.setNodeMarkup(offset, node.type, { mapStyle: style, bulletStyle: depth }, node.marks);
+ }
+ });
+ };
//remove all node typeand apply the passed-in one to the selected text
- changeToNodeType(nodeType: NodeType | undefined, view: EditorView) {
- //remove old
- liftListItem(schema.nodes.list_item)(view.state, view.dispatch);
- if (nodeType) { //add new
+ changeToNodeType = (nodeType: NodeType | undefined, view: EditorView) => {
+ //remove oldif (nodeType) { //add new
+ if (nodeType === schema.nodes.bullet_list) {
wrapInList(nodeType)(view.state, view.dispatch);
+ } else {
+ var marks = view.state.storedMarks || (view.state.selection.$to.parentOffset && view.state.selection.$from.marks());
+ if (!wrapInList(schema.nodes.ordered_list)(view.state, (tx2: any) => {
+ this.updateBullets(tx2, (nodeType as any).attrs.mapStyle);
+ marks && tx2.ensureMarks([...marks]);
+ marks && tx2.setStoredMarks([...marks]);
+
+ view.dispatch(tx2);
+ })) {
+ let tx2 = view.state.tr;
+ this.updateBullets(tx2, (nodeType as any).attrs.mapStyle);
+ marks && tx2.ensureMarks([...marks]);
+ marks && tx2.setStoredMarks([...marks]);
+
+ view.dispatch(tx2);
+ }
}
}
@@ -602,10 +644,9 @@ export class TooltipTextMenu {
if (!this.view.state.selection.empty && $from && $from.nodeAfter) {
if (this._brushMarks && to - from > 0) {
this.view.dispatch(this.view.state.tr.removeMark(from, to));
- this._brushMarks.forEach((mark: Mark) => {
+ Array.from(this._brushMarks).filter(m => m.type !== schema.marks.user_mark).forEach((mark: Mark) => {
const markType = mark.type;
this.changeToMarkInGroup(markType, this.view, []);
-
});
}
}
@@ -849,8 +890,6 @@ export class TooltipTextMenu {
this.updateFontSizeDropdown("Various");
}
}
- !this.HackToFixTextSelectionGlitch &&
- this.view.dispatch(this.view.state.tr.setStoredMarks(this._activeMarks)); // bcz: what's the purpose of this line? It messes up text selection without the Hack.
this.update_mark_doms();
}
diff --git a/src/client/util/clamp.js b/src/client/util/clamp.js
new file mode 100644
index 000000000..9c7fd78a4
--- /dev/null
+++ b/src/client/util/clamp.js
@@ -0,0 +1,15 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.default = clamp;
+function clamp(min, val, max) {
+ if (val < min) {
+ return min;
+ }
+ if (val > max) {
+ return max;
+ }
+ return val;
+} \ No newline at end of file
diff --git a/src/client/util/convertToCSSPTValue.js b/src/client/util/convertToCSSPTValue.js
new file mode 100644
index 000000000..179557953
--- /dev/null
+++ b/src/client/util/convertToCSSPTValue.js
@@ -0,0 +1,43 @@
+'use strict';
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.PT_TO_PX_RATIO = exports.PX_TO_PT_RATIO = undefined;
+exports.default = convertToCSSPTValue;
+exports.toClosestFontPtSize = toClosestFontPtSize;
+
+// var _FontSizeCommandMenuButton = require('./ui/FontSizeCommandMenuButton');
+
+var SIZE_PATTERN = /([\d\.]+)(px|pt)/i;
+
+var PX_TO_PT_RATIO = exports.PX_TO_PT_RATIO = 0.7518796992481203; // 1 / 1.33.
+var PT_TO_PX_RATIO = exports.PT_TO_PX_RATIO = 1.33;
+
+function convertToCSSPTValue(styleValue) {
+ var matches = styleValue.match(SIZE_PATTERN);
+ if (!matches) {
+ return 0;
+ }
+ var value = parseFloat(matches[1]);
+ var unit = matches[2];
+ if (!value || !unit) {
+ return 0;
+ }
+ if (unit === 'px') {
+ value = PX_TO_PT_RATIO * value;
+ }
+ return value;
+}
+
+function toClosestFontPtSize(styleValue) {
+ var originalPTValue = convertToCSSPTValue(styleValue);
+
+ // if (_FontSizeCommandMenuButton.FONT_PT_SIZES.includes(originalPTValue)) {
+ // return originalPTValue;
+ // }
+
+ return _FontSizeCommandMenuButton.FONT_PT_SIZES.reduce(function (prev, curr) {
+ return Math.abs(curr - originalPTValue) < Math.abs(prev - originalPTValue) ? curr : prev;
+ }, Number.NEGATIVE_INFINITY);
+} \ No newline at end of file
diff --git a/src/client/util/prosemirrorPatches.js b/src/client/util/prosemirrorPatches.js
new file mode 100644
index 000000000..188e3e1c5
--- /dev/null
+++ b/src/client/util/prosemirrorPatches.js
@@ -0,0 +1,139 @@
+'use strict';
+
+Object.defineProperty(exports, '__esModule', { value: true });
+
+var prosemirrorInputRules = require('prosemirror-inputrules');
+var prosemirrorTransform = require('prosemirror-transform');
+var prosemirrorModel = require('prosemirror-model');
+
+exports.liftListItem = liftListItem;
+exports.sinkListItem = sinkListItem;
+exports.wrappingInputRule = wrappingInputRule;
+// :: (NodeType) → (state: EditorState, dispatch: ?(tr: Transaction)) → bool
+// Create a command to lift the list item around the selection up into
+// a wrapping list.
+function liftListItem(itemType) {
+ return function (tx, dispatch) {
+ var ref = tx.selection;
+ var $from = ref.$from;
+ var $to = ref.$to;
+ var range = $from.blockRange($to, function (node) { return node.childCount && node.firstChild.type == itemType; });
+ if (!range) { return false }
+ if (!dispatch) { return true }
+ if ($from.node(range.depth - 1).type == itemType) // Inside a parent list
+ { return liftToOuterList(tx, dispatch, itemType, range) }
+ else // Outer list node
+ { return liftOutOfList(tx, dispatch, range) }
+ }
+}
+
+function liftToOuterList(tr, dispatch, itemType, range) {
+ var end = range.end, endOfList = range.$to.end(range.depth);
+ if (end < endOfList) {
+ // There are siblings after the lifted items, which must become
+ // children of the last item
+ tr.step(new prosemirrorTransform.ReplaceAroundStep(end - 1, endOfList, end, endOfList,
+ new prosemirrorModel.Slice(prosemirrorModel.Fragment.from(itemType.create(null, range.parent.copy())), 1, 0), 1, true));
+ range = new prosemirrorModel.NodeRange(tr.doc.resolve(range.$from.pos), tr.doc.resolve(endOfList), range.depth);
+ }
+ dispatch(tr.lift(range, prosemirrorTransform.liftTarget(range)).scrollIntoView());
+ return true
+}
+
+function liftOutOfList(tr, dispatch, range) {
+ var list = range.parent;
+ // Merge the list items into a single big item
+ for (var pos = range.end, i = range.endIndex - 1, e = range.startIndex; i > e; i--) {
+ pos -= list.child(i).nodeSize;
+ tr.delete(pos - 1, pos + 1);
+ }
+ var $start = tr.doc.resolve(range.start), item = $start.nodeAfter;
+ var atStart = range.startIndex == 0, atEnd = range.endIndex == list.childCount;
+ var parent = $start.node(-1), indexBefore = $start.index(-1);
+ if (!parent.canReplace(indexBefore + (atStart ? 0 : 1), indexBefore + 1,
+ item.content.append(atEnd ? prosemirrorModel.Fragment.empty : prosemirrorModel.Fragment.from(list)))) { return false }
+ var start = $start.pos, end = start + item.nodeSize;
+ // Strip off the surrounding list. At the sides where we're not at
+ // the end of the list, the existing list is closed. At sides where
+ // this is the end, it is overwritten to its end.
+ tr.step(new prosemirrorTransform.ReplaceAroundStep(start - (atStart ? 1 : 0), end + (atEnd ? 1 : 0), start + 1, end - 1,
+ new prosemirrorModel.Slice((atStart ? prosemirrorModel.Fragment.empty : prosemirrorModel.Fragment.from(list.copy(prosemirrorModel.Fragment.empty)))
+ .append(atEnd ? prosemirrorModel.Fragment.empty : prosemirrorModel.Fragment.from(list.copy(prosemirrorModel.Fragment.empty))),
+ atStart ? 0 : 1, atEnd ? 0 : 1), atStart ? 0 : 1));
+ dispatch(tr.scrollIntoView());
+ return true
+}
+
+// :: (NodeType) → (state: EditorState, dispatch: ?(tr: Transaction)) → bool
+// Create a command to sink the list item around the selection down
+// into an inner list.
+function sinkListItem(itemType) {
+ return function (state, dispatch) {
+ var ref = state.selection;
+ var $from = ref.$from;
+ var $to = ref.$to;
+ var range = $from.blockRange($to, function (node) { return node.childCount && node.firstChild.type == itemType; });
+ if (!range) { return false }
+ var startIndex = range.startIndex;
+ if (startIndex == 0) { return false }
+ var parent = range.parent, nodeBefore = parent.child(startIndex - 1);
+ if (nodeBefore.type != itemType) { return false; }
+
+ if (dispatch) {
+ var nestedBefore = nodeBefore.lastChild && nodeBefore.lastChild.type == parent.type;
+ var inner = prosemirrorModel.Fragment.from(nestedBefore ? itemType.create() : null);
+ let slice = new prosemirrorModel.Slice(prosemirrorModel.Fragment.from(itemType.create(null, prosemirrorModel.Fragment.from(parent.type.create(parent.attrs, inner)))),
+ nestedBefore ? 3 : 1, 0);
+ var before = range.start, after = range.end;
+ dispatch(state.tr.step(new prosemirrorTransform.ReplaceAroundStep(before - (nestedBefore ? 3 : 1), after,
+ before, after, slice, 1, true))
+ .scrollIntoView());
+ }
+ return true
+ }
+}
+
+function findWrappingOutside(range, type) {
+ var parent = range.parent;
+ var startIndex = range.startIndex;
+ var endIndex = range.endIndex;
+ var around = parent.contentMatchAt(startIndex).findWrapping(type);
+ if (!around) { return null }
+ var outer = around.length ? around[0] : type;
+ return parent.canReplaceWith(startIndex, endIndex, outer) ? around : null
+}
+
+function findWrappingInside(range, type) {
+ var parent = range.parent;
+ var startIndex = range.startIndex;
+ var endIndex = range.endIndex;
+ var inner = parent.child(startIndex);
+ var inside = type.contentMatch.findWrapping(inner.type);
+ if (!inside) { return null }
+ var lastType = inside.length ? inside[inside.length - 1] : type;
+ var innerMatch = lastType.contentMatch;
+ for (var i = startIndex; innerMatch && i < endIndex; i++) { innerMatch = innerMatch.matchType(parent.child(i).type); }
+ if (!innerMatch || !innerMatch.validEnd) { return null }
+ return inside
+}
+function findWrapping(range, nodeType, attrs, innerRange, customWithAttrs = null) {
+ if (innerRange === void 0) innerRange = range;
+ let withAttrs = (type) => ({ type: type, attrs: null });
+ var around = findWrappingOutside(range, nodeType);
+ var inner = around && findWrappingInside(innerRange, nodeType);
+ if (!inner) { return null }
+ return around.map(withAttrs).concat({ type: nodeType, attrs: attrs }).concat(inner.map(customWithAttrs ? customWithAttrs : withAttrs))
+}
+function wrappingInputRule(regexp, nodeType, getAttrs, joinPredicate, customWithAttrs = null) {
+ return new prosemirrorInputRules.InputRule(regexp, function (state, match, start, end) {
+ var attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs;
+ var tr = state.tr.delete(start, end);
+ var $start = tr.doc.resolve(start), range = $start.blockRange(), wrapping = range && findWrapping(range, nodeType, attrs, undefined, customWithAttrs);
+ if (!wrapping) { return null }
+ tr.wrap(range, wrapping);
+ var before = tr.doc.resolve(start - 1).nodeBefore;
+ if (before && before.type == nodeType && prosemirrorTransform.canJoin(tr.doc, start - 1) &&
+ (!joinPredicate || joinPredicate(match, before))) { tr.join(start - 1); }
+ return tr
+ })
+} \ No newline at end of file
diff --git a/src/client/util/toCSSLineSpacing.js b/src/client/util/toCSSLineSpacing.js
new file mode 100644
index 000000000..939d11a0e
--- /dev/null
+++ b/src/client/util/toCSSLineSpacing.js
@@ -0,0 +1,64 @@
+'use strict';
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.default = toCSSLineSpacing;
+
+
+// Line spacing names and their values.
+var LINE_SPACING_100 = exports.LINE_SPACING_100 = '125%';
+var LINE_SPACING_115 = exports.LINE_SPACING_115 = '138%';
+var LINE_SPACING_150 = exports.LINE_SPACING_150 = '165%';
+var LINE_SPACING_200 = exports.LINE_SPACING_200 = '232%';
+
+var SINGLE_LINE_SPACING = exports.SINGLE_LINE_SPACING = LINE_SPACING_100;
+var DOUBLE_LINE_SPACING = exports.DOUBLE_LINE_SPACING = LINE_SPACING_200;
+
+var NUMBER_VALUE_PATTERN = /^\d+(.\d+)?$/;
+
+// Normalize the css line-height vlaue to percentage-based value if applicable.
+// Also, it calibrates the incorrect line spacing value exported from Google
+// Doc.
+function toCSSLineSpacing(source) {
+ if (!source) {
+ return '';
+ }
+
+ var strValue = String(source);
+
+ // e.g. line-height: 1.5;
+ if (NUMBER_VALUE_PATTERN.test(strValue)) {
+ var numValue = parseFloat(strValue);
+ strValue = String(Math.round(numValue * 100)) + '%';
+ }
+
+ // Google Doc exports line spacing with wrong values. For instance:
+ // - Single => 100%
+ // - 1.15 => 115%
+ // - Double => 200%
+ // But the actual CSS value measured in Google Doc is like this:
+ // - Single => 125%
+ // - 1.15 => 138%
+ // - Double => 232%
+ // The following `if` block will calibrate the value if applicable.
+
+ if (strValue === '100%') {
+ return LINE_SPACING_100;
+ }
+
+ if (strValue === '115%') {
+ return LINE_SPACING_115;
+ }
+
+ if (strValue === '150%') {
+ return LINE_SPACING_150;
+ }
+
+ if (strValue === '200%') {
+ return LINE_SPACING_200;
+ }
+
+ // e.g. line-height: 15px;
+ return strValue;
+} \ No newline at end of file