diff options
Diffstat (limited to 'src/client/views/nodes')
-rw-r--r-- | src/client/views/nodes/CollectionFreeFormDocumentView.tsx | 1 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentContentsView.tsx | 4 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 18 | ||||
-rw-r--r-- | src/client/views/nodes/FieldView.tsx | 2 | ||||
-rw-r--r-- | src/client/views/nodes/FormattedTextBox.scss | 88 | ||||
-rw-r--r-- | src/client/views/nodes/FormattedTextBox.tsx | 203 | ||||
-rw-r--r-- | src/client/views/nodes/FormattedTextBoxComment.scss | 34 | ||||
-rw-r--r-- | src/client/views/nodes/FormattedTextBoxComment.tsx | 152 | ||||
-rw-r--r-- | src/client/views/nodes/KeyValuePair.tsx | 1 | ||||
-rw-r--r-- | src/client/views/nodes/LinkEditor.scss | 145 | ||||
-rw-r--r-- | src/client/views/nodes/LinkEditor.tsx | 400 | ||||
-rw-r--r-- | src/client/views/nodes/LinkMenu.scss | 137 | ||||
-rw-r--r-- | src/client/views/nodes/LinkMenu.tsx | 75 | ||||
-rw-r--r-- | src/client/views/nodes/LinkMenuGroup.tsx | 105 | ||||
-rw-r--r-- | src/client/views/nodes/LinkMenuItem.tsx | 139 | ||||
-rw-r--r-- | src/client/views/nodes/WebBox.tsx | 25 |
16 files changed, 418 insertions, 1111 deletions
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 7631ecc6c..c9c394960 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -8,7 +8,6 @@ import { DocumentView, DocumentViewProps, positionSchema } from "./DocumentView" import "./DocumentView.scss"; import React = require("react"); import { Doc } from "../../../new_fields/Doc"; -import { returnEmptyString } from "../../../Utils"; export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { x?: number; diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index d77662355..d0e117fe4 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -14,6 +14,7 @@ import { ImageBox } from "./ImageBox"; import { DragBox } from "./DragBox"; import { ButtonBox } from "./ButtonBox"; import { PresBox } from "./PresBox"; +import { LinkFollowBox } from "../linking/LinkFollowBox"; import { IconBox } from "./IconBox"; import { KeyValueBox } from "./KeyValueBox"; import { PDFBox } from "./PDFBox"; @@ -30,6 +31,7 @@ import { List } from "../../../new_fields/List"; import { Doc } from "../../../new_fields/Doc"; import DirectoryImportBox from "../../util/Import & Export/DirectoryImportBox"; import { ScriptField } from "../../../new_fields/ScriptField"; +import { fromPromise } from "mobx-utils"; const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? type BindingProps = Without<FieldViewProps, 'fieldKey'>; @@ -108,7 +110,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { if (!this.layout && (this.props.layoutKey !== "overlayLayout" || !this.templates.length)) return (null); return <ObserverJsxParser blacklistedAttrs={[]} - components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, DragBox, ButtonBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox }} + components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, DragBox, ButtonBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox, LinkFollowBox }} bindings={this.CreateBindings()} jsx={this.finalLayout} showWarnings={true} diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 3cf86b6f9..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} />); @@ -761,18 +759,20 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } let showTextTitle = showTitle && StrCast(this.layoutDoc.layout).startsWith("<FormattedTextBox") ? showTitle : undefined; let brushDegree = Doc.IsBrushedDegree(this.props.Document); + let fullDegree = Doc.isBrushedHighlightedDegree(this.props.Document); + // console.log(fullDegree) let borderRounding = StrCast(Doc.GetProto(this.props.Document).borderRounding); - let localScale = this.props.ScreenToLocalTransform().Scale * brushDegree; + let localScale = this.props.ScreenToLocalTransform().Scale * fullDegree; return ( <div className={`documentView-node${this.topMost ? "-topmost" : ""}`} ref={this._mainCont} style={{ pointerEvents: this.layoutDoc.isBackground && !this.isSelected() ? "none" : "all", color: foregroundColor, - outlineColor: ["transparent", "maroon", "maroon"][brushDegree], - outlineStyle: ["none", "dashed", "solid"][brushDegree], - outlineWidth: brushDegree && !borderRounding ? `${localScale}px` : "0px", - border: brushDegree && borderRounding ? `${["none", "dashed", "solid"][brushDegree]} ${["transparent", "maroon", "maroon"][brushDegree]} ${localScale}px` : undefined, + outlineColor: ["transparent", "maroon", "maroon", "yellow"][fullDegree], + outlineStyle: ["none", "dashed", "solid", "solid"][fullDegree], + outlineWidth: fullDegree && !borderRounding ? `${localScale}px` : "0px", + border: fullDegree && borderRounding ? `${["none", "dashed", "solid", "solid"][fullDegree]} ${["transparent", "maroon", "maroon", "yellow"][fullDegree]} ${localScale}px` : undefined, borderRadius: "inherit", background: backgroundColor, width: nativeWidth, @@ -808,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..8f47402c4 100644 --- a/src/client/views/nodes/FormattedTextBox.scss +++ b/src/client/views/nodes/FormattedTextBox.scss @@ -65,4 +65,90 @@ .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; +} + +footnote { + display: inline-block; + position: relative; + cursor: pointer; + div { + padding : 0 !important; + } +} + +footnote::after { + content: counter(prosemirror-footnote); + vertical-align: super; + font-size: 75%; + counter-increment: prosemirror-footnote; +} + +.ProseMirror { + counter-reset: prosemirror-footnote; + } + +.footnote-tooltip { + cursor: auto; + font-size: 75%; + position: absolute; + left: -30px; + top: calc(100% + 10px); + background: silver; + padding: 3px; + border-radius: 2px; + max-width: 100px; + min-width: 50px; + width: max-content; +} + +.footnote-tooltip::before { + border: 5px solid silver; + border-top-width: 0px; + border-left-color: transparent; + border-right-color: transparent; + position: absolute; + top: -5px; + left: 27px; + content: " "; + height: 0; + width: 0; +} + +ol { counter-reset: deci1 0;} +.decimal1-ol {counter-reset: deci1; 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 } +.decimal5-ol {counter-reset: deci5; p { display: inline }; font-size: 10 } +.decimal6-ol {counter-reset: deci6; p { display: inline }; font-size: 10 } +.decimal7-ol {counter-reset: deci7; 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;} +.decimal1:before { content: counter(deci1) ")"; counter-increment: deci1; display:inline-block; width: 30} +.decimal2:before { content: counter(deci1) "." counter(deci2) ")"; counter-increment: deci2; display:inline-block; width: 35} +.decimal3:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) ")"; counter-increment: deci3; display:inline-block; width: 35} +.decimal4:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) ")"; counter-increment: deci4; display:inline-block; width: 40} +.decimal5:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) "." counter(deci5) ")"; counter-increment: deci5; display:inline-block; width: 40} +.decimal6:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) "." counter(deci5) "." counter(deci6) ")"; counter-increment: deci6; display:inline-block; width: 45} +.decimal7:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) "." counter(deci5) "." counter(deci6) "." counter(deci7) ")"; counter-increment: deci7; display:inline-block; width: 50} +.upper-alpha:before { content: counter(deci1) "." counter(ualph, upper-alpha) ")"; counter-increment: ualph; display:inline-block; width: 35 } +.lower-roman:before { content: counter(deci1) "." counter(ualph, upper-alpha) "." counter(lroman, lower-roman) ")"; counter-increment: lroman;display:inline-block; width: 50 } +.lower-alpha:before { content: counter(deci1) "." 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 6af5c8f0b..b671d06ea 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,14 +15,14 @@ import { List } from '../../../new_fields/List'; import { RichTextField } 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'; import { DragManager } from "../../util/DragManager"; import buildKeymap from "../../util/ProsemirrorExampleTransfer"; import { inpRules } from "../../util/RichTextRules"; -import { ImageResizeView, schema, SummarizedView } from "../../util/RichTextSchema"; +import { ImageResizeView, schema, SummarizedView, OrderedListView, FootnoteView } from "../../util/RichTextSchema"; import { SelectionManager } from "../../util/SelectionManager"; import { TooltipLinkingMenu } from "../../util/TooltipLinkingMenu"; import { TooltipTextMenu } from "../../util/TooltipTextMenu"; @@ -39,6 +39,7 @@ import { ReplaceStep } from 'prosemirror-transform'; import { DocumentType } from '../../documents/DocumentTypes'; import { RichTextUtils } from '../../../new_fields/RichTextUtils'; import * as _ from "lodash"; +import { formattedTextBoxCommentPlugin, FormattedTextBoxComment } from './FormattedTextBoxComment'; library.add(faEdit); library.add(faSmile, faTextHeight, faUpload); @@ -68,27 +69,27 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe public static LayoutString(fieldStr: string = "data") { return FieldView.LayoutString(FormattedTextBox, fieldStr); } - public static blankState = () => EditorState.create(FormattedTextBox.Instance._configuration); + public static blankState = () => EditorState.create(FormattedTextBox.Instance.config); public static Instance: FormattedTextBox; - private _configuration: any; - 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; @@ -117,7 +118,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe @undoBatch public setFontColor(color: string) { - let self = this; if (this._editorView!.state.selection.from === this._editorView!.state.selection.to) return false; if (this._editorView!.state.selection.to - this._editorView!.state.selection.from > this._editorView!.state.doc.nodeSize - 3) { this.props.Document.color = color; @@ -130,15 +130,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); } @@ -172,21 +170,40 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } } + // this should be internal to prosemirror, but is needed + // here to make sure that footnote view nodes in the overlay editor + // get removed when they're not selected. + syncNodeSelection(view: any, sel: any) { + if (sel instanceof NodeSelection) { + var desc = view.docView.descAt(sel.from); + if (desc != view.lastSelectedViewDesc) { + if (view.lastSelectedViewDesc) { + view.lastSelectedViewDesc.deselectNode(); + view.lastSelectedViewDesc = null; + } + if (desc) { desc.selectNode(); } + view.lastSelectedViewDesc = desc; + } + } else { + if (view.lastSelectedViewDesc) { + view.lastSelectedViewDesc.deselectNode(); + view.lastSelectedViewDesc = null; + } + } + } + dispatchTransaction = (tx: Transaction) => { if (this._editorView) { const state = this._editorView.state.apply(tx); - FormattedTextBox._toolTipTextMenu && (FormattedTextBox._toolTipTextMenu.HackToFixTextSelectionGlitch = true); this._editorView.updateState(state); - FormattedTextBox._toolTipTextMenu && (FormattedTextBox._toolTipTextMenu.HackToFixTextSelectionGlitch = false); - if (state.selection.empty && FormattedTextBox._toolTipTextMenu) { - const marks = tx.storedMarks; - if (marks) { FormattedTextBox._toolTipTextMenu.mark_key_pressed(marks); } + this.syncNodeSelection(this._editorView, this._editorView.state.selection); // bcz: ugh -- shouldn't be needed but without this the overlay view's footnote popup doesn't get deselected + if (state.selection.empty && FormattedTextBox._toolTipTextMenu && tx.storedMarks) { + FormattedTextBox._toolTipTextMenu.mark_key_pressed(tx.storedMarks); } this._applyingChange = true; - const fieldkey = "preview"; - if (this.extensionDoc) this.extensionDoc.text = state.doc.textBetween(0, state.doc.content.size, "\n\n"); - if (this.extensionDoc) this.extensionDoc.lastModified = new DateField(new Date(Date.now())); + this.extensionDoc && (this.extensionDoc.text = state.doc.textBetween(0, state.doc.content.size, "\n\n")); + this.extensionDoc && (this.extensionDoc.lastModified = new DateField(new Date(Date.now()))); this.dataDoc[this.props.fieldKey] = new RichTextField(JSON.stringify(state.toJSON())); this._applyingChange = false; let title = StrCast(this.dataDoc.title); @@ -200,7 +217,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe public highlightSearchTerms = (terms: String[]) => { if (this._editorView && (this._editorView as any).docView) { - const fieldkey = "preview"; const doc = this._editorView.state.doc; const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight); doc.nodesBetween(0, doc.content.size, (node: ProsNode, pos: number, parent: ProsNode, index: number) => { @@ -241,12 +257,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 @@ -270,13 +282,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(); + } } } @@ -320,15 +331,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() { - this._configuration = { + @computed get config() { + return { schema, inpRules, //these currently don't do anything, but could eventually be helpful plugins: this.props.isOverlay ? [ @@ -341,13 +348,23 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe props: { attributes: { class: "ProseMirror-example-setup-style" } } - }) + }), + formattedTextBoxCommentPlugin ] : [ 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(), @@ -370,13 +387,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe incomingValue => { if (this._editorView && !this._applyingChange) { let updatedState = JSON.parse(incomingValue); - this._editorView.updateState(EditorState.fromJSON(this._configuration, updatedState)); + this._editorView.updateState(EditorState.fromJSON(this.config, updatedState)); this.tryUpdateHeight(); } } ); - this.pullReactionDisposer = reaction( + this._pullReactionDisposer = reaction( () => this.props.Document[Pulls], () => { if (!DocumentDecorations.hasPulledHack) { @@ -387,7 +404,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } ); - this.pushReactionDisposer = reaction( + this._pushReactionDisposer = reaction( () => this.props.Document[Pushes], () => { if (!DocumentDecorations.hasPushedHack) { @@ -412,7 +429,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this.dataDoc.lastModified = undefined; } }, { fireImmediately: true }); - this.setupEditor(this._configuration, this.dataDoc, this.props.fieldKey); + + + this.setupEditor(this.config, this.dataDoc, this.props.fieldKey); this._searchReactionDisposer = reaction(() => { return StrCast(this.props.Document.search_string); @@ -509,7 +528,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } } - clipboardTextSerializer = (slice: Slice): string => { let text = "", separated = true; const from = 0, to = slice.content.size; @@ -609,37 +627,47 @@ 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, nodeViews: { image(node, view, getPos) { return new ImageResizeView(node, view, getPos); }, star(node, view, getPos) { return new SummarizedView(node, view, getPos); }, + ordered_list(node, view, getPos) { return new OrderedListView(node, view, getPos); }, + footnote(node, view, getPos) { return new FootnoteView(node, view, getPos) } }, clipboardTextSerializer: this.clipboardTextSerializer, handlePaste: this.handlePaste, }); + (this._editorView as any).isOverlay = this.props.isOverlay; if (startup) { Doc.GetProto(doc).documentText = undefined; this._editorView.dispatch(this._editorView.state.tr.insertText(startup)); } } - 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 => { @@ -703,15 +731,21 @@ 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"; - } + FormattedTextBoxComment.textBox = this; 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); @@ -754,7 +788,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe return self._toolTipTextMenu = new TooltipTextMenu(_editorView, myprops); } }); - //this.props.Document.tooltip = self._toolTipTextMenu; } tooltipLinkingMenuPlugin() { @@ -772,13 +805,32 @@ 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(); + } + 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; + return now.toLocaleDateString() + ' ' + h + ':' + m + ' ' + 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; @@ -805,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; @@ -845,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..255000936 --- /dev/null +++ b/src/client/views/nodes/FormattedTextBoxComment.tsx @@ -0,0 +1,152 @@ +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"; +import { schema } from "../../util/RichTextSchema"; +import { DocServer } from "../../DocServer"; +import { Utils } from "../../../Utils"; +import { StrCast } from "../../../new_fields/Types"; + +export let formattedTextBoxCommentPlugin = new Plugin({ + view(editorView) { return new FormattedTextBoxComment(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 findLinkMark(marks: Mark[]): Mark | undefined { + return marks.find(m => m.type === schema.marks.link); +} +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 FormattedTextBoxComment { + 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 (!FormattedTextBoxComment.tooltip) { + const root = document.getElementById("root"); + let input = document.createElement("input"); + input.type = "checkbox"; + FormattedTextBoxComment.tooltip = document.createElement("div"); + FormattedTextBoxComment.tooltipText = document.createElement("div"); + FormattedTextBoxComment.tooltip.appendChild(FormattedTextBoxComment.tooltipText); + FormattedTextBoxComment.tooltip.className = "FormattedTextBox-tooltip"; + FormattedTextBoxComment.tooltip.style.pointerEvents = "all"; + FormattedTextBoxComment.tooltip.appendChild(input); + FormattedTextBoxComment.tooltip.onpointerdown = (e: PointerEvent) => { + let keep = e.target && (e.target as any).type === "checkbox"; + FormattedTextBoxComment.opened = keep || !FormattedTextBoxComment.opened; + FormattedTextBoxComment.textBox && FormattedTextBoxComment.textBox.setAnnotation( + FormattedTextBoxComment.start, FormattedTextBoxComment.end, FormattedTextBoxComment.mark, + FormattedTextBoxComment.opened, keep); + }; + root && root.appendChild(FormattedTextBoxComment.tooltip); + } + this.update(view, undefined); + } + + public static Hide() { + FormattedTextBoxComment.textBox = undefined; + FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = "none"); + } + public static SetState(textBox: any, opened: boolean, start: number, end: number, mark: Mark) { + FormattedTextBoxComment.textBox = textBox; + FormattedTextBoxComment.start = start; + FormattedTextBoxComment.end = end; + FormattedTextBoxComment.mark = mark; + FormattedTextBoxComment.opened = opened; + FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.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; + + let set = "none" + if (state.selection.$from) { + let nbef = findStartOfMark(state.selection.$from, view, findOtherUserMark); + let naft = findEndOfMark(state.selection.$from, view, findOtherUserMark); + const spos = state.selection.$from.pos - nbef; + const epos = state.selection.$from.pos + naft; + let child = state.selection.$from.nodeBefore; + let mark = child && findOtherUserMark(child.marks); + let noselection = view.state.selection.$from === view.state.selection.$to; + if (mark && child && (nbef || naft) && (!mark.attrs.opened || noselection)) { + FormattedTextBoxComment.SetState(this, mark.attrs.opened, spos, epos, mark); + } + if (mark && child && nbef && naft) { + FormattedTextBoxComment.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); + FormattedTextBoxComment.tooltip.style.left = (left - box.left) + "px"; + FormattedTextBoxComment.tooltip.style.bottom = (box.bottom - start.top) + "px"; + set = ""; + } + } + if (set === "none" && state.selection.$from) { + FormattedTextBoxComment.textBox = undefined; + let nbef = findStartOfMark(state.selection.$from, view, findLinkMark); + let naft = findEndOfMark(state.selection.$from, view, findLinkMark); + let child = state.selection.$from.nodeBefore; + let mark = child && findLinkMark(child.marks); + if (mark && child && nbef && naft) { + FormattedTextBoxComment.tooltipText.textContent = "link : " + (mark.attrs.title || mark.attrs.href); + if (mark.attrs.href.indexOf(Utils.prepend("/doc/")) === 0) { + let docTarget = mark.attrs.href.replace(Utils.prepend("/doc/"), "").split("?")[0]; + docTarget && DocServer.GetRefField(docTarget).then(linkDoc => + (linkDoc as Doc) && (FormattedTextBoxComment.tooltipText.textContent = "link :" + StrCast((linkDoc as Doc)!.title))); + } + // 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); + FormattedTextBoxComment.tooltip.style.left = (left - box.left) + "px"; + FormattedTextBoxComment.tooltip.style.bottom = (box.bottom - start.top) + "px"; + set = ""; + } + } + FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = set); + } + + destroy() { FormattedTextBoxComment.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/nodes/LinkEditor.scss b/src/client/views/nodes/LinkEditor.scss deleted file mode 100644 index fc5f2410c..000000000 --- a/src/client/views/nodes/LinkEditor.scss +++ /dev/null @@ -1,145 +0,0 @@ -@import "../globalCssVariables"; - -.linkEditor { - width: 100%; - height: auto; - font-size: 12px; // TODO -} - -.linkEditor-back { - margin-bottom: 6px; -} - -.linkEditor-info { - border-bottom: 0.5px solid $light-color-secondary; - padding-bottom: 6px; - margin-bottom: 6px; - display: flex; - justify-content: space-between; - - .linkEditor-linkedTo { - width: calc(100% - 26px); - } -} - -.linkEditor-button { - width: 20px; - height: 20px; - margin-left: 6px; - padding: 0; - // font-size: 12px; - border-radius: 10px; - - &:disabled { - background-color: gray; - } -} - -.linkEditor-groupsLabel { - display: flex; - justify-content: space-between; -} - -.linkEditor-group { - background-color: $light-color-secondary; - padding: 6px; - margin: 3px 0; - border-radius: 3px; - - .linkEditor-group-row { - display: flex; - margin-bottom: 3px; - - .linkEditor-group-row-label { - margin-right: 6px; - } - } - - .linkEditor-metadata-row { - display: flex; - justify-content: space-between; - margin-bottom: 6px; - - .linkEditor-error { - border-color: red; - } - - input { - width: calc(50% - 16px); - height: 20px; - } - - button { - width: 20px; - height: 20px; - margin-left: 3px; - padding: 0; - font-size: 10px; - } - } -} - - -.linkEditor-dropdown { - width: 100%; - position: relative; - z-index: 999; - - input { - width: 100%; - } - - .linkEditor-options-wrapper { - width: 100%; - position: absolute; - top: 19px; - left: 0; - display: flex; - flex-direction: column; - } - - .linkEditor-option { - background-color: $light-color-secondary; - border: 1px solid $intermediate-color; - border-top: 0; - padding: 3px; - cursor: pointer; - - &:hover { - background-color: lightgray; - } - - &.onDown { - background-color: gray; - } - } -} - -.linkEditor-typeButton { - background-color: transparent; - color: $dark-color; - width: 100%; - height: 20px; - padding: 0 3px; - padding-bottom: 2px; - text-align: left; - text-transform: none; - letter-spacing: normal; - font-size: 12px; - font-weight: bold; - - &:hover { - background-color: $light-color; - } -} - -.linkEditor-group-buttons { - height: 20px; - display: flex; - justify-content: flex-end; - margin-top: 5px; - - .linkEditor-button { - margin-left: 6px; - } -}
\ No newline at end of file diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx deleted file mode 100644 index ecb3e9db4..000000000 --- a/src/client/views/nodes/LinkEditor.tsx +++ /dev/null @@ -1,400 +0,0 @@ -import { observable, computed, action, trace } from "mobx"; -import React = require("react"); -import { observer } from "mobx-react"; -import './LinkEditor.scss'; -import { StrCast, Cast, FieldValue } from "../../../new_fields/Types"; -import { Doc } from "../../../new_fields/Doc"; -import { LinkManager } from "../../util/LinkManager"; -import { Docs } from "../../documents/Documents"; -import { Utils } from "../../../Utils"; -import { faArrowLeft, faEllipsisV, faTable, faTrash, faCog, faExchangeAlt, faTimes, faPlus } from '@fortawesome/free-solid-svg-icons'; -import { library } from "@fortawesome/fontawesome-svg-core"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { SetupDrag } from "../../util/DragManager"; -import { SchemaHeaderField, RandomPastel } from "../../../new_fields/SchemaHeaderField"; - -library.add(faArrowLeft, faEllipsisV, faTable, faTrash, faCog, faExchangeAlt, faTimes, faPlus); - - -interface GroupTypesDropdownProps { - groupType: string; - setGroupType: (group: string) => void; -} -// this dropdown could be generalized -@observer -class GroupTypesDropdown extends React.Component<GroupTypesDropdownProps> { - @observable private _searchTerm: string = this.props.groupType; - @observable private _groupType: string = this.props.groupType; - @observable private _isEditing: boolean = false; - - @action - createGroup = (groupType: string): void => { - this.props.setGroupType(groupType); - LinkManager.Instance.addGroupType(groupType); - } - - @action - onChange = (val: string): void => { - this._searchTerm = val; - this._groupType = val; - this._isEditing = true; - } - - @action - onKeyDown = (e: React.KeyboardEvent): void => { - if (e.key === "Enter") { - let allGroupTypes = Array.from(LinkManager.Instance.getAllGroupTypes()); - let groupOptions = allGroupTypes.filter(groupType => groupType.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1); - let exactFound = groupOptions.findIndex(groupType => groupType.toUpperCase() === this._searchTerm.toUpperCase()); - - if (exactFound > -1) { - let groupType = groupOptions[exactFound]; - this.props.setGroupType(groupType); - this._groupType = groupType; - } else { - this.createGroup(this._searchTerm); - this._groupType = this._searchTerm; - } - - this._searchTerm = this._groupType; - this._isEditing = false; - } - } - - @action - onOptionClick = (value: string, createNew: boolean): void => { - if (createNew) { - this.createGroup(this._searchTerm); - this._groupType = this._searchTerm; - - } else { - this.props.setGroupType(value); - this._groupType = value; - - } - this._searchTerm = this._groupType; - this._isEditing = false; - } - - @action - onButtonPointerDown = (): void => { - this._isEditing = true; - } - - renderOptions = (): JSX.Element[] | JSX.Element => { - if (this._searchTerm === "") return <></>; - - let allGroupTypes = Array.from(LinkManager.Instance.getAllGroupTypes()); - let groupOptions = allGroupTypes.filter(groupType => groupType.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1); - let exactFound = groupOptions.findIndex(groupType => groupType.toUpperCase() === this._searchTerm.toUpperCase()) > -1; - - let options = groupOptions.map(groupType => { - let ref = React.createRef<HTMLDivElement>(); - return <div key={groupType} ref={ref} className="linkEditor-option" - onClick={() => this.onOptionClick(groupType, false)}>{groupType}</div>; - }); - - // if search term does not already exist as a group type, give option to create new group type - if (!exactFound && this._searchTerm !== "") { - let ref = React.createRef<HTMLDivElement>(); - options.push(<div key={""} ref={ref} className="linkEditor-option" - onClick={() => this.onOptionClick(this._searchTerm, true)}>Define new "{this._searchTerm}" relationship</div>); - } - - return options; - } - - render() { - if (this._isEditing || this._groupType === "") { - return ( - <div className="linkEditor-dropdown"> - <input type="text" value={this._groupType} placeholder="Search for or create a new group" - onChange={e => this.onChange(e.target.value)} onKeyDown={this.onKeyDown} autoFocus></input> - <div className="linkEditor-options-wrapper"> - {this.renderOptions()} - </div> - </div > - ); - } else { - return <button className="linkEditor-typeButton" onClick={() => this.onButtonPointerDown()}>{this._groupType}</button>; - } - } -} - - -interface LinkMetadataEditorProps { - id: string; - groupType: string; - mdDoc: Doc; - mdKey: string; - mdValue: string; - changeMdIdKey: (id: string, newKey: string) => void; -} -@observer -class LinkMetadataEditor extends React.Component<LinkMetadataEditorProps> { - @observable private _key: string = this.props.mdKey; - @observable private _value: string = this.props.mdValue; - @observable private _keyError: boolean = false; - - @action - setMetadataKey = (value: string): void => { - let groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType); - - // don't allow user to create existing key - let newIndex = groupMdKeys.findIndex(key => key.toUpperCase() === value.toUpperCase()); - if (newIndex > -1) { - this._keyError = true; - this._key = value; - return; - } else { - this._keyError = false; - } - - // set new value for key - let currIndex = groupMdKeys.findIndex(key => { - return StrCast(key).toUpperCase() === this._key.toUpperCase(); - }); - if (currIndex === -1) console.error("LinkMetadataEditor: key was not found"); - groupMdKeys[currIndex] = value; - - this.props.changeMdIdKey(this.props.id, value); - this._key = value; - LinkManager.Instance.setMetadataKeysForGroup(this.props.groupType, [...groupMdKeys]); - } - - @action - setMetadataValue = (value: string): void => { - if (!this._keyError) { - this._value = value; - this.props.mdDoc[this._key] = value; - } - } - - @action - removeMetadata = (): void => { - let groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType); - - let index = groupMdKeys.findIndex(key => key.toUpperCase() === this._key.toUpperCase()); - if (index === -1) console.error("LinkMetadataEditor: key was not found"); - groupMdKeys.splice(index, 1); - - LinkManager.Instance.setMetadataKeysForGroup(this.props.groupType, groupMdKeys); - this._key = ""; - } - - render() { - return ( - <div className="linkEditor-metadata-row"> - <input className={this._keyError ? "linkEditor-error" : ""} type="text" value={this._key === "new key" ? "" : this._key} placeholder="key" onChange={e => this.setMetadataKey(e.target.value)}></input>: - <input type="text" value={this._value} placeholder="value" onChange={e => this.setMetadataValue(e.target.value)}></input> - <button onClick={() => this.removeMetadata()}><FontAwesomeIcon icon="times" size="sm" /></button> - </div> - ); - } -} - -interface LinkGroupEditorProps { - sourceDoc: Doc; - linkDoc: Doc; - groupDoc: Doc; -} -@observer -export class LinkGroupEditor extends React.Component<LinkGroupEditorProps> { - - private _metadataIds: Map<string, string> = new Map(); - - constructor(props: LinkGroupEditorProps) { - super(props); - - let groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(StrCast(props.groupDoc.type)); - groupMdKeys.forEach(key => { - this._metadataIds.set(key, Utils.GenerateGuid()); - }); - } - - @action - setGroupType = (groupType: string): void => { - this.props.groupDoc.type = groupType; - } - - removeGroupFromLink = (groupType: string): void => { - LinkManager.Instance.removeGroupFromAnchor(this.props.linkDoc, this.props.sourceDoc, groupType); - } - - deleteGroup = (groupType: string): void => { - LinkManager.Instance.deleteGroupType(groupType); - } - - copyGroup = async (groupType: string): Promise<void> => { - let sourceGroupDoc = this.props.groupDoc; - const sourceMdDoc = await Cast(sourceGroupDoc.metadata, Doc); - if (!sourceMdDoc) return; - - let destDoc = LinkManager.Instance.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc); - // let destGroupList = LinkManager.Instance.getAnchorGroups(this.props.linkDoc, destDoc); - let keys = LinkManager.Instance.getMetadataKeysInGroup(groupType); - - // create new metadata doc with copied kvp - let destMdDoc = new Doc(); - destMdDoc.anchor1 = StrCast(sourceMdDoc.anchor2); - destMdDoc.anchor2 = StrCast(sourceMdDoc.anchor1); - keys.forEach(key => { - let val = sourceMdDoc[key] === undefined ? "" : StrCast(sourceMdDoc[key]); - destMdDoc[key] = val; - }); - - // create new group doc with new metadata doc - let destGroupDoc = new Doc(); - destGroupDoc.type = groupType; - destGroupDoc.metadata = destMdDoc; - - if (destDoc) { - LinkManager.Instance.addGroupToAnchor(this.props.linkDoc, destDoc, destGroupDoc, true); - } - } - - @action - addMetadata = (groupType: string): void => { - this._metadataIds.set("new key", Utils.GenerateGuid()); - let mdKeys = LinkManager.Instance.getMetadataKeysInGroup(groupType); - // only add "new key" if there is no other key with value "new key"; prevents spamming - if (mdKeys.indexOf("new key") === -1) mdKeys.push("new key"); - LinkManager.Instance.setMetadataKeysForGroup(groupType, mdKeys); - } - - // for key rendering purposes - changeMdIdKey = (id: string, newKey: string) => { - this._metadataIds.set(newKey, id); - } - - renderMetadata = (): JSX.Element[] => { - let metadata: Array<JSX.Element> = []; - let groupDoc = this.props.groupDoc; - const mdDoc = FieldValue(Cast(groupDoc.metadata, Doc)); - if (!mdDoc) { - return []; - } - let groupType = StrCast(groupDoc.type); - let groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(groupType); - - groupMdKeys.forEach((key) => { - let val = StrCast(mdDoc[key]); - metadata.push( - <LinkMetadataEditor key={"mded-" + this._metadataIds.get(key)} id={this._metadataIds.get(key)!} groupType={groupType} mdDoc={mdDoc} mdKey={key} mdValue={val} changeMdIdKey={this.changeMdIdKey} /> - ); - }); - return metadata; - } - - viewGroupAsTable = (groupType: string): JSX.Element => { - let keys = LinkManager.Instance.getMetadataKeysInGroup(groupType); - let index = keys.indexOf(""); - if (index > -1) keys.splice(index, 1); - let cols = ["anchor1", "anchor2", ...[...keys]].map(c => new SchemaHeaderField(c, "#f1efeb")); - let docs: Doc[] = LinkManager.Instance.getAllMetadataDocsInGroup(groupType); - let createTable = action(() => Docs.Create.SchemaDocument(cols, docs, { width: 500, height: 300, title: groupType + " table" })); - let ref = React.createRef<HTMLDivElement>(); - return <div ref={ref}><button className="linkEditor-button" onPointerDown={SetupDrag(ref, createTable)} title="Drag to view relationship table"><FontAwesomeIcon icon="table" size="sm" /></button></div>; - } - - render() { - let groupType = StrCast(this.props.groupDoc.type); - // if ((groupType && LinkManager.Instance.getMetadataKeysInGroup(groupType).length > 0) || groupType === "") { - let buttons; - if (groupType === "") { - buttons = ( - <> - <button className="linkEditor-button" disabled={true} title="Add KVP"><FontAwesomeIcon icon="plus" size="sm" /></button> - <button className="linkEditor-button" disabled title="Copy group to opposite anchor"><FontAwesomeIcon icon="exchange-alt" size="sm" /></button> - <button className="linkEditor-button" onClick={() => this.removeGroupFromLink(groupType)} title="Remove group from link"><FontAwesomeIcon icon="times" size="sm" /></button> - <button className="linkEditor-button" disabled title="Delete group"><FontAwesomeIcon icon="trash" size="sm" /></button> - <button className="linkEditor-button" disabled title="Drag to view relationship table"><FontAwesomeIcon icon="table" size="sm" /></button> - </> - ); - } else { - buttons = ( - <> - <button className="linkEditor-button" onClick={() => this.addMetadata(groupType)} title="Add KVP"><FontAwesomeIcon icon="plus" size="sm" /></button> - <button className="linkEditor-button" onClick={() => this.copyGroup(groupType)} title="Copy group to opposite anchor"><FontAwesomeIcon icon="exchange-alt" size="sm" /></button> - <button className="linkEditor-button" onClick={() => this.removeGroupFromLink(groupType)} title="Remove group from link"><FontAwesomeIcon icon="times" size="sm" /></button> - <button className="linkEditor-button" onClick={() => this.deleteGroup(groupType)} title="Delete group"><FontAwesomeIcon icon="trash" size="sm" /></button> - {this.viewGroupAsTable(groupType)} - </> - ); - } - return ( - <div className="linkEditor-group"> - <div className="linkEditor-group-row "> - <p className="linkEditor-group-row-label">type:</p> - <GroupTypesDropdown groupType={groupType} setGroupType={this.setGroupType} /> - </div> - {this.renderMetadata().length > 0 ? <p className="linkEditor-group-row-label">metadata:</p> : <></>} - {this.renderMetadata()} - <div className="linkEditor-group-buttons"> - {buttons} - </div> - </div> - ); - } -} - - -interface LinkEditorProps { - sourceDoc: Doc; - linkDoc: Doc; - showLinks: () => void; -} -@observer -export class LinkEditor extends React.Component<LinkEditorProps> { - - @action - deleteLink = (): void => { - LinkManager.Instance.deleteLink(this.props.linkDoc); - this.props.showLinks(); - } - - @action - addGroup = (): void => { - // create new metadata document for group - let mdDoc = new Doc(); - mdDoc.anchor1 = this.props.sourceDoc.title; - let opp = LinkManager.Instance.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc); - if (opp) { - mdDoc.anchor2 = opp.title; - } - - // create new group document - let groupDoc = new Doc(); - groupDoc.type = ""; - groupDoc.metadata = mdDoc; - - LinkManager.Instance.addGroupToAnchor(this.props.linkDoc, this.props.sourceDoc, groupDoc); - } - - render() { - let destination = LinkManager.Instance.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc); - - let groupList = LinkManager.Instance.getAnchorGroups(this.props.linkDoc, this.props.sourceDoc); - let groups = groupList.map(groupDoc => { - return <LinkGroupEditor key={"gred-" + StrCast(groupDoc.type)} linkDoc={this.props.linkDoc} sourceDoc={this.props.sourceDoc} groupDoc={groupDoc} />; - }); - - if (destination) { - return ( - <div className="linkEditor"> - <button className="linkEditor-back" onPointerDown={() => this.props.showLinks()}><FontAwesomeIcon icon="arrow-left" size="sm" /></button> - <div className="linkEditor-info"> - <p className="linkEditor-linkedTo">editing link to: <b>{destination.proto!.title}</b></p> - <button className="linkEditor-button" onPointerDown={() => this.deleteLink()} title="Delete link"><FontAwesomeIcon icon="trash" size="sm" /></button> - </div> - <div className="linkEditor-groupsLabel"> - <b>Relationships:</b> - <button className="linkEditor-button" onClick={() => this.addGroup()} title=" Add Group"><FontAwesomeIcon icon="plus" size="sm" /></button> - </div> - {groups.length > 0 ? groups : <div className="linkEditor-group">There are currently no relationships associated with this link.</div>} - </div> - - ); - } - } -}
\ No newline at end of file diff --git a/src/client/views/nodes/LinkMenu.scss b/src/client/views/nodes/LinkMenu.scss deleted file mode 100644 index a4018bd2d..000000000 --- a/src/client/views/nodes/LinkMenu.scss +++ /dev/null @@ -1,137 +0,0 @@ -@import "../globalCssVariables"; - -.linkMenu { - width: 100%; - height: auto; -} - -.linkMenu-list { - max-height: 200px; - overflow-y: scroll; -} - -.linkMenu-group { - border-bottom: 0.5px solid lightgray; - padding: 5px 0; - - - &:last-child { - border-bottom: none; - } - - .linkMenu-group-name { - display: flex; - - &:hover { - p { - background-color: lightgray; - } - p.expand-one { - width: calc(100% - 26px); - } - .linkEditor-tableButton { - display: block; - } - } - - p { - width: 100%; - padding: 4px 6px; - line-height: 12px; - border-radius: 5px; - font-weight: bold; - } - - .linkEditor-tableButton { - display: none; - } - } -} - -.linkMenu-item { - // border-top: 0.5px solid $main-accent; - position: relative; - display: flex; - font-size: 12px; - - - .link-name { - position: relative; - - p { - padding: 4px 6px; - line-height: 12px; - border-radius: 5px; - overflow-wrap: break-word; - } - } - - .linkMenu-item-content { - width: 100%; - } - - .link-metadata { - padding: 0 10px 0 16px; - margin-bottom: 4px; - color: $main-accent; - font-style: italic; - font-size: 10.5px; - } - - &:hover { - .linkMenu-item-buttons { - display: flex; - } - .linkMenu-item-content { - &.expand-two p { - width: calc(100% - 52px); - background-color: lightgray; - } - &.expand-three p { - width: calc(100% - 84px); - background-color: lightgray; - } - } - } -} - -.linkMenu-item-buttons { - display: none; - position: absolute; - top: 50%; - right: 0; - transform: translateY(-50%); - - .button { - width: 20px; - height: 20px; - margin: 0; - margin-right: 6px; - border-radius: 50%; - cursor: pointer; - pointer-events: auto; - background-color: $dark-color; - color: $light-color; - font-size: 65%; - transition: transform 0.2s; - text-align: center; - position: relative; - - .fa-icon { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - } - - &:last-child { - margin-right: 0; - } - &:hover { - background: $main-accent; - } - } -} - - - diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx deleted file mode 100644 index 1908889e9..000000000 --- a/src/client/views/nodes/LinkMenu.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { action, observable } from "mobx"; -import { observer } from "mobx-react"; -import { DocumentView } from "./DocumentView"; -import { LinkEditor } from "./LinkEditor"; -import './LinkMenu.scss'; -import React = require("react"); -import { Doc } from "../../../new_fields/Doc"; -import { LinkManager } from "../../util/LinkManager"; -import { LinkMenuGroup } from "./LinkMenuGroup"; -import { faTrash } from '@fortawesome/free-solid-svg-icons'; -import { library } from "@fortawesome/fontawesome-svg-core"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - -library.add(faTrash); - -interface Props { - docView: DocumentView; - changeFlyout: () => void; - addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void; -} - -@observer -export class LinkMenu extends React.Component<Props> { - - @observable private _editingLink?: Doc; - - @action - componentWillReceiveProps() { - this._editingLink = undefined; - } - - clearAllLinks = () => { - LinkManager.Instance.deleteAllLinksOnAnchor(this.props.docView.props.Document); - } - - renderAllGroups = (groups: Map<string, Array<Doc>>): Array<JSX.Element> => { - let linkItems: Array<JSX.Element> = []; - groups.forEach((group, groupType) => { - linkItems.push( - <LinkMenuGroup - key={groupType} - sourceDoc={this.props.docView.props.Document} - group={group} - groupType={groupType} - showEditor={action((linkDoc: Doc) => this._editingLink = linkDoc)} - addDocTab={this.props.addDocTab} /> - ); - }); - - // if source doc has no links push message - if (linkItems.length === 0) linkItems.push(<p key="">No links have been created yet. Drag the linking button onto another document to create a link.</p>); - - return linkItems; - } - - render() { - let sourceDoc = this.props.docView.props.Document; - let groups: Map<string, Doc[]> = LinkManager.Instance.getRelatedGroupedLinks(sourceDoc); - if (this._editingLink === undefined) { - return ( - <div className="linkMenu"> - <button className="linkEditor-button linkEditor-clearButton" onClick={() => this.clearAllLinks()} title="Clear all links"><FontAwesomeIcon icon="trash" size="sm" /></button> - {/* <input id="linkMenu-searchBar" type="text" placeholder="Search..."></input> */} - <div className="linkMenu-list"> - {this.renderAllGroups(groups)} - </div> - </div> - ); - } else { - return ( - <LinkEditor sourceDoc={this.props.docView.props.Document} linkDoc={this._editingLink} showLinks={action(() => this._editingLink = undefined)}></LinkEditor> - ); - } - } -}
\ No newline at end of file diff --git a/src/client/views/nodes/LinkMenuGroup.tsx b/src/client/views/nodes/LinkMenuGroup.tsx deleted file mode 100644 index e04044266..000000000 --- a/src/client/views/nodes/LinkMenuGroup.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { action, observable } from "mobx"; -import { observer } from "mobx-react"; -import { DocumentView } from "./DocumentView"; -import { LinkMenuItem } from "./LinkMenuItem"; -import { LinkEditor } from "./LinkEditor"; -import './LinkMenu.scss'; -import React = require("react"); -import { Doc, DocListCast } from "../../../new_fields/Doc"; -import { Id } from "../../../new_fields/FieldSymbols"; -import { LinkManager } from "../../util/LinkManager"; -import { DragLinksAsDocuments, DragManager, SetupDrag } from "../../util/DragManager"; -import { emptyFunction } from "../../../Utils"; -import { Docs } from "../../documents/Documents"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { UndoManager } from "../../util/UndoManager"; -import { StrCast } from "../../../new_fields/Types"; -import { SchemaHeaderField, RandomPastel } from "../../../new_fields/SchemaHeaderField"; - -interface LinkMenuGroupProps { - sourceDoc: Doc; - group: Doc[]; - groupType: string; - showEditor: (linkDoc: Doc) => void; - addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void; -} - -@observer -export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> { - - private _drag = React.createRef<HTMLDivElement>(); - private _table = React.createRef<HTMLDivElement>(); - - onLinkButtonDown = (e: React.PointerEvent): void => { - e.stopPropagation(); - document.removeEventListener("pointermove", this.onLinkButtonMoved); - document.addEventListener("pointermove", this.onLinkButtonMoved); - document.removeEventListener("pointerup", this.onLinkButtonUp); - document.addEventListener("pointerup", this.onLinkButtonUp); - } - - onLinkButtonUp = (e: PointerEvent): void => { - document.removeEventListener("pointermove", this.onLinkButtonMoved); - document.removeEventListener("pointerup", this.onLinkButtonUp); - e.stopPropagation(); - } - - - onLinkButtonMoved = async (e: PointerEvent) => { - UndoManager.RunInBatch(() => { - if (this._drag.current !== null && (e.movementX > 1 || e.movementY > 1)) { - document.removeEventListener("pointermove", this.onLinkButtonMoved); - document.removeEventListener("pointerup", this.onLinkButtonUp); - - let draggedDocs = this.props.group.map(linkDoc => { - let opp = LinkManager.Instance.getOppositeAnchor(linkDoc, this.props.sourceDoc); - if (opp) return opp; - }) as Doc[]; - let dragData = new DragManager.DocumentDragData(draggedDocs, draggedDocs.map(d => undefined)); - - DragManager.StartLinkedDocumentDrag([this._drag.current], dragData, e.x, e.y, { - handlers: { - dragComplete: action(emptyFunction), - }, - hideSource: false - }); - } - }, "drag links"); - e.stopPropagation(); - } - - viewGroupAsTable = (groupType: string): JSX.Element => { - let keys = LinkManager.Instance.getMetadataKeysInGroup(groupType); - let index = keys.indexOf(""); - if (index > -1) keys.splice(index, 1); - let cols = ["anchor1", "anchor2", ...[...keys]].map(c => new SchemaHeaderField(c, "#f1efeb")); - let docs: Doc[] = LinkManager.Instance.getAllMetadataDocsInGroup(groupType); - let createTable = action(() => Docs.Create.SchemaDocument(cols, docs, { width: 500, height: 300, title: groupType + " table" })); - let ref = React.createRef<HTMLDivElement>(); - return <div ref={ref}><button className="linkEditor-button linkEditor-tableButton" onPointerDown={SetupDrag(ref, createTable)} title="Drag to view relationship table"><FontAwesomeIcon icon="table" size="sm" /></button></div>; - } - - render() { - let groupItems = this.props.group.map(linkDoc => { - let destination = LinkManager.Instance.getOppositeAnchor(linkDoc, this.props.sourceDoc); - if (destination && this.props.sourceDoc) { - return <LinkMenuItem key={destination[Id] + this.props.sourceDoc[Id]} groupType={this.props.groupType} - addDocTab={this.props.addDocTab} - linkDoc={linkDoc} sourceDoc={this.props.sourceDoc} destinationDoc={destination} showEditor={this.props.showEditor} />; - } - }); - - return ( - <div className="linkMenu-group"> - <div className="linkMenu-group-name"> - <p ref={this._drag} onPointerDown={this.onLinkButtonDown} - className={this.props.groupType === "*" || this.props.groupType === "" ? "" : "expand-one"} > {this.props.groupType}:</p> - {this.props.groupType === "*" || this.props.groupType === "" ? <></> : this.viewGroupAsTable(this.props.groupType)} - </div> - <div className="linkMenu-group-wrapper"> - {groupItems} - </div> - </div > - ); - } -}
\ No newline at end of file diff --git a/src/client/views/nodes/LinkMenuItem.tsx b/src/client/views/nodes/LinkMenuItem.tsx deleted file mode 100644 index 90b335933..000000000 --- a/src/client/views/nodes/LinkMenuItem.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faEdit, faEye, faTimes, faArrowRight, faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { observer } from "mobx-react"; -import { DocumentManager } from "../../util/DocumentManager"; -import { undoBatch } from "../../util/UndoManager"; -import './LinkMenu.scss'; -import React = require("react"); -import { Doc, DocListCastAsync } from '../../../new_fields/Doc'; -import { StrCast, Cast, FieldValue, NumCast } from '../../../new_fields/Types'; -import { observable, action } from 'mobx'; -import { LinkManager } from '../../util/LinkManager'; -import { DragLinkAsDocument } from '../../util/DragManager'; -import { CollectionDockingView } from '../collections/CollectionDockingView'; -import { SelectionManager } from '../../util/SelectionManager'; -library.add(faEye, faEdit, faTimes, faArrowRight, faChevronDown, faChevronUp); - - -interface LinkMenuItemProps { - groupType: string; - linkDoc: Doc; - sourceDoc: Doc; - destinationDoc: Doc; - showEditor: (linkDoc: Doc) => void; - addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void; -} - -@observer -export class LinkMenuItem extends React.Component<LinkMenuItemProps> { - private _drag = React.createRef<HTMLDivElement>(); - @observable private _showMore: boolean = false; - @action toggleShowMore() { this._showMore = !this._showMore; } - - @undoBatch - onFollowLink = async (e: React.PointerEvent): Promise<void> => { - e.stopPropagation(); - e.persist(); - let jumpToDoc = this.props.destinationDoc; - let pdfDoc = FieldValue(Cast(this.props.destinationDoc, Doc)); - if (pdfDoc) { - jumpToDoc = pdfDoc; - } - let proto = Doc.GetProto(this.props.linkDoc); - let targetContext = await Cast(proto.targetContext, Doc); - let sourceContext = await Cast(proto.sourceContext, Doc); - let self = this; - - - let dockingFunc = (document: Doc) => { this.props.addDocTab(document, undefined, "inTab"); SelectionManager.DeselectAll(); }; - if (e.ctrlKey) { - dockingFunc = (document: Doc) => CollectionDockingView.Instance.AddRightSplit(document, undefined); - } - - if (this.props.destinationDoc === self.props.linkDoc.anchor2 && targetContext) { - DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, false, async document => dockingFunc(document), undefined, targetContext); - } - else if (this.props.destinationDoc === self.props.linkDoc.anchor1 && sourceContext) { - DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, false, document => dockingFunc(sourceContext!)); - } - else if (DocumentManager.Instance.getDocumentView(jumpToDoc)) { - DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, undefined, undefined, NumCast((this.props.destinationDoc === self.props.linkDoc.anchor2 ? self.props.linkDoc.anchor2Page : self.props.linkDoc.anchor1Page))); - } - else { - DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, false, dockingFunc); - } - } - - onEdit = (e: React.PointerEvent): void => { - e.stopPropagation(); - this.props.showEditor(this.props.linkDoc); - } - - renderMetadata = (): JSX.Element => { - let groups = LinkManager.Instance.getAnchorGroups(this.props.linkDoc, this.props.sourceDoc); - let index = groups.findIndex(groupDoc => StrCast(groupDoc.type).toUpperCase() === this.props.groupType.toUpperCase()); - let groupDoc = index > -1 ? groups[index] : undefined; - - let mdRows: Array<JSX.Element> = []; - if (groupDoc) { - let mdDoc = Cast(groupDoc.metadata, Doc, null); - if (mdDoc) { - let keys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);//groupMetadataKeys.get(this.props.groupType); - mdRows = keys.map(key => { - return (<div key={key} className="link-metadata-row"><b>{key}</b>: {StrCast(mdDoc[key])}</div>); - }); - } - } - - return (<div className="link-metadata">{mdRows}</div>); - } - - onLinkButtonDown = (e: React.PointerEvent): void => { - e.stopPropagation(); - document.removeEventListener("pointermove", this.onLinkButtonMoved); - document.addEventListener("pointermove", this.onLinkButtonMoved); - document.removeEventListener("pointerup", this.onLinkButtonUp); - document.addEventListener("pointerup", this.onLinkButtonUp); - } - - onLinkButtonUp = (e: PointerEvent): void => { - document.removeEventListener("pointermove", this.onLinkButtonMoved); - document.removeEventListener("pointerup", this.onLinkButtonUp); - e.stopPropagation(); - } - - onLinkButtonMoved = async (e: PointerEvent) => { - if (this._drag.current !== null && (e.movementX > 1 || e.movementY > 1)) { - document.removeEventListener("pointermove", this.onLinkButtonMoved); - document.removeEventListener("pointerup", this.onLinkButtonUp); - - DragLinkAsDocument(this._drag.current, e.x, e.y, this.props.linkDoc, this.props.sourceDoc); - } - e.stopPropagation(); - } - - render() { - - let keys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);//groupMetadataKeys.get(this.props.groupType); - let canExpand = keys ? keys.length > 0 : false; - - return ( - <div className="linkMenu-item"> - <div className={canExpand ? "linkMenu-item-content expand-three" : "linkMenu-item-content expand-two"}> - <div className="link-name"> - <p ref={this._drag} onPointerDown={this.onLinkButtonDown}>{StrCast(this.props.destinationDoc.title)}</p> - <div className="linkMenu-item-buttons"> - {canExpand ? <div title="Show more" className="button" onPointerDown={() => this.toggleShowMore()}> - <FontAwesomeIcon className="fa-icon" icon={this._showMore ? "chevron-up" : "chevron-down"} size="sm" /></div> : <></>} - <div title="Edit link" className="button" onPointerDown={this.onEdit}><FontAwesomeIcon className="fa-icon" icon="edit" size="sm" /></div> - <div title="Follow link" className="button" onPointerDown={this.onFollowLink}><FontAwesomeIcon className="fa-icon" icon="arrow-right" size="sm" /></div> - </div> - </div> - {this._showMore ? this.renderMetadata() : <></>} - </div> - - </div > - ); - } -}
\ No newline at end of file diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index f0140d04b..29eef27a0 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -1,24 +1,29 @@ -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, observable } from "mobx"; import { observer } from "mobx-react"; -import { FieldResult, Doc } from "../../../new_fields/Doc"; +import { FieldResult, Doc, Field } from "../../../new_fields/Doc"; import { HtmlField } from "../../../new_fields/HtmlField"; -import { InkTool } from "../../../new_fields/InkField"; -import { Cast, NumCast } from "../../../new_fields/Types"; import { WebField } from "../../../new_fields/URLField"; -import { Utils } from "../../../Utils"; import { DocumentDecorations } from "../DocumentDecorations"; import { InkingControl } from "../InkingControl"; import { FieldView, FieldViewProps } from './FieldView'; -import { KeyValueBox } from "./KeyValueBox"; import "./WebBox.scss"; import React = require("react"); +import { InkTool } from "../../../new_fields/InkField"; +import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types"; +import { Utils } from "../../../Utils"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faStickyNote } from '@fortawesome/free-solid-svg-icons'; +import { observable, action, computed } from "mobx"; +import { listSpec } from "../../../new_fields/Schema"; +import { RefField } from "../../../new_fields/RefField"; +import { ObjectField } from "../../../new_fields/ObjectField"; +import { updateSourceFile } from "typescript"; +import { KeyValueBox } from "./KeyValueBox"; +import { setReactionScheduler } from "mobx/lib/internal"; +import { library } from "@fortawesome/fontawesome-svg-core"; import { SelectionManager } from "../../util/SelectionManager"; import { Docs } from "../../documents/Documents"; -import { faStickyNote } from "@fortawesome/free-solid-svg-icons"; -import { library } from "@fortawesome/fontawesome-svg-core"; -library.add(faStickyNote) +library.add(faStickyNote); @observer export class WebBox extends React.Component<FieldViewProps> { |