diff options
author | bob <bcz@cs.brown.edu> | 2019-08-29 13:49:45 -0400 |
---|---|---|
committer | bob <bcz@cs.brown.edu> | 2019-08-29 13:49:45 -0400 |
commit | e2317289507fd6d20096dc1d2abd5ab1d815e895 (patch) | |
tree | 1a4a98af879b3dc66b5844deec2d9c8917c2fe97 /src | |
parent | 0b43aead85c0de5877c0ecea7703acb6f2539249 (diff) | |
parent | 22a5999626b11cf75cafbcd421601e668438f6ad (diff) |
Merge branch 'master' into monika_linking
Diffstat (limited to 'src')
29 files changed, 498 insertions, 242 deletions
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 124faf266..ec731da84 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -204,4 +204,4 @@ export class DocumentManager { } } } -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 +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/ProsemirrorExampleTransfer.ts b/src/client/util/ProsemirrorExampleTransfer.ts index 3cdfba59a..3979d8a49 100644 --- a/src/client/util/ProsemirrorExampleTransfer.ts +++ b/src/client/util/ProsemirrorExampleTransfer.ts @@ -79,66 +79,71 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?: bind("Mod-s", TooltipTextMenu.insertStar); + let nodeTypeMark = (depth: number) => depth === 2 ? "indent2" : depth === 4 ? "indent3" : depth === 6 ? "indent4" : "indent1"; - let levelMark = (depth: number) => { - let p10 = schema.marks.pFontSize.create({ fontSize: 10 }); - let p14 = schema.marks.pFontSize.create({ fontSize: 14 }); - let p18 = schema.marks.pFontSize.create({ fontSize: 18 }); - let p24 = schema.marks.pFontSize.create({ fontSize: 24 }); - return depth == 0 ? p24 : depth == 2 ? p18 : depth == 4 ? p14 : p10; - } let bulletFunc = (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()); let depth = range && range.depth ? range.depth : 0; - let nodeType = depth == 2 ? schema.nodes.cap_alphabet_list : depth == 4 ? schema.nodes.roman_list : depth == 6 ? schema.nodes.alphabet_list : schema.nodes.ordered_list; - let nodeTypeMark = depth == 2 ? schema.marks.mcap_alphabet_list : depth == 4 ? schema.marks.mroman_list : depth == 6 ? schema.marks.malphabet_list : schema.marks.mordered_list; - let created = levelMark(depth); - if (!sinkListItem(nodeType /*schema.nodes.list_item */)(state, (tx2: Transaction) => { + if (!sinkListItem(schema.nodes.list_item)(state, (tx2: Transaction) => { const resolvedPos = tx2.doc.resolve(range!.start); - let ns = new NodeSelection(resolvedPos); - let tx3 = tx2.addMark(ns.from - 1, ns.to, created).addMark(ns.from - 1, ns.to, nodeTypeMark as any).setSelection(TextSelection.create(tx2.doc, ns.to - (depth == 0 ? 3 : 1))); - marks && tx3.ensureMarks([...marks, created]); - marks && tx3.setStoredMarks([...marks, created]); - - dispatch(tx3); + let path = (resolvedPos as any).path; + for (let i = path.length - 1; i > 0; i--) { + if (path[i].type === schema.nodes.ordered_list) { + path[i].attrs.bulletStyle = nodeTypeMark(depth); + break; + } + } + marks && tx2.ensureMarks([...marks]); + marks && tx2.setStoredMarks([...marks]); + dispatch(tx2); })) { let sxf = state.tr.setSelection(TextSelection.create(state.doc, range!.start, range!.end)); let newstate = state.applyTransaction(sxf); - if (!wrapInList(nodeType)(newstate.state, (tx2: Transaction) => { - const resolvedPos = tx2.doc.resolve(range!.start); - let ns = new TextSelection(resolvedPos, tx2.doc.resolve(range!.end + 1)); // new NodeSelection(resolvedPos); - let tx3 = tx2.setSelection(ns).removeMark(ns.from, ns.to, created).addMark(ns.from, ns.to, created).setSelection(TextSelection.create(tx2.doc, ns.to)); - let tx4 = depth > 0 ? tx3.insertText(" ").setSelection(TextSelection.create(tx2.doc, ns.to - 2, ns.to + 2)).deleteSelection() : tx3; - marks && tx4.ensureMarks([...marks, created]); - marks && tx4.setStoredMarks([...marks, created]); - - dispatch(tx4); + if (!wrapInList(schema.nodes.ordered_list)(newstate.state, (tx2: Transaction) => { + const resolvedPos = tx2.doc.resolve(Math.round((range!.start + range!.end) / 2)); + let path = (resolvedPos as any).path; + for (let i = path.length - 1; i > 0; i--) { + if (path[i].type === schema.nodes.ordered_list) { + path[i].attrs.bulletStyle = nodeTypeMark(depth); + break; + } + } + // 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 fail"); } } - } - bind("Tab", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => bulletFunc(state, dispatch)); + }; + + bind("Tab", bulletFunc); bind("Shift-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()); - let created = levelMark(range && range.depth ? range.depth - 4 : 0); + let depth = range && range.depth > 3 ? range.depth - 4 : 0; liftListItem(schema.nodes.list_item)(state, (tx2: Transaction) => { try { const resolvedPos = tx2.doc.resolve(Math.round((range!.start + range!.end) / 2)); - let nodeIndex = resolvedPos.pos - (resolvedPos.nodeBefore && resolvedPos.nodeBefore.type.name === "text" ? resolvedPos.nodeBefore!.nodeSize : 0); - let ns = new NodeSelection(tx2.doc.resolve(nodeIndex)); - if (resolvedPos.nodeAfter && resolvedPos.nodeAfter.type.name === "list_item") - ns = new NodeSelection(tx2.doc.resolve(nodeIndex + 1)); - let tx3 = tx2.setSelection(ns).removeMark(ns.from, ns.to, created).addMark(ns.from, ns.to, created).setSelection(TextSelection.create(tx2.doc, ns.to)); - marks && tx3.ensureMarks([...marks, created]); - marks && tx3.setStoredMarks([...marks, created]); - dispatch(tx3); + + let path = (resolvedPos as any).path; + for (let i = path.length - 1; i > 0; i--) { + if (path[i].type === schema.nodes.ordered_list) { + path[i].attrs.bulletStyle = nodeTypeMark(depth); + break; + } + } + + marks && tx2.ensureMarks([...marks]); + marks && tx2.setStoredMarks([...marks]); + dispatch(tx2); } catch (e) { dispatch(tx2); } @@ -155,7 +160,8 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?: if (!splitBlockKeepMarks(state, (tx3: Transaction) => { marks && tx3.ensureMarks(marks); marks && tx3.setStoredMarks(marks); - if (!liftListItem(schema.nodes.list_item)(state, (tx4: Transaction) => dispatch(tx4))) { + if (!liftListItem(schema.nodes.list_item)(state, dispatch as ((tx: Transaction<Schema<any, any>>) => void)) + ) { dispatch(tx3); } })) { diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx index f128162c2..a642ee46c 100644 --- a/src/client/util/RichTextSchema.tsx +++ b/src/client/util/RichTextSchema.tsx @@ -1,6 +1,7 @@ import { DOMOutputSpecArray, MarkSpec, Node, NodeSpec, Schema, Slice } from "prosemirror-model"; import { bulletList, listItem, orderedList } from 'prosemirror-schema-list'; import { TextSelection } from "prosemirror-state"; +import { Doc } from "../../new_fields/Doc"; const pDOM: DOMOutputSpecArray = ["p", 0], blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"], preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0]; @@ -175,43 +176,17 @@ export const nodes: { [index: string]: NodeSpec } = { content: 'list_item+', group: 'block', attrs: { - bulletStyle: { default: "decimal" }, + bulletStyle: { default: "" }, + mapStyle: { default: "decimal" } }, toDOM(node: Node<any>) { - return ['ol', { style: `list-style: ${node.attrs.bulletStyle}` }, 0] - } - }, - alphabet_list: { - ...orderedList, - content: 'list_item+', - group: 'block', - attrs: { - bulletStyle: { default: "lower-alpha" }, - }, - toDOM(node: Node<any>) { - return ['ol', { style: `list-style: ${node.attrs.bulletStyle}` }, 0] - } - }, - cap_alphabet_list: { - ...orderedList, - content: 'list_item+', - group: 'block', - attrs: { - bulletStyle: { default: "upper-alpha" }, - }, - toDOM(node: Node<any>) { - return ['ol', { style: `list-style: ${node.attrs.bulletStyle}` }, 0] - } - }, - roman_list: { - ...orderedList, - content: 'list_item+', - group: 'block', - attrs: { - bulletStyle: { default: "lower-roman" }, - }, - toDOM(node: Node<any>) { - return ['ol', { style: `list-style: ${node.attrs.bulletStyle}` }, 0] + const bs = node.attrs.bulletStyle; + const decMap = bs === "indent1" ? "decimal" : bs === "indent2" ? "decimal2" : bs === "indent3" ? "decimal3" : bs === "indent4" ? "decimal4" : ""; + const multiMap = bs === "indent1" ? "decimal" : bs === "indent2" ? "upper-alpha" : bs === "indent3" ? "lower-roman" : bs === "indent4" ? "lower-alpha" : ""; + let map = node.attrs.mapStyle === "decimal" ? decMap : multiMap; + for (let i = 0; i < node.childCount; i++) node.child(i).attrs.className = map; + return ['ol', { class: `${map}-ol`, style: `list-style: none;` }, 0]; + //return ['ol', { class: `${node.attrs.bulletStyle}`, style: `list-style: ${node.attrs.bulletStyle};`, 0] } }, //this doesn't currently work for some reason @@ -220,9 +195,10 @@ export const nodes: { [index: string]: NodeSpec } = { content: 'list_item+', group: 'block', // parseDOM: [{ tag: "ul" }, { style: 'list-style-type=disc' }], - // toDOM() { return ['ol', { - // style: 'list-type: hebrew' - // }] } + toDOM(node: Node<any>) { + for (let i = 0; i < node.childCount; i++) node.child(i).attrs.className = ""; + return ['ul', 0]; + } }, //bullet_list: { @@ -234,8 +210,14 @@ export const nodes: { [index: string]: NodeSpec } = { //select: state => true, // }, list_item: { + attrs: { + className: { default: "" } + }, ...listItem, - content: 'paragraph block*' + content: 'paragraph block*', + toDOM(node: any) { + return ["li", { class: node.attrs.className }, 0]; + } }, }; @@ -267,7 +249,7 @@ export const marks: { [index: string]: MarkSpec } = { // :: 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; } }, @@ -319,13 +301,15 @@ export const marks: { [index: string]: MarkSpec } = { toDOM: () => ['sup'] }, - malphabet_list: { - }, - mcap_alphabet_list: { - }, - mroman_list: { - }, - mo_list: { + mbulletType: { + attrs: { + bulletType: { default: "decimal" } + }, + toDOM(node: any) { + return ['span', { + style: `background: ${node.attrs.bulletType === "decimal" ? "yellow" : node.attrs.bulletType === "upper-alpha" ? "blue" : "green"}` + }]; + } }, highlight: { @@ -346,6 +330,28 @@ 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", + inclusive: false, + 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', { style: "font-size:2" }, 0]] + ) : + ['span', 0]; + } + }, + // :: MarkSpec Code font mark. Represented as a `<code>` element. code: { @@ -599,6 +605,8 @@ export class SummarizedView { attrs.textslice = newSelection.content().toJSON(); view.dispatch(view.state.tr.setNodeMarkup(y, undefined, attrs)); view.dispatch(view.state.tr.setSelection(newSelection).deleteSelection(view.state, () => { })); + let marks = view.state.storedMarks.filter((m: any) => m.type !== view.state.schema.marks.highlight); + view.state.storedMarks = marks; self._collapsed.textContent = "㊉"; } else { // node.attrs.visibility = !node.attrs.visibility; diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx index 77396c829..e979e6cde 100644 --- a/src/client/util/TooltipTextMenu.tsx +++ b/src/client/util/TooltipTextMenu.tsx @@ -28,11 +28,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; @@ -179,10 +179,8 @@ export class TooltipTextMenu { //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.alphabet_list, "a)"); - this.listTypeToIcon.set(schema.nodes.cap_alphabet_list, "A)"); - this.listTypeToIcon.set(schema.nodes.roman_list, "i."); + 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()); @@ -515,10 +513,28 @@ export class TooltipTextMenu { //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 + //remove oldif (nodeType) { //add new + if (nodeType === schema.nodes.bullet_list) { wrapInList(nodeType)(view.state, view.dispatch); + } else { + var ref = view.state.selection; + var range = ref.$from.blockRange(ref.$to); + var marks = view.state.storedMarks || (view.state.selection.$to.parentOffset && view.state.selection.$from.marks()); + wrapInList(schema.nodes.ordered_list)(view.state, (tx2: any) => { + const resolvedPos = tx2.doc.resolve(Math.round((range!.start + range!.end) / 2)); + let path = resolvedPos.path; + for (let i = path.length - 1; i > 0; i--) { + if (path[i].type === schema.nodes.ordered_list) { + path[i].attrs.bulletStyle = "indent1"; + path[i].attrs.mapStyle = (nodeType as any).attrs.mapStyle; + break; + } + } + marks && tx2.ensureMarks([...marks]); + marks && tx2.setStoredMarks([...marks]); + + view.dispatch(tx2); + }); } } diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 371b76823..700a4b49d 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -200,14 +200,22 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> this.onBackgroundUp(e); } + @observable _forceUpdate = 0; + _lastBox = { x: 0, y: 0, r: 0, b: 0 }; @computed get Bounds(): { x: number, y: number, b: number, r: number } { - return SelectionManager.SelectedDocuments().reduce((bounds, documentView) => { + let x = this._forceUpdate; + this._lastBox = SelectionManager.SelectedDocuments().reduce((bounds, documentView) => { if (documentView.props.renderDepth === 0 || Doc.AreProtosEqual(documentView.props.Document, CurrentUserUtils.UserDocument)) { return bounds; } let transform = (documentView.props.ScreenToLocalTransform().scale(documentView.props.ContentScaling())).inverse(); + if (transform.TranslateX === 0 && transform.TranslateY === 0) { + setTimeout(action(() => this._forceUpdate++), 0); // bcz: fix CollectionStackingView's getTransform() somehow... + return this._lastBox; + } + var [sptX, sptY] = transform.transformPoint(0, 0); let [bptX, bptY] = transform.transformPoint(documentView.props.PanelWidth(), documentView.props.PanelHeight()); return { @@ -215,6 +223,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b) }; }, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: Number.MIN_VALUE, b: Number.MIN_VALUE }); + return this._lastBox; } onBackgroundDown = (e: React.PointerEvent): void => { @@ -822,11 +831,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> let templates: Map<Template, boolean> = new Map(); Array.from(Object.values(Templates.TemplateList)).map(template => { - let sorted = SelectionManager.ViewsSortedVertically(); // slice().sort((doc1, doc2) => { - // if (NumCast(doc1.props.Document.y) > NumCast(doc2.props.Document.x)) return 1; - // if (NumCast(doc1.props.Document.x) < NumCast(doc2.props.Document.x)) return -1; - // return 0; - // }); + let sorted = SelectionManager.ViewsSortedVertically(); let docTemps = sorted.reduce((res: string[], doc: DocumentView, i) => { let temps = doc.props.Document.templates; if (temps instanceof List) { diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx index e95e5b777..755e5de14 100644 --- a/src/client/views/MainOverlayTextBox.tsx +++ b/src/client/views/MainOverlayTextBox.tsx @@ -2,7 +2,7 @@ import { action, observable, reaction, trace } from 'mobx'; import { observer } from 'mobx-react'; import "normalize.css"; import * as React from 'react'; -import { Doc } from '../../new_fields/Doc'; +import { Doc, DocListCast } from '../../new_fields/Doc'; import { BoolCast } from '../../new_fields/Types'; import { emptyFunction, returnTrue, returnZero, Utils, returnOne } from '../../Utils'; import { DragManager } from '../util/DragManager'; @@ -42,13 +42,17 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps> return this._textBox && this._textBox.setFontColor(color); } - constructor(props: MainOverlayTextBoxProps) { super(props); this._textProxyDiv = React.createRef(); MainOverlayTextBox.Instance = this; reaction(() => FormattedTextBox.InputBoxOverlay, (box?: FormattedTextBox) => { + const tb = this._textBox; + const container = tb && tb.props.ContainingCollectionView; + if (tb && container) { + tb.rebuildEditor();// this forces the edited text box to completely recreate itself to avoid get out of sync -- e.g., bullet labels are only computed when the dom elements are created + } this._textBox = box; if (box) { this.ChromeHeight = box.props.ChromeHeight; @@ -146,7 +150,7 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps> DataDoc={FormattedTextBox.InputBoxOverlay.props.DataDoc} onClick={undefined} ChromeHeight={this.ChromeHeight} - isSelected={returnTrue} select={emptyFunction} renderDepth={0} selectOnLoad={true} + isSelected={returnTrue} select={emptyFunction} renderDepth={0} ContainingCollectionView={undefined} whenActiveChanged={emptyFunction} active={returnTrue} ContentScaling={returnOne} ScreenToLocalTransform={this._textXf} PanelWidth={returnZero} PanelHeight={returnZero} focus={emptyFunction} pinToPres={returnZero} addDocTab={this.addDocTab} outer_div={(tooltip: HTMLElement) => { this._tooltip = tooltip; this.updateTooltip(); }} /> diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 20fc1dce8..1fc8d1397 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -329,7 +329,6 @@ export class MainView extends React.Component { PanelHeight={this.getPHeight} renderDepth={0} backgroundColor={returnEmptyString} - selectOnLoad={false} focus={emptyFunction} parentActive={returnTrue} whenActiveChanged={emptyFunction} @@ -392,7 +391,6 @@ export class MainView extends React.Component { PanelWidth={this.flyoutWidthFunc} PanelHeight={this.getPHeight} renderDepth={0} - selectOnLoad={false} focus={emptyFunction} backgroundColor={returnEmptyString} parentActive={returnTrue} diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx index bc02a49c6..a1afe651c 100644 --- a/src/client/views/OverlayView.tsx +++ b/src/client/views/OverlayView.tsx @@ -184,7 +184,6 @@ export class OverlayView extends React.Component { PanelHeight={returnOne} ScreenToLocalTransform={Transform.Identity} renderDepth={1} - selectOnLoad={false} parentActive={returnTrue} whenActiveChanged={emptyFunction} focus={emptyFunction} diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index dc0cbda0b..95f94875c 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -636,7 +636,6 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { PanelHeight={this.panelHeight} ScreenToLocalTransform={this.ScreenToLocalTransform} renderDepth={0} - selectOnLoad={false} parentActive={returnTrue} whenActiveChanged={emptyFunction} focus={emptyFunction} diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx index 551b485e7..9c26a08f0 100644 --- a/src/client/views/collections/CollectionSchemaCells.tsx +++ b/src/client/views/collections/CollectionSchemaCells.tsx @@ -153,7 +153,6 @@ export class CollectionSchemaCell extends React.Component<CellProps> { isSelected: returnFalse, select: emptyFunction, renderDepth: this.props.renderDepth + 1, - selectOnLoad: false, ScreenToLocalTransform: Transform.Identity, focus: emptyFunction, active: returnFalse, diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 4008dea51..9d83aa6c1 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -1006,7 +1006,6 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre parentActive={this.props.active} ScreenToLocalTransform={this.getTransform} renderDepth={this.props.renderDepth + 1} - selectOnLoad={false} ContentScaling={this.contentScaling} PanelWidth={this.PanelWidth} PanelHeight={this.PanelHeight} diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 6f22f2d2a..97b31bf2a 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -278,7 +278,8 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { } getDocTransform(doc: Doc, dref: HTMLDivElement) { - let y = this._scroll; + if (!dref) return Transform.Identity(); + let y = this._scroll; // required for document decorations to update when the text box container is scrolled let { scale, translateX, translateY } = Utils.GetScreenTransform(dref); let outerXf = Utils.GetScreenTransform(this._masonryGridRef!); let offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY); diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index f30e99362..bc4fe7dd7 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -2,7 +2,7 @@ import React = require("react"); import { library } from '@fortawesome/fontawesome-svg-core'; import { faPalette } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, observable } from "mobx"; +import { action, observable, trace } from "mobx"; import { observer } from "mobx-react"; import { Doc, WidthSym } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 04133fb5b..50f03005c 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -432,26 +432,28 @@ class TreeView extends React.Component<TreeViewProps> { } let ascending = Cast(containingCollection.sortAscending, "boolean", null); - if (ascending !== undefined) docs.sort(function (a, b): 1 | -1 { - let descA = ascending ? b : a; - let descB = ascending ? a : b; - let first = descA.title; - let second = descB.title; - // TODO find better way to sort how to sort.................. - if (typeof first === 'number' && typeof second === 'number') { - return (first - second) > 0 ? 1 : -1; - } - if (typeof first === 'string' && typeof second === 'string') { - return first > second ? 1 : -1; - } - if (typeof first === 'boolean' && typeof second === 'boolean') { - // if (first === second) { // bugfixing?: otherwise, the list "flickers" because the list is resorted during every load - // return Number(descA.x) > Number(descB.x) ? 1 : -1; - // } - return first > second ? 1 : -1; - } - return ascending ? 1 : -1; - }); + if (ascending !== undefined) { + docs.sort(function (a, b): 1 | -1 { + let descA = ascending ? b : a; + let descB = ascending ? a : b; + let first = descA.title; + let second = descB.title; + // TODO find better way to sort how to sort.................. + if (typeof first === 'number' && typeof second === 'number') { + return (first - second) > 0 ? 1 : -1; + } + if (typeof first === 'string' && typeof second === 'string') { + return first > second ? 1 : -1; + } + if (typeof first === 'boolean' && typeof second === 'boolean') { + // if (first === second) { // bugfixing?: otherwise, the list "flickers" because the list is resorted during every load + // return Number(descA.x) > Number(descB.x) ? 1 : -1; + // } + return first > second ? 1 : -1; + } + return ascending ? 1 : -1; + }); + } let rowWidth = () => panelWidth() - 20; return docs.map((child, i) => { diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx index b2df0e747..c897af17e 100644 --- a/src/client/views/collections/CollectionViewChromes.tsx +++ b/src/client/views/collections/CollectionViewChromes.tsx @@ -42,16 +42,18 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro _templateCommand = { title: "set template", script: "this.target.childLayout = this.source ? this.source[0] : undefined", params: ["target", "source"], + initialize: emptyFunction, immediate: (draggedDocs: Doc[]) => this.props.CollectionView.props.Document.childLayout = draggedDocs.length ? draggedDocs[0] : undefined }; _contentCommand = { // title: "set content", script: "getProto(this.target).data = aliasDocs(this.source.map(async p => await p));", params: ["target", "source"], // bcz: doesn't look like we can do async stuff in scripting... title: "set content", script: "getProto(this.target).data = aliasDocs(this.source);", params: ["target", "source"], + initialize: emptyFunction, immediate: (draggedDocs: Doc[]) => Doc.GetProto(this.props.CollectionView.props.Document).data = new List<Doc>(draggedDocs.map((d: any) => Doc.MakeAlias(d))) }; _viewCommand = { title: "restore view", script: "this.target.panX = this.restoredPanX; this.target.panY = this.restoredPanY; this.target.scale = this.restoredScale;", params: ["target"], - immediate: (draggedDocs: Doc[]) => { this.props.CollectionView.props.Document.panX = 0; this.props.CollectionView.props.Document.panY = 0; this.props.CollectionView.props.Document.scale = 1 }, + immediate: (draggedDocs: Doc[]) => { this.props.CollectionView.props.Document.panX = 0; this.props.CollectionView.props.Document.panY = 0; this.props.CollectionView.props.Document.scale = 1; }, initialize: (button: Doc) => { button.restoredPanX = this.props.CollectionView.props.Document.panX; button.restoredPanY = this.props.CollectionView.props.Document.panY; button.restoredScale = this.props.CollectionView.props.Document.scale; } }; _freeform_commands = [this._contentCommand, this._templateCommand, this._viewCommand]; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index f185222ce..a7acd9e91 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,7 +1,7 @@ import { library } from "@fortawesome/fontawesome-svg-core"; import { faEye } from "@fortawesome/free-regular-svg-icons"; import { faBraille, faChalkboard, faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faPaintBrush, faTable, faUpload } from "@fortawesome/free-solid-svg-icons"; -import { action, computed, IReactionDisposer, observable, reaction } from "mobx"; +import { action, computed, IReactionDisposer, observable, reaction, trace } from "mobx"; import { observer } from "mobx-react"; import { Doc, DocListCastAsync, Field, FieldResult, HeightSym, Opt, WidthSym } from "../../../../new_fields/Doc"; import { Id } from "../../../../new_fields/FieldSymbols"; @@ -38,6 +38,7 @@ import "./CollectionFreeFormView.scss"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); import { DocServer } from "../../../DocServer"; +import { FormattedTextBox } from "../../nodes/FormattedTextBox"; library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard); @@ -165,8 +166,6 @@ export namespace PivotView { return prev; }, elements); - target.resetSelectOnLoaded(); - return docViews; }; @@ -177,25 +176,35 @@ const PanZoomDocument = makeInterface(panZoomSchema, positionSchema, pageSchema) @observer export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { - private _selectOnLoaded: string = ""; // id of document that should be selected once it's loaded (used for click-to-type) private _lastX: number = 0; private _lastY: number = 0; private get _pwidth() { return this.props.PanelWidth(); } private get _pheight() { return this.props.PanelHeight(); } private inkKey = "ink"; private _childLayoutDisposer?: IReactionDisposer; + private _childDisposer?: IReactionDisposer; componentDidMount() { - this._childLayoutDisposer = reaction(() => [this.childDocs, Cast(this.props.Document.childLayout, Doc)], - async (args) => { - this.childDocs.filter(doc => args[1] instanceof Doc || doc.layout instanceof Doc).map(async doc => { - if (!Doc.AreProtosEqual(args[1] as Doc, (await doc).layout as Doc)) { - Doc.ApplyTemplateTo(args[1] as Doc, (await doc), undefined); + this._childDisposer = reaction(() => this.childDocs, + async (childDocs) => { + let childLayout = Cast(this.props.Document.childLayout, Doc) as Doc; + childLayout && childDocs.map(async doc => { + if (!Doc.AreProtosEqual(childLayout, (await doc).layout as Doc)) { + Doc.ApplyTemplateTo(childLayout, doc, undefined); + } + }); + }); + this._childLayoutDisposer = reaction(() => Cast(this.props.Document.childLayout, Doc), + async (childLayout) => { + this.childDocs.map(async doc => { + if (!Doc.AreProtosEqual(childLayout as Doc, (await doc).layout as Doc)) { + Doc.ApplyTemplateTo(childLayout as Doc, doc, undefined); } }); }); } componentWillUnmount() { + this._childDisposer && this._childDisposer(); this._childLayoutDisposer && this._childLayoutDisposer(); } @@ -241,7 +250,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { private getContainerTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth, -this.borderWidth); private getLocalTransform = (): Transform => Transform.Identity().scale(1 / this.zoomScaling()).translate(this.panX(), this.panY()); private addLiveTextBox = (newBox: Doc) => { - this._selectOnLoaded = newBox[Id];// track the new text box so we can give it a prop that tells it to focus itself when it's displayed + FormattedTextBox.SelectOnLoad = newBox[Id];// track the new text box so we can give it a prop that tells it to focus itself when it's displayed this.addDocument(newBox, false); } private addDocument = (newBox: Doc, allowDuplicates: boolean) => { @@ -642,7 +651,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { onClick: this.props.onClick, ScreenToLocalTransform: childLayout.z ? this.getTransformOverlay : this.getTransform, renderDepth: this.props.renderDepth + 1, - selectOnLoad: childLayout[Id] === this._selectOnLoaded, PanelWidth: childLayout[WidthSym], PanelHeight: childLayout[HeightSym], ContentScaling: returnOne, @@ -668,7 +676,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { onClick: this.props.onClick, ScreenToLocalTransform: this.getTransform, renderDepth: this.props.renderDepth, - selectOnLoad: layoutDoc[Id] === this._selectOnLoaded, PanelWidth: layoutDoc[WidthSym], PanelHeight: layoutDoc[HeightSym], ContentScaling: returnOne, @@ -768,13 +775,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { return prev; }, elements); - this.resetSelectOnLoaded(); - return docviews; } - resetSelectOnLoaded = () => setTimeout(() => this._selectOnLoaded = "", 600);// bcz: surely there must be a better way .... - @computed.struct get views() { let source = this.elements; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index f682c3d22..b60730a6b 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -92,7 +92,6 @@ export interface DocumentViewProps { PanelWidth: () => number; PanelHeight: () => number; focus: (doc: Doc, willZoom: boolean, scale?: number) => void; - selectOnLoad: boolean; parentActive: () => boolean; whenActiveChanged: (isActive: boolean) => void; bringToFront: (doc: Doc, sendToBack?: boolean) => void; @@ -601,7 +600,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu this.makeBtnClicked(); }, icon: "window-restore" }); - makes.push({ description: this.props.Document.ignoreClick ? "Selectable" : "Unselectable", event: () => this.props.Document.ignoreClick = !this.props.Document.ignoreClick, icon: this.props.Document.ignoreClick ? "unlock" : "lock" }) + makes.push({ description: this.props.Document.ignoreClick ? "Selectable" : "Unselectable", event: () => this.props.Document.ignoreClick = !this.props.Document.ignoreClick, icon: this.props.Document.ignoreClick ? "unlock" : "lock" }); !existingMake && cm.addItem({ description: "Make...", subitems: makes, icon: "hand-point-right" }); let existing = ContextMenu.Instance.findByDescription("Layout..."); let layoutItems: ContextMenuProps[] = existing && "subitems" in existing ? existing.subitems : []; @@ -717,7 +716,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu isSelected={this.isSelected} select={this.select} onClick={this.onClickHandler} - selectOnLoad={this.props.selectOnLoad} layoutKey={"layout"} fitToBox={BoolCast(this.props.Document.fitToBox) ? true : this.props.fitToBox} DataDoc={this.dataDoc} />); @@ -810,7 +808,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } {!showCaption ? (null) : <div style={{ position: "absolute", bottom: 0, transformOrigin: "bottom left", width: `${100 * this.props.ContentScaling()}%`, transform: `scale(${1 / this.props.ContentScaling()})` }}> - <FormattedTextBox {...this.props} onClick={this.onClickHandler} DataDoc={this.dataDoc} active={returnTrue} isSelected={this.isSelected} focus={emptyFunction} select={this.select} selectOnLoad={this.props.selectOnLoad} fieldExt={""} hideOnLeave={true} fieldKey={showCaption} /> + <FormattedTextBox {...this.props} onClick={this.onClickHandler} DataDoc={this.dataDoc} active={returnTrue} isSelected={this.isSelected} focus={emptyFunction} select={this.select} fieldExt={""} hideOnLeave={true} fieldKey={showCaption} /> </div> } </div> diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index f0f1b3b73..d9774303b 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -36,7 +36,6 @@ export interface FieldViewProps { isSelected: () => boolean; select: (isCtrlPressed: boolean) => void; renderDepth: number; - selectOnLoad: boolean; addDocument?: (document: Doc, allowDuplicates?: boolean) => boolean; addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void; pinToPres: (document: Doc) => void; @@ -108,7 +107,6 @@ export class FieldView extends React.Component<FieldViewProps> { // PanelWidth={returnHundred} // PanelHeight={returnHundred} // renderDepth={0} //TODO Why is renderDepth reset? - // selectOnLoad={false} // focus={emptyFunction} // isSelected={this.props.isSelected} // select={returnFalse} diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss index 1b537cc52..03e81bfca 100644 --- a/src/client/views/nodes/FormattedTextBox.scss +++ b/src/client/views/nodes/FormattedTextBox.scss @@ -65,4 +65,37 @@ .em { font-style: italic; -}
\ No newline at end of file +} + +.userMarkOpen { + background: rgba(255, 255, 0, 0.267); + display: inline; +} +.userMark { + background: rgba(255, 255, 0, 0.267); + font-size: 2px; + display: inline-grid; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width:10px; + min-height:10px; + text-align:center; + align-content: center; +} + +ol { counter-reset: deci 0;} +.decimal-ol { counter-reset: deci 0; p { display: inline }; font-size: 24 } +.decimal2-ol {counter-reset: deci2; p { display: inline }; font-size: 18 } +.decimal3-ol {counter-reset: deci3; p { display: inline }; font-size: 14 } +.decimal4-ol {counter-reset: deci4; p { display: inline }; font-size: 10 } +.upper-alpha-ol {counter-reset: ualph; p { display: inline }; font-size: 18 } +.lower-roman-ol {counter-reset: lroman; p { display: inline }; font-size: 14; } +.lower-alpha-ol {counter-reset: lalpha; p { display: inline }; font-size: 10;} +.decimal:before { content: counter(deci) " "; counter-increment: deci; display:inline-block; width: 30} +.decimal2:before { content: counter(deci) "." counter(deci2) " "; counter-increment: deci2; display:inline-block; width: 35} +.decimal3:before { content: counter(deci) "." counter(deci2) "." counter(deci3) " "; counter-increment: deci3; display:inline-block; width: 35} +.decimal4:before { content: counter(deci) "." counter(deci2) "." counter(deci3) "." counter(deci4) " "; counter-increment: deci4; display:inline-block; width: 40} +.upper-alpha:before { content: counter(deci) "." counter(ualph, upper-alpha) " "; counter-increment: ualph; display:inline-block; width: 35 } +.lower-roman:before { content: counter(deci) "." counter(ualph, upper-alpha) "." counter(lroman, lower-roman) " "; counter-increment: lroman;display:inline-block; width: 50 } +.lower-alpha:before { content: counter(deci) "." counter(ualph, upper-alpha) "." counter(lroman, lower-roman) "." counter(lalpha, lower-alpha)" "; counter-increment: lalpha; display:inline-block; width: 35} diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index acfd2a3b8..0d4376d8d 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -5,8 +5,8 @@ import { observer } from "mobx-react"; import { baseKeymap } from "prosemirror-commands"; import { history } from "prosemirror-history"; import { keymap } from "prosemirror-keymap"; -import { Fragment, Node, Node as ProsNode, NodeType, Slice } from "prosemirror-model"; -import { EditorState, Plugin, Transaction, TextSelection } from "prosemirror-state"; +import { Fragment, Node, Node as ProsNode, NodeType, Slice, Mark, ResolvedPos } from "prosemirror-model"; +import { EditorState, Plugin, Transaction, TextSelection, NodeSelection } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import { DateField } from '../../../new_fields/DateField'; import { Doc, DocListCast, Opt, WidthSym } from "../../../new_fields/Doc"; @@ -15,7 +15,7 @@ import { List } from '../../../new_fields/List'; import { RichTextField, ToPlainText, FromPlainText } from "../../../new_fields/RichTextField"; import { BoolCast, Cast, NumCast, StrCast, DateCast } from "../../../new_fields/Types"; import { createSchema, makeInterface } from "../../../new_fields/Schema"; -import { Utils } from '../../../Utils'; +import { Utils, numberRange } from '../../../Utils'; import { DocServer } from "../../DocServer"; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentManager } from '../../util/DocumentManager'; @@ -37,6 +37,8 @@ import { DocumentDecorations } from '../DocumentDecorations'; import { DictationManager } from '../../util/DictationManager'; import { ReplaceStep } from 'prosemirror-transform'; import { DocumentType } from '../../documents/DocumentTypes'; +import { selectionSizePlugin, findStartOfMark, findUserMark, findEndOfMark, findOtherUserMark, SelectionSizeTooltip } from './FormattedTextBoxComment'; +import { date } from 'serializr'; library.add(faEdit); library.add(faSmile, faTextHeight, faUpload); @@ -68,25 +70,25 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe public static LayoutString(fieldStr: string = "data") { return FieldView.LayoutString(FormattedTextBox, fieldStr); } - public static Instance: FormattedTextBox; - private _ref: React.RefObject<HTMLDivElement>; + private static _toolTipTextMenu: TooltipTextMenu | undefined = undefined; + private _ref: React.RefObject<HTMLDivElement> = React.createRef(); private _proseRef?: HTMLDivElement; private _editorView: Opt<EditorView>; - private static _toolTipTextMenu: TooltipTextMenu | undefined = undefined; private _applyingChange: boolean = false; private _linkClicked = ""; + private _undoTyping?: UndoManager.Batch; private _reactionDisposer: Opt<IReactionDisposer>; private _searchReactionDisposer?: Lambda; private _textReactionDisposer: Opt<IReactionDisposer>; private _heightReactionDisposer: Opt<IReactionDisposer>; private _proxyReactionDisposer: Opt<IReactionDisposer>; - private pullReactionDisposer: Opt<IReactionDisposer>; - private pushReactionDisposer: Opt<IReactionDisposer>; + private _pullReactionDisposer: Opt<IReactionDisposer>; + private _pushReactionDisposer: Opt<IReactionDisposer>; private dropDisposer?: DragManager.DragDropDisposer; - public get CurrentDiv(): HTMLDivElement { return this._ref.current!; } - @observable _entered = false; + @observable private _entered = false; @observable public static InputBoxOverlay?: FormattedTextBox = undefined; + public static SelectOnLoad = ""; public static InputBoxOverlayScroll: number = 0; public static IsFragment(html: string) { return html.indexOf("data-pm-slice") !== -1; @@ -127,15 +129,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe constructor(props: FieldViewProps) { super(props); - FormattedTextBox.Instance = this; - this._ref = React.createRef(); if (this.props.isOverlay) { DragManager.StartDragFunctions.push(() => FormattedTextBox.InputBoxOverlay = undefined); } - - document.addEventListener("paste", this.paste); } + public get CurrentDiv(): HTMLDivElement { return this._ref.current!; } + @computed get extensionDoc() { return Doc.resolvedFieldDataDoc(this.dataDoc, this.props.fieldKey, "dummy"); } @computed get dataDoc() { return this.props.DataDoc && (BoolCast(this.props.Document.isTemplate) || BoolCast(this.props.DataDoc.isTemplate) || this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : Doc.GetProto(this.props.Document); } @@ -235,12 +235,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe protected createDropTarget = (ele: HTMLDivElement) => { this._proseRef = ele; - if (this.dropDisposer) { - this.dropDisposer(); - } - if (ele) { - this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }); - } + this.dropDisposer && this.dropDisposer(); + ele && (this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } })); } @undoBatch @@ -264,13 +260,12 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } recordKeyHandler = (e: KeyboardEvent) => { - if (this.props.Document !== SelectionManager.SelectedDocuments()[0].props.Document) { - return; - } - if (e.key === "R" && e.altKey) { - e.stopPropagation(); - e.preventDefault(); - this.recordBullet(); + if (this.props.Document === SelectionManager.SelectedDocuments()[0].props.Document) { + if (e.key === "R" && e.altKey) { + e.stopPropagation(); + e.preventDefault(); + this.recordBullet(); + } } } @@ -314,15 +309,11 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } private newListItems = (count: number) => { - let listItems: any[] = []; - for (let i = 0; i < count; i++) { - listItems.push(schema.nodes.list_item.create(undefined, schema.nodes.paragraph.create())); - } - return listItems; + return numberRange(count).map(x => schema.nodes.list_item.create(undefined, schema.nodes.paragraph.create())); } - componentDidMount() { - const config = { + @computed get config() { + return { schema, inpRules, //these currently don't do anything, but could eventually be helpful plugins: this.props.isOverlay ? [ @@ -335,13 +326,23 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe props: { attributes: { class: "ProseMirror-example-setup-style" } } - }) + }), + selectionSizePlugin ] : [ history(), keymap(buildKeymap(schema)), keymap(baseKeymap), ] }; + } + + @action + rebuildEditor() { + this.setupEditor(this.config, this.dataDoc, this.props.fieldKey); + } + + componentDidMount() { + document.addEventListener("paste", this.paste); if (!this.props.isOverlay) { this._proxyReactionDisposer = reaction(() => this.props.isSelected(), @@ -364,13 +365,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe incomingValue => { if (this._editorView && !this._applyingChange) { let updatedState = JSON.parse(incomingValue); - this._editorView.updateState(EditorState.fromJSON(config, updatedState)); + this._editorView.updateState(EditorState.fromJSON(this.config, updatedState)); this.tryUpdateHeight(); } } ); - this.pullReactionDisposer = reaction( + this._pullReactionDisposer = reaction( () => this.props.Document[Pulls], () => { if (!DocumentDecorations.hasPulledHack) { @@ -381,7 +382,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } ); - this.pushReactionDisposer = reaction( + this._pushReactionDisposer = reaction( () => this.props.Document[Pushes], () => { if (!DocumentDecorations.hasPushedHack) { @@ -406,7 +407,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this.dataDoc.lastModified = undefined; } }, { fireImmediately: true }); - this.setupEditor(config, this.dataDoc, this.props.fieldKey); + + + this.setupEditor(this.config, this.dataDoc, this.props.fieldKey); this._searchReactionDisposer = reaction(() => { return StrCast(this.props.Document.search_string); @@ -506,7 +509,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } } - clipboardTextSerializer = (slice: Slice): string => { let text = "", separated = true; const from = 0, to = slice.content.size; @@ -606,6 +608,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } } if (this._proseRef) { + this._editorView && this._editorView.destroy(); this._editorView = new EditorView(this._proseRef, { state: field && field.Data ? EditorState.fromJSON(config, JSON.parse(field.Data)) : EditorState.create(config), dispatchTransaction: this.dispatchTransaction, @@ -622,21 +625,27 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } } - if (this.props.selectOnLoad) { - if (!this.props.isOverlay) this.props.select(false); - else this._editorView!.focus(); + if (this.props.Document[Id] === FormattedTextBox.SelectOnLoad) { + FormattedTextBox.SelectOnLoad = ""; + this.props.select(false); } + else if (this.props.isOverlay) this._editorView!.focus(); + var markerss = this._editorView!.state.storedMarks || (this._editorView!.state.selection.$to.parentOffset && this._editorView!.state.selection.$from.marks()); + let newMarks = [...(markerss ? markerss.filter(m => m.type !== schema.marks.user_mark) : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail })]; + this._editorView!.state.storedMarks = newMarks; + } componentWillUnmount() { - this._editorView && this._editorView.destroy(); this._reactionDisposer && this._reactionDisposer(); this._proxyReactionDisposer && this._proxyReactionDisposer(); this._textReactionDisposer && this._textReactionDisposer(); - this.pushReactionDisposer && this.pushReactionDisposer(); - this.pullReactionDisposer && this.pullReactionDisposer(); + this._pushReactionDisposer && this._pushReactionDisposer(); + this._pullReactionDisposer && this._pullReactionDisposer(); this._heightReactionDisposer && this._heightReactionDisposer(); this._searchReactionDisposer && this._searchReactionDisposer(); + document.removeEventListener("paste", this.paste); + this._editorView && this._editorView.destroy(); } onPointerDown = (e: React.PointerEvent): void => { @@ -700,15 +709,38 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe e.preventDefault(); } } + onPointerUp = (e: React.PointerEvent): void => { - if (FormattedTextBox._toolTipTextMenu && FormattedTextBox._toolTipTextMenu.tooltip) { - //this._toolTipTextMenu.tooltip.style.opacity = "1"; + let view = this._editorView!; + const pos = view.posAtCoords({ left: e.clientX, top: e.clientY }); + const rpos = pos && view.state.doc.resolve(pos.pos); + let noselection = view.state.selection.$from === view.state.selection.$to; + let set = false; + if (pos && rpos) { + let nbef = findStartOfMark(rpos, view, findOtherUserMark); + let naft = findEndOfMark(rpos, view, findOtherUserMark); + const spos = pos.pos - nbef; + const epos = pos.pos + naft; + let child = rpos.nodeBefore || rpos.nodeAfter; + let mark = child && findOtherUserMark(child.marks); + if (mark && child && (nbef || naft) && (!mark.attrs.opened || noselection)) { + SelectionSizeTooltip.SetState(this, mark.attrs.opened, spos, epos, mark); + set = true; + } } + !set && SelectionSizeTooltip.Hide(); if (e.buttons === 1 && this.props.isSelected() && !e.altKey) { e.stopPropagation(); } } + setAnnotation = (start: number, end: number, mark: Mark, opened: boolean, keep: boolean = false) => { + let view = this._editorView!; + let mid = view.state.doc.resolve(Math.round((start + end) / 2)); + let nmark = view.state.schema.marks.user_mark.create({ ...mark.attrs, userid: keep ? Doc.CurrentUserEmail : mark.attrs.userid, opened: opened }); + view.dispatch(view.state.tr.removeMark(start, end, nmark).addMark(start, end, nmark).setSelection(new TextSelection(mid, mid))); + } + @action onFocused = (e: React.FocusEvent): void => { document.removeEventListener("keypress", this.recordKeyHandler); @@ -751,7 +783,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe return self._toolTipTextMenu = new TooltipTextMenu(_editorView, myprops); } }); - //this.props.Document.tooltip = self._toolTipTextMenu; } tooltipLinkingMenuPlugin() { @@ -769,13 +800,37 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this._undoTyping = undefined; } } - public _undoTyping?: UndoManager.Batch; onKeyPress = (e: React.KeyboardEvent) => { if (e.key === "Escape") { SelectionManager.DeselectAll(); } e.stopPropagation(); - if (e.key === "Tab") e.preventDefault(); + if (e.key === "Tab" || e.key === "Enter") { // bullets typically change "levels" when tab or enter is used. sometimes backspcae, so maybe that should be added. + e.preventDefault(); + setTimeout(() => { // force re-rendering of bullet numbers that may have had their bullet labels change. (Our prosemirrior code re-"marks" the changed bullets, but nothing causes the Dom to be re-rendered which is where the nubering takes place) + SelectionManager.DeselectAll(); + SelectionManager.SelectDoc(DocumentManager.Instance.getDocumentView(this.props.Document, this.props.ContainingCollectionView)!, false); + }, 0); + } + function timenow() { + var now = new Date(); + let ampm = 'am'; + let h = now.getHours(); + let m: any = now.getMinutes(); + let s: any = now.getSeconds(); + if (h >= 12) { + if (h > 12) h -= 12; + ampm = 'pm'; + } + + if (m < 10) m = '0' + m; + if (s < 10) s = '0' + s; + return now.toLocaleDateString() + ' ' + h + ':' + m + ':' + s + ' ' + ampm; + } + var markerss = this._editorView!.state.storedMarks || (this._editorView!.state.selection.$to.parentOffset && this._editorView!.state.selection.$from.marks()); + let newMarks = [...(markerss ? markerss.filter(m => m.type !== schema.marks.user_mark) : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() })]; + this._editorView!.state.storedMarks = newMarks; + // stop propagation doesn't seem to stop propagation of native keyboard events. // so we set a flag on the native event that marks that the event's been handled. (e.nativeEvent as any).DASHFormattedTextBoxHandled = true; @@ -802,24 +857,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } } - @action - onPointerEnter = (e: React.PointerEvent) => { - this._entered = true; - } - @action - onPointerLeave = (e: React.PointerEvent) => { - this._entered = false; - } - - specificContextMenu = (e: React.MouseEvent): void => { - // let subitems: ContextMenuProps[] = []; - // subitems.push({ - // description: BoolCast(this.props.Document.autoHeight) ? "Manual Height" : "Auto Height", - // event: action(() => Doc.GetProto(this.props.Document).autoHeight = !BoolCast(this.props.Document.autoHeight)), icon: "expand-arrows-alt" - // }); - // ContextMenu.Instance.addItem({ description: "Text Funcs...", subitems: subitems, icon: "text-height" }); - } - render() { let self = this; @@ -842,14 +879,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe onKeyDown={this.onKeyPress} onFocus={this.onFocused} onClick={this.onClick} - onContextMenu={this.specificContextMenu} onBlur={this.onBlur} onPointerUp={this.onPointerUp} onPointerDown={this.onPointerDown} onMouseDown={this.onMouseDown} onWheel={this.onPointerWheel} - onPointerEnter={this.onPointerEnter} - onPointerLeave={this.onPointerLeave} + onPointerEnter={action(() => this._entered = true)} + onPointerLeave={action(() => this._entered = false)} > <div className={`formattedTextBox-inner${rounded}`} ref={this.createDropTarget} style={{ whiteSpace: "pre-wrap" }} /> </div> diff --git a/src/client/views/nodes/FormattedTextBoxComment.scss b/src/client/views/nodes/FormattedTextBoxComment.scss new file mode 100644 index 000000000..792cee182 --- /dev/null +++ b/src/client/views/nodes/FormattedTextBoxComment.scss @@ -0,0 +1,34 @@ +.FormattedTextBox-tooltip { + position: absolute; + pointer-events: none; + z-index: 20; + background: white; + border: 1px solid silver; + border-radius: 2px; + padding: 2px 10px; + margin-bottom: 7px; + -webkit-transform: translateX(-50%); + transform: translateX(-50%); + } + .FormattedTextBox-tooltip:before { + content: ""; + height: 0; width: 0; + position: absolute; + left: 50%; + margin-left: -5px; + bottom: -6px; + border: 5px solid transparent; + border-bottom-width: 0; + border-top-color: silver; + } + .FormattedTextBox-tooltip:after { + content: ""; + height: 0; width: 0; + position: absolute; + left: 50%; + margin-left: -5px; + bottom: -4.5px; + border: 5px solid transparent; + border-bottom-width: 0; + border-top-color: white; + }
\ No newline at end of file diff --git a/src/client/views/nodes/FormattedTextBoxComment.tsx b/src/client/views/nodes/FormattedTextBoxComment.tsx new file mode 100644 index 000000000..9123f8aed --- /dev/null +++ b/src/client/views/nodes/FormattedTextBoxComment.tsx @@ -0,0 +1,110 @@ +import { Plugin, EditorState } from "prosemirror-state" +import './FormattedTextBoxComment.scss' +import { ResolvedPos, Mark } from "prosemirror-model"; +import { EditorView } from "prosemirror-view"; +import { Doc } from "../../../new_fields/Doc"; + +export let selectionSizePlugin = new Plugin({ + view(editorView) { return new SelectionSizeTooltip(editorView); } +}) +export function findOtherUserMark(marks: Mark[]): Mark | undefined { + return marks.find(m => m.attrs.userid && m.attrs.userid !== Doc.CurrentUserEmail); +} +export function findUserMark(marks: Mark[]): Mark | undefined { + return marks.find(m => m.attrs.userid); +} +export function findStartOfMark(rpos: ResolvedPos, view: EditorView, finder: (marks: Mark[]) => Mark | undefined) { + let before = 0; + let nbef = rpos.nodeBefore; + while (nbef && finder(nbef.marks)) { + before += nbef.nodeSize; + rpos = view.state.doc.resolve(rpos.pos - nbef.nodeSize); + rpos && (nbef = rpos.nodeBefore); + } + return before; +} +export function findEndOfMark(rpos: ResolvedPos, view: EditorView, finder: (marks: Mark[]) => Mark | undefined) { + let after = 0; + let naft = rpos.nodeAfter; + while (naft && finder(naft.marks)) { + after += naft.nodeSize; + rpos = view.state.doc.resolve(rpos.pos + naft.nodeSize); + rpos && (naft = rpos.nodeAfter); + } + return after; +} + + +export class SelectionSizeTooltip { + static tooltip: HTMLElement; + static tooltipText: HTMLElement; + static start: number; + static end: number; + static mark: Mark; + static opened: boolean; + static textBox: any; + constructor(view: any) { + if (!SelectionSizeTooltip.tooltip) { + const root = document.getElementById("root"); + let input = document.createElement("input"); + input.type = "checkbox"; + SelectionSizeTooltip.tooltip = document.createElement("div"); + SelectionSizeTooltip.tooltipText = document.createElement("div"); + SelectionSizeTooltip.tooltip.appendChild(SelectionSizeTooltip.tooltipText); + SelectionSizeTooltip.tooltip.className = "FormattedTextBox-tooltip"; + SelectionSizeTooltip.tooltip.style.pointerEvents = "all"; + SelectionSizeTooltip.tooltip.appendChild(input); + SelectionSizeTooltip.tooltip.onpointerdown = (e: PointerEvent) => { + let keep = e.target && (e.target as any).type === "checkbox"; + SelectionSizeTooltip.opened = keep || !SelectionSizeTooltip.opened; + SelectionSizeTooltip.textBox.setAnnotation( + SelectionSizeTooltip.start, SelectionSizeTooltip.end, SelectionSizeTooltip.mark, + SelectionSizeTooltip.opened, keep); + }; + root && root.appendChild(SelectionSizeTooltip.tooltip); + } + this.update(view, undefined); + } + + public static Hide() { + SelectionSizeTooltip.textBox = undefined; + SelectionSizeTooltip.tooltip && (SelectionSizeTooltip.tooltip.style.display = "none"); + } + public static SetState(textBox: any, opened: boolean, start: number, end: number, mark: Mark) { + SelectionSizeTooltip.textBox = textBox; + SelectionSizeTooltip.start = start; + SelectionSizeTooltip.end = end; + SelectionSizeTooltip.mark = mark; + SelectionSizeTooltip.opened = opened; + SelectionSizeTooltip.textBox && SelectionSizeTooltip.tooltip && (SelectionSizeTooltip.tooltip.style.display = ""); + } + + update(view: EditorView, lastState?: EditorState) { + let state = view.state; + // Don't do anything if the document/selection didn't change + if (lastState && lastState.doc.eq(state.doc) && + lastState.selection.eq(state.selection)) return; + + if (state.selection.$from) { + let nbef = findStartOfMark(state.selection.$from, view, findOtherUserMark); + let naft = findEndOfMark(state.selection.$from, view, findOtherUserMark); + let child = state.selection.$from.nodeBefore; + let mark = child && findOtherUserMark(child.marks); + if (mark && child && nbef && naft) { + SelectionSizeTooltip.tooltipText.textContent = mark.attrs.userid + " " + mark.attrs.modified; + // These are in screen coordinates + // let start = view.coordsAtPos(state.selection.from), end = view.coordsAtPos(state.selection.to); + let start = view.coordsAtPos(state.selection.from - nbef), end = view.coordsAtPos(state.selection.from - nbef); + // The box in which the tooltip is positioned, to use as base + let box = (document.getElementById("main-div") as any).getBoundingClientRect(); + // Find a center-ish x position from the selection endpoints (when + // crossing lines, end may be more to the left) + let left = Math.max((start.left + end.left) / 2, start.left + 3); + SelectionSizeTooltip.tooltip.style.left = (left - box.left) + "px"; + SelectionSizeTooltip.tooltip.style.bottom = (box.bottom - start.top) + "px"; + } + } + } + + destroy() { SelectionSizeTooltip.tooltip.style.display = "none" } +} diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index 8001b24a7..5afd4d834 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -60,7 +60,6 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { isSelected: returnFalse, select: emptyFunction, renderDepth: 1, - selectOnLoad: false, active: returnFalse, whenActiveChanged: emptyFunction, ScreenToLocalTransform: Transform.Identity, diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 258e218f0..e5917fefc 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -152,12 +152,18 @@ export class PDFViewer extends React.Component<IViewerProps> { if (this._pageSizes.length === 0) { this._isPage = Array<string>(this.props.pdf.numPages); this._pageSizes = Array<{ width: number, height: number }>(this.props.pdf.numPages); + this._visibleElements = Array<JSX.Element>(this.props.pdf.numPages); await Promise.all(this._pageSizes.map<Pdfjs.PDFPromise<any>>((val, i) => this.props.pdf.getPage(i + 1).then(action((page: Pdfjs.PDFPageProxy) => { this._pageSizes.splice(i, 1, { width: (page.view[page.rotate === 0 || page.rotate === 180 ? 2 : 3] - page.view[page.rotate === 0 || page.rotate === 180 ? 0 : 1]) * scale, height: (page.view[page.rotate === 0 || page.rotate === 180 ? 3 : 2] - page.view[page.rotate === 0 || page.rotate === 180 ? 1 : 0]) * scale }); + this._visibleElements.splice(i, 1, + <div key={`${this.props.url}-placeholder-${i + 1}`} className="pdfviewer-placeholder" + style={{ width: this._pageSizes[i].width, height: this._pageSizes[i].height }}> + "PAGE IS LOADING... " + </div>); this.getPlaceholderPage(i); })))); this.props.loaded(Math.max(...this._pageSizes.map(i => i.width)), this._pageSizes[0].height, this.props.pdf.numPages); diff --git a/src/client/views/pdf/Page.tsx b/src/client/views/pdf/Page.tsx index 0de1777e6..8df2dce29 100644 --- a/src/client/views/pdf/Page.tsx +++ b/src/client/views/pdf/Page.tsx @@ -64,13 +64,14 @@ export default class Page extends React.Component<IPageProps> { // lower scale = easier to read at small sizes, higher scale = easier to read at large sizes if (this._state !== "rendering" && !this._page && this._canvas.current && this._textLayer.current) { this._state = "rendering"; - let viewport = page.getViewport(scale); + let viewport = page.getViewport({ scale: scale }); this._canvas.current.width = this._width = viewport.width; this._canvas.current.height = this._height = viewport.height; this.props.pageLoaded(viewport); let ctx = this._canvas.current.getContext("2d"); if (ctx) { - page.render({ canvasContext: ctx, viewport: viewport }); // renders the page onto the canvas context + //@ts-ignore + page.render({ canvasContext: ctx, viewport: viewport, enableWebGL: true }); // renders the page onto the canvas context page.getTextContent().then(res => // renders text onto the text container //@ts-ignore Pdfjs.renderTextLayer({ @@ -258,7 +259,7 @@ export default class Page extends React.Component<IPageProps> { } } } - let text = selRange.extractContents().textContent; + let text = selRange.cloneContents().textContent; text && this.props.setSelectionText(text); // clear selection diff --git a/src/client/views/presentationview/PresentationElement.tsx b/src/client/views/presentationview/PresentationElement.tsx index 83413814f..80aa25f48 100644 --- a/src/client/views/presentationview/PresentationElement.tsx +++ b/src/client/views/presentationview/PresentationElement.tsx @@ -359,7 +359,6 @@ export default class PresentationElement extends React.Component<PresentationEle PanelHeight={() => 90} focus={emptyFunction} backgroundColor={returnEmptyString} - selectOnLoad={false} parentActive={returnFalse} whenActiveChanged={returnFalse} bringToFront={emptyFunction} diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx index 567bc6c97..386b5fe74 100644 --- a/src/client/views/search/SearchItem.tsx +++ b/src/client/views/search/SearchItem.tsx @@ -210,7 +210,6 @@ export class SearchItem extends React.Component<SearchItemProps> { PanelHeight={returnYDimension} focus={emptyFunction} backgroundColor={returnEmptyString} - selectOnLoad={false} parentActive={returnFalse} whenActiveChanged={returnFalse} bringToFront={emptyFunction} diff --git a/src/server/authentication/config/passport.ts b/src/server/authentication/config/passport.ts index d42741410..10b17de71 100644 --- a/src/server/authentication/config/passport.ts +++ b/src/server/authentication/config/passport.ts @@ -42,9 +42,11 @@ export let isAuthenticated = (req: Request, res: Response, next: NextFunction) = export let isAuthorized = (req: Request, res: Response, next: NextFunction) => { const provider = req.path.split("/").slice(-1)[0]; - if (_.find(req.user.tokens, { kind: provider })) { - next(); - } else { - res.redirect(`/auth/${provider}`); + if (req.user) { + if (_.find((req.user as any).tokens, { kind: provider })) { + next(); + } else { + res.redirect(`/auth/${provider}`); + } } };
\ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index 34a0a19f1..fca90a585 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -116,7 +116,7 @@ function addSecureRoute(method: Method, ) { let abstracted = (req: express.Request, res: express.Response) => { if (req.user) { - handler(req.user, res, req); + handler(req.user as any, res, req); } else { req.session!.target = req.originalUrl; onRejection(res, req); @@ -447,7 +447,7 @@ function LoadPage(file: string, pageNumber: number, res: Response) { console.log(pageNumber); pdf.getPage(pageNumber).then((page: Pdfjs.PDFPageProxy) => { console.log("reading " + page); - let viewport = page.getViewport(1); + let viewport = page.getViewport({ scale: 1 }); let canvasAndContext = factory.create(viewport.width, viewport.height); let renderContext = { canvasContext: canvasAndContext.context, @@ -811,8 +811,8 @@ const EndpointHandlerMap = new Map<GoogleApiServerUtils.Action, GoogleApiServerU ]); app.post(RouteStore.googleDocs + "/:sector/:action", (req, res) => { - let sector = req.params.sector; - let action = req.params.action; + let sector: any = req.params.sector; + let action: any = req.params.action; GoogleApiServerUtils.GetEndpoint(GoogleApiServerUtils.Service[sector], { credentials, token }).then(endpoint => { let handler = EndpointHandlerMap.get(action); if (endpoint && handler) { |