diff options
Diffstat (limited to 'src/client/views/nodes/FormattedTextBox.tsx')
| -rw-r--r-- | src/client/views/nodes/FormattedTextBox.tsx | 233 | 
1 files changed, 144 insertions, 89 deletions
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 5c635cc0c..376b5a574 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -1,58 +1,50 @@  import { library } from '@fortawesome/fontawesome-svg-core';  import { faEdit, faSmile } from '@fortawesome/free-solid-svg-icons'; -import { action, IReactionDisposer, observable, reaction } from "mobx"; +import { action, IReactionDisposer, observable, reaction, runInAction } from "mobx";  import { observer } from "mobx-react";  import { baseKeymap } from "prosemirror-commands";  import { history } from "prosemirror-history";  import { keymap } from "prosemirror-keymap"; +import { NodeType } from 'prosemirror-model';  import { EditorState, Plugin, Transaction } from "prosemirror-state";  import { EditorView } from "prosemirror-view"; -import { Doc, Field, Opt, WidthSym, HeightSym } from "../../../new_fields/Doc"; +import { Doc, Opt } from "../../../new_fields/Doc"; +import { Id } from '../../../new_fields/FieldSymbols'; +import { List } from '../../../new_fields/List';  import { RichTextField } from "../../../new_fields/RichTextField"; -import { createSchema, makeInterface } from "../../../new_fields/Schema"; -import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; +import { createSchema, listSpec, makeInterface } from "../../../new_fields/Schema"; +import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types";  import { DocServer } from "../../DocServer"; -import { DocumentManager } from "../../util/DocumentManager"; +import { Docs } from '../../documents/Documents'; +import { DocumentManager } from '../../util/DocumentManager';  import { DragManager } from "../../util/DragManager"; -import buildKeymap from "../../util/ProsemirrorKeymap"; +import buildKeymap from "../../util/ProsemirrorExampleTransfer";  import { inpRules } from "../../util/RichTextRules"; -import { ImageResizeView, schema } from "../../util/RichTextSchema"; +import { ImageResizeView, schema, SummarizedView } from "../../util/RichTextSchema";  import { SelectionManager } from "../../util/SelectionManager";  import { TooltipLinkingMenu } from "../../util/TooltipLinkingMenu";  import { TooltipTextMenu } from "../../util/TooltipTextMenu";  import { undoBatch, UndoManager } from "../../util/UndoManager";  import { ContextMenu } from "../../views/ContextMenu"; -import { CollectionDockingView } from "../collections/CollectionDockingView"; +import { ContextMenuProps } from '../ContextMenuItem';  import { DocComponent } from "../DocComponent";  import { InkingControl } from "../InkingControl"; +import { Templates } from '../Templates';  import { FieldView, FieldViewProps } from "./FieldView";  import "./FormattedTextBox.scss";  import React = require("react"); -import { DocUtils } from '../../documents/Documents';  library.add(faEdit);  library.add(faSmile);  // FormattedTextBox: Displays an editable plain text node that maps to a specified Key of a Document  // -//  HTML Markup:  <FormattedTextBox Doc={Document's ID} FieldKey={Key's name} -// -//  In Code, the node's HTML is specified in the document's parameterized structure as: -//        document.SetField(KeyStore.Layout,  "<FormattedTextBox doc={doc} fieldKey={<KEYNAME>Key} />"); -//  and the node's binding to the specified document KEYNAME as: -//        document.SetField(KeyStore.LayoutKeys, new ListField([KeyStore.<KEYNAME>])); -//  The Jsx parser at run time will bind: -//        'fieldKey' property to the Key stored in LayoutKeys -//    and 'doc' property to the document that is being rendered -// -//  When rendered() by React, this extracts the TextController from the Document stored at the -//  specified Key and assigns it to an HTML input node.  When changes are made to this node, -//  this will edit the document and assign the new value to that field. -//]  export interface FormattedTextBoxProps {      isOverlay?: boolean;      hideOnLeave?: boolean; +    height?: string; +    color?: string;  }  const richTextSchema = createSchema({ @@ -68,34 +60,53 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe          return FieldView.LayoutString(FormattedTextBox, fieldStr);      }      private _ref: React.RefObject<HTMLDivElement>; -    private _proseRef: React.RefObject<HTMLDivElement>; +    private _proseRef?: HTMLDivElement;      private _editorView: Opt<EditorView>; -    private _gotDown: boolean = false; -    private _dropDisposer?: DragManager.DragDropDisposer; +    private _toolTipTextMenu: TooltipTextMenu | undefined = undefined; +    private _applyingChange: boolean = false; +    private _linkClicked = "";      private _reactionDisposer: Opt<IReactionDisposer>; -    private _inputReactionDisposer: Opt<IReactionDisposer>;      private _proxyReactionDisposer: Opt<IReactionDisposer>; +    private dropDisposer?: DragManager.DragDropDisposer;      public get CurrentDiv(): HTMLDivElement { return this._ref.current!; } +    @observable _entered = false;      @observable public static InputBoxOverlay?: FormattedTextBox = undefined;      public static InputBoxOverlayScroll: number = 0; +    public static IsFragment(html: string) { +        return html.indexOf("data-pm-slice") !== -1; +    } +    public static GetHref(html: string): string { +        let parser = new DOMParser(); +        let parsedHtml = parser.parseFromString(html, 'text/html'); +        if (parsedHtml.body.childNodes.length === 1 && parsedHtml.body.childNodes[0].childNodes.length === 1 && +            (parsedHtml.body.childNodes[0].childNodes[0] as any).href) { +            return (parsedHtml.body.childNodes[0].childNodes[0] as any).href; +        } +        return ""; +    } +    public static GetDocFromUrl(url: string) { +        if (url.startsWith(document.location.origin)) { +            let start = url.indexOf(window.location.origin); +            let path = url.substr(start, url.length - start); +            let docid = path.replace(DocServer.prepend("/doc/"), "").split("?")[0]; +            return docid; +        } +        return ""; +    }      constructor(props: FieldViewProps) {          super(props);          this._ref = React.createRef(); -        this._proseRef = React.createRef();          if (this.props.isOverlay) {              DragManager.StartDragFunctions.push(() => FormattedTextBox.InputBoxOverlay = undefined);          }      } -    _applyingChange: boolean = false; - -    _lastState: any = undefined;      dispatchTransaction = (tx: Transaction) => {          if (this._editorView) { -            const state = this._lastState = this._editorView.state.apply(tx); +            const state = this._editorView.state.apply(tx);              this._editorView.updateState(state);              this._applyingChange = true;              Doc.SetOnPrototype(this.props.Document, this.props.fieldKey, new RichTextField(JSON.stringify(state.toJSON()))); @@ -111,25 +122,52 @@ 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) } }); +        } +    } +      @undoBatch      @action      drop = async (e: Event, de: DragManager.DropEvent) => { -        if (de.data instanceof DragManager.LinkDragData) { -            let sourceDoc = de.data.linkSourceDocument; -            let destDoc = this.props.Document; - -            DocUtils.MakeLink(sourceDoc, destDoc); -            de.data.droppedDocuments.push(destDoc); +        // We're dealing with a link to a document +        if (de.data instanceof DragManager.EmbedDragData && de.data.urlField) { +            // We're dealing with an internal document drop +            let url = de.data.urlField.url.href; +            let model: NodeType = (url.includes(".mov") || url.includes(".mp4")) ? schema.nodes.video : schema.nodes.image; +            this._editorView!.dispatch(this._editorView!.state.tr.insert(0, model.create({ src: url })));              e.stopPropagation(); +        } else { +            if (de.data instanceof DragManager.DocumentDragData) { +                let ldocs = Cast(this.props.Document.subBulletDocs, listSpec(Doc)); +                if (!ldocs) { +                    this.props.Document.subBulletDocs = new List<Doc>([]); +                } +                ldocs = Cast(this.props.Document.subBulletDocs, listSpec(Doc)); +                if (!ldocs) return; +                if (!ldocs || !ldocs[0] || ldocs[0] instanceof Promise || StrCast((ldocs[0] as Doc).layout).indexOf("CollectionView") === -1) { +                    ldocs.splice(0, 0, Docs.StackingDocument([], { title: StrCast(this.props.Document.title) + "-subBullets", x: NumCast(this.props.Document.x), y: NumCast(this.props.Document.y) + NumCast(this.props.Document.height), width: 300, height: 300 })); +                    this.props.addDocument && this.props.addDocument(ldocs[0] as Doc); +                    this.props.Document.templates = new List<string>([Templates.Bullet.Layout]); +                    this.props.Document.isBullet = true; +                } +                let stackDoc = (ldocs[0] as Doc); +                if (de.data.moveDocument) { +                    de.data.moveDocument(de.data.draggedDocuments[0], stackDoc, (doc) => { +                        Cast(stackDoc.data, listSpec(Doc))!.push(doc); +                        return true; +                    }); +                } +            }          }      }      componentDidMount() { -        if (this._ref.current) { -            this._dropDisposer = DragManager.MakeDropTarget(this._ref.current, { -                handlers: { drop: this.drop.bind(this) } -            }); -        }          const config = {              schema,              inpRules, //these currently don't do anything, but could eventually be helpful @@ -151,57 +189,52 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe                  ]          }; -        if (this.props.isOverlay) { -            this._inputReactionDisposer = reaction(() => FormattedTextBox.InputBoxOverlay, -                () => { -                    if (this._editorView) { -                        this._editorView.destroy(); -                    } -                    this.setupEditor(config, this.props.Document);// MainOverlayTextBox.Instance.TextDoc); // bcz: not sure why, but the order of events is such that this.props.Document hasn't updated yet, so without forcing the editor to the MainOverlayTextBox, it will display the previously focused textbox -                } -            ); -        } else { +        if (!this.props.isOverlay) {              this._proxyReactionDisposer = reaction(() => this.props.isSelected(),                  () => {                      if (this.props.isSelected()) {                          FormattedTextBox.InputBoxOverlay = this;                          FormattedTextBox.InputBoxOverlayScroll = this._ref.current!.scrollTop;                      } -                }); +                }, { fireImmediately: true });          } -          this._reactionDisposer = reaction(              () => {                  const field = this.props.Document ? Cast(this.props.Document[this.props.fieldKey], RichTextField) : undefined; -                return field ? field.Data : undefined; +                return field ? field.Data : `{"doc":{"type":"doc","content":[]},"selection":{"type":"text","anchor":0,"head":0}}`;              }, -            field => field && this._editorView && !this._applyingChange && +            field => this._editorView && !this._applyingChange &&                  this._editorView.updateState(EditorState.fromJSON(config, JSON.parse(field)))          ); -        this.setupEditor(config, this.props.Document); +        this.setupEditor(config, this.props.Document, this.props.fieldKey);      } -    private setupEditor(config: any, doc?: Doc) { -        let field = doc ? Cast(doc[this.props.fieldKey], RichTextField) : undefined; -        if (this._proseRef.current) { -            this._editorView = new EditorView(this._proseRef.current, { +    private setupEditor(config: any, doc: Doc, fieldKey: string) { +        let field = doc ? Cast(doc[fieldKey], RichTextField) : undefined; +        let startup = StrCast(doc.documentText); +        startup = startup.startsWith("@@@") ? startup.replace("@@@", "") : ""; +        if (!startup && !field && doc) { +            startup = StrCast(doc[fieldKey]); +        } +        if (this._proseRef) { +            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); } +                    image(node, view, getPos) { return new ImageResizeView(node, view, getPos); }, +                    star(node, view, getPos) { return new SummarizedView(node, view, getPos); }                  }              }); -            let text = StrCast(this.props.Document.documentText); -            if (text.startsWith("@@@")) { -                this.props.Document.proto!.documentText = undefined; -                this._editorView.dispatch(this._editorView.state.tr.insertText(text.replace("@@@", ""))); +            if (startup) { +                Doc.GetProto(doc).documentText = undefined; +                this._editorView.dispatch(this._editorView.state.tr.insertText(startup));              }          }          if (this.props.selectOnLoad) { -            this.props.select(false); -            this._editorView!.focus(); +            if (!this.props.isOverlay) this.props.select(false); +            else this._editorView!.focus();          }      } @@ -212,15 +245,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe          if (this._reactionDisposer) {              this._reactionDisposer();          } -        if (this._inputReactionDisposer) { -            this._inputReactionDisposer(); -        }          if (this._proxyReactionDisposer) {              this._proxyReactionDisposer();          } -        if (this._dropDisposer) { -            this._dropDisposer(); -        }      }      onPointerDown = (e: React.PointerEvent): void => { @@ -238,10 +265,18 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe              }              if (href) {                  if (href.indexOf(DocServer.prepend("/doc/")) === 0) { -                    let docid = href.replace(DocServer.prepend("/doc/"), "").split("?")[0]; -                    DocServer.GetRefField(docid).then(f => { -                        (f instanceof Doc) && DocumentManager.Instance.jumpToDocument(f, ctrlKey, document => this.props.addDocTab(document, "inTab")) -                    }); +                    this._linkClicked = href.replace(DocServer.prepend("/doc/"), "").split("?")[0]; +                    if (this._linkClicked) { +                        DocServer.GetRefField(this._linkClicked).then(f => { +                            (f instanceof Doc) && DocumentManager.Instance.jumpToDocument(f, ctrlKey, document => this.props.addDocTab(document, "inTab")); +                        }); +                        e.stopPropagation(); +                        e.preventDefault(); +                    } +                } else { +                    let webDoc = Docs.WebDocument(href, { x: NumCast(this.props.Document.x, 0) + NumCast(this.props.Document.width, 0), y: NumCast(this.props.Document.y) }); +                    this.props.addDocument && this.props.addDocument(webDoc); +                    this._linkClicked = webDoc[Id];                  }                  e.stopPropagation();                  e.preventDefault(); @@ -249,7 +284,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe          }          if (e.button === 2 || (e.button === 0 && e.ctrlKey)) { -            this._gotDown = true;              e.preventDefault();          }      } @@ -279,7 +313,12 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe      }      onClick = (e: React.MouseEvent): void => { -        this._proseRef.current!.focus(); +        this._proseRef!.focus(); +        if (this._linkClicked) { +            this._linkClicked = ""; +            e.preventDefault(); +            e.stopPropagation(); +        }      }      onMouseDown = (e: React.MouseEvent): void => {          if (!this.props.isSelected()) { // preventing default allows the onClick to be generated instead of being swallowed by the text box itself @@ -297,7 +336,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe          });      } -    _toolTipTextMenu: TooltipTextMenu | undefined = undefined;      tooltipLinkingMenuPlugin() {          let myprops = this.props;          return new Plugin({ @@ -331,10 +369,17 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe          if (!this._undoTyping) {              this._undoTyping = UndoManager.StartBatch("undoTyping");          } +        if (this.props.isOverlay && this.props.Document.autoHeight) { +            let xf = this._ref.current!.getBoundingClientRect(); +            let scrBounds = this.props.ScreenToLocalTransform().transformBounds(0, 0, xf.width, xf.height); +            let nh = NumCast(this.props.Document.nativeHeight, 0); +            let dh = NumCast(this.props.Document.height, 0); +            let sh = scrBounds.height; +            this.props.Document.height = nh ? dh / nh * sh : sh; +            this.props.Document.proto!.nativeHeight = nh ? sh : undefined; +        }      } -    @observable -    _entered = false;      @action      onPointerEnter = (e: React.PointerEvent) => {          this._entered = true; @@ -343,6 +388,15 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe      onPointerLeave = (e: React.PointerEvent) => {          this._entered = false;      } + +    specificContextMenu = (e: React.MouseEvent): void => { +        let subitems: ContextMenuProps[] = []; +        subitems.push({ +            description: BoolCast(this.props.Document.autoHeight, false) ? "Manual Height" : "Auto Height", +            event: action(() => Doc.GetProto(this.props.Document).autoHeight = !BoolCast(this.props.Document.autoHeight, false)), icon: "expand-arrows-alt" +        }); +        ContextMenu.Instance.addItem({ description: "Text Funcs...", subitems: subitems }); +    }      render() {          let style = this.props.isOverlay ? "scroll" : "hidden";          let rounded = NumCast(this.props.Document.borderRounding) < 0 ? "-rounded" : ""; @@ -350,26 +404,27 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe          return (              <div className={`formattedTextBox-cont-${style}`} ref={this._ref}                  style={{ +                    height: this.props.height ? this.props.height : undefined,                      background: this.props.hideOnLeave ? "rgba(0,0,0,0.4)" : undefined,                      opacity: this.props.hideOnLeave ? (this._entered || this.props.isSelected() || this.props.Document.libraryBrush ? 1 : 0.1) : 1, -                    color: this.props.hideOnLeave ? "white" : "initial", +                    color: this.props.color ? this.props.color : this.props.hideOnLeave ? "white" : "initial",                      pointerEvents: interactive ? "all" : "none", +                    fontSize: "13px"                  }} -                // onKeyDown={this.onKeyPress} -                onKeyPress={this.onKeyPress} +                onKeyDown={this.onKeyPress}                  onFocus={this.onFocused}                  onClick={this.onClick} +                onContextMenu={this.specificContextMenu}                  onBlur={this.onBlur}                  onPointerUp={this.onPointerUp}                  onPointerDown={this.onPointerDown}                  onMouseDown={this.onMouseDown} -                onContextMenu={this.specificContextMenu}                  // tfs: do we need this event handler                  onWheel={this.onPointerWheel}                  onPointerEnter={this.onPointerEnter}                  onPointerLeave={this.onPointerLeave}              > -                <div className={`formattedTextBox-inner${rounded}`} style={{ whiteSpace: "pre-wrap", pointerEvents: this.props.Document.isButton && !this.props.isSelected() ? "none" : "all" }} ref={this._proseRef} /> +                <div className={`formattedTextBox-inner${rounded}`} ref={this.createDropTarget} style={{ whiteSpace: "pre-wrap", pointerEvents: this.props.Document.isButton && !this.props.isSelected() ? "none" : "all" }} />              </div>          );      }  | 
