diff options
Diffstat (limited to 'src/client/util')
| -rw-r--r-- | src/client/util/DictationManager.ts | 78 | ||||
| -rw-r--r-- | src/client/util/DocumentManager.ts | 4 | ||||
| -rw-r--r-- | src/client/util/DragManager.ts | 38 | ||||
| -rw-r--r-- | src/client/util/ParagraphNodeSpec.ts | 133 | ||||
| -rw-r--r-- | src/client/util/ProsemirrorExampleTransfer.ts | 213 | ||||
| -rw-r--r-- | src/client/util/RichTextRules.ts | 114 | ||||
| -rw-r--r-- | src/client/util/RichTextSchema.tsx | 484 | ||||
| -rw-r--r-- | src/client/util/SelectionManager.ts | 1 | ||||
| -rw-r--r-- | src/client/util/TooltipLinkingMenu.tsx | 20 | ||||
| -rw-r--r-- | src/client/util/TooltipTextMenu.tsx | 89 | ||||
| -rw-r--r-- | src/client/util/clamp.js | 15 | ||||
| -rw-r--r-- | src/client/util/convertToCSSPTValue.js | 43 | ||||
| -rw-r--r-- | src/client/util/prosemirrorPatches.js | 139 | ||||
| -rw-r--r-- | src/client/util/toCSSLineSpacing.js | 64 |
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 |
