diff options
Diffstat (limited to 'src/client/views/nodes')
| -rw-r--r-- | src/client/views/nodes/Annotation.tsx | 117 | ||||
| -rw-r--r-- | src/client/views/nodes/DocumentView.scss | 1 | ||||
| -rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 72 | ||||
| -rw-r--r-- | src/client/views/nodes/FormattedTextBox.scss | 29 | ||||
| -rw-r--r-- | src/client/views/nodes/FormattedTextBox.tsx | 472 | ||||
| -rw-r--r-- | src/client/views/nodes/FormattedTextBoxComment.tsx | 10 | ||||
| -rw-r--r-- | src/client/views/nodes/IconBox.tsx | 6 | ||||
| -rw-r--r-- | src/client/views/nodes/ImageBox.tsx | 2 | ||||
| -rw-r--r-- | src/client/views/nodes/PDFBox.scss | 4 | ||||
| -rw-r--r-- | src/client/views/nodes/PDFBox.tsx | 33 | ||||
| -rw-r--r-- | src/client/views/nodes/PresBox.tsx | 33 | ||||
| -rw-r--r-- | src/client/views/nodes/VideoBox.tsx | 57 |
12 files changed, 344 insertions, 492 deletions
diff --git a/src/client/views/nodes/Annotation.tsx b/src/client/views/nodes/Annotation.tsx deleted file mode 100644 index 3e4ed6bf1..000000000 --- a/src/client/views/nodes/Annotation.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import "./ImageBox.scss"; -import React = require("react"); -import { observer } from "mobx-react"; -import { observable, action } from 'mobx'; -import 'react-pdf/dist/Page/AnnotationLayer.css'; - -interface IProps { - Span: HTMLSpanElement; - X: number; - Y: number; - Highlights: any[]; - Annotations: any[]; - CurrAnno: any[]; - -} - -/** - * Annotation class is used to take notes on a particular highlight. You can also change highlighted span's color - * Improvements to be made: Removing the annotation when onRemove is called. (Removing this, not just the highlighted span). - * Also need to support multiline highlighting - * - * Written by: Andrew Kim - */ -@observer -export class Annotation extends React.Component<IProps> { - - /** - * changes color of the span (highlighted section) - */ - onColorChange = (e: React.PointerEvent) => { - if (e.currentTarget.innerHTML === "r") { - this.props.Span.style.backgroundColor = "rgba(255,0,0, 0.3)"; - } else if (e.currentTarget.innerHTML === "b") { - this.props.Span.style.backgroundColor = "rgba(0,255, 255, 0.3)"; - } else if (e.currentTarget.innerHTML === "y") { - this.props.Span.style.backgroundColor = "rgba(255,255,0, 0.3)"; - } else if (e.currentTarget.innerHTML === "g") { - this.props.Span.style.backgroundColor = "rgba(76, 175, 80, 0.3)"; - } - - } - - /** - * removes the highlighted span. Supposed to remove Annotation too, but I don't know how to unmount this - */ - @action - onRemove = (e: any) => { - let index: number = -1; - //finding the highlight in the highlight array - this.props.Highlights.forEach((e) => { - for (const span of e.spans) { - if (span === this.props.Span) { - index = this.props.Highlights.indexOf(e); - this.props.Highlights.splice(index, 1); - } - } - }); - - //removing from CurrAnno and Annotation array - this.props.Annotations.splice(index, 1); - this.props.CurrAnno.pop(); - - //removing span from div - if (this.props.Span.parentElement) { - let nodesArray = this.props.Span.parentElement.childNodes; - nodesArray.forEach((e) => { - if (e === this.props.Span) { - if (this.props.Span.parentElement) { - this.props.Highlights.forEach((item) => { - if (item === e) { - item.remove(); - } - }); - e.remove(); - } - } - }); - } - - - } - - render() { - return ( - <div - style={{ - position: "absolute", - top: "20px", - left: "0px", - zIndex: 1, - transform: `translate(${this.props.X}px, ${this.props.Y}px)`, - - }}> - <div style={{ width: "200px", height: "50px", backgroundColor: "orange" }}> - <button - style={{ borderRadius: "25px", width: "25%", height: "100%" }} - onClick={this.onRemove} - >x</button> - <div style={{ width: "75%", height: "100%", display: "inline-block" }}> - <button onPointerDown={this.onColorChange} style={{ backgroundColor: "red", borderRadius: "50%", color: "transparent" }}>r</button> - <button onPointerDown={this.onColorChange} style={{ backgroundColor: "blue", borderRadius: "50%", color: "transparent" }}>b</button> - <button onPointerDown={this.onColorChange} style={{ backgroundColor: "yellow", borderRadius: "50%", color: "transparent" }}>y</button> - <button onPointerDown={this.onColorChange} style={{ backgroundColor: "green", borderRadius: "50%", color: "transparent" }}>g</button> - </div> - - </div> - <div style={{ width: "200px", height: "200" }}> - <textarea style={{ width: "100%", height: "100%" }} - defaultValue="Enter Text Here..." - - ></textarea> - </div> - </div> - - ); - } -}
\ No newline at end of file diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index 4ea200e6d..b3e7898c1 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -5,6 +5,7 @@ top: 0; left:0; border-radius: inherit; + transition : outline .3s linear; // background: $light-color; //overflow: hidden; transform-origin: left top; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index ded50890d..a670d280d 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -111,6 +111,7 @@ export const documentSchema = createSchema({ type: "string", // enumerated type of document maximizeLocation: "string", // flag for where to place content when following a click interaction (e.g., onRight, inPlace, inTab) lockedPosition: "boolean", // whether the document can be spatially manipulated + inOverlay: "boolean", // whether the document is rendered in an OverlayView which handles selection/dragging differently borderRounding: "string", // border radius rounding of document searchFields: "string", // the search fields to display when this document matches a search in its metadata heading: "number", // the logical layout 'heading' of this document (used by rule provider to stylize h1 header elements, from h2, etc) @@ -206,7 +207,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu buttonClick = async (altKey: boolean, ctrlKey: boolean) => { let maximizedDocs = await DocListCastAsync(this.props.Document.maximizedDocs); let summarizedDocs = await DocListCastAsync(this.props.Document.summarizedDocs); - let linkedDocs = LinkManager.Instance.getAllRelatedLinks(this.props.Document); + let linkDocs = LinkManager.Instance.getAllRelatedLinks(this.props.Document); let expandedDocs: Doc[] = []; expandedDocs = maximizedDocs ? [...maximizedDocs, ...expandedDocs] : expandedDocs; expandedDocs = summarizedDocs ? [...summarizedDocs, ...expandedDocs] : expandedDocs; @@ -223,33 +224,16 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu expandedDocs.forEach(maxDoc => (!this.props.addDocTab(maxDoc, undefined, "close") && this.props.addDocTab(maxDoc, undefined, maxLocation))); } } - else if (linkedDocs.length) { - SelectionManager.DeselectAll(); - let first = linkedDocs.filter(d => Doc.AreProtosEqual(d.anchor1 as Doc, this.props.Document) && !d.anchor1anchored); - let second = linkedDocs.filter(d => Doc.AreProtosEqual(d.anchor2 as Doc, this.props.Document) && !d.anchor2anchored); - let firstUnshown = first.filter(d => DocumentManager.Instance.getDocumentViews(d.anchor2 as Doc).length === 0); - let secondUnshown = second.filter(d => DocumentManager.Instance.getDocumentViews(d.anchor1 as Doc).length === 0); - if (firstUnshown.length) first = [firstUnshown[0]]; - if (secondUnshown.length) second = [secondUnshown[0]]; - let linkedFwdDocs = first.length ? [first[0].anchor2 as Doc, first[0].anchor1 as Doc] : second.length ? [second[0].anchor1 as Doc, second[0].anchor1 as Doc] : undefined; - - // @TODO: shouldn't always follow target context - let linkedFwdContextDocs = [first.length ? await (first[0].targetContext) as Doc : undefined, undefined]; - let linkedFwdPage = [first.length ? NumCast(first[0].anchor2Page, undefined) : undefined, undefined]; - - if (linkedFwdDocs && !linkedFwdDocs.some(l => l instanceof Promise)) { - let maxLocation = StrCast(linkedFwdDocs[0].maximizeLocation, "inTab"); - let targetContext = !Doc.AreProtosEqual(linkedFwdContextDocs[altKey ? 1 : 0], this.props.ContainingCollectionDoc) ? linkedFwdContextDocs[altKey ? 1 : 0] : undefined; - DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], ctrlKey, false, - // open up target if it's not already in view ... by zooming into the button document first and setting flag to reset zoom afterwards - doc => this.props.focus(this.props.Document, true, 1, () => this.props.addDocTab(doc, undefined, maxLocation)), - linkedFwdPage[altKey ? 1 : 0], targetContext); - } + else if (linkDocs.length) { + DocumentManager.Instance.FollowLink(undefined, this.props.Document, + // open up target if it's not already in view ... by zooming into the button document first and setting flag to reset zoom afterwards + (doc: Doc, maxLocation: string) => this.props.focus(this.props.Document, true, 1, () => this.props.addDocTab(doc, undefined, maxLocation)), + ctrlKey, altKey, this.props.ContainingCollectionDoc); } } onPointerDown = (e: React.PointerEvent): void => { - if (e.nativeEvent.cancelBubble) return; + if (e.nativeEvent.cancelBubble && e.button === 0) return; this._downX = e.clientX; this._downY = e.clientY; this._hitTemplateDrag = false; @@ -260,17 +244,19 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu this._hitTemplateDrag = true; } } - if (this.active && e.button === 0 && !this.Document.lockedPosition) e.stopPropagation(); // events stop at the lowest document that is active. if right dragging, we let it go through though to allow for context menu clicks. PointerMove callbacks should remove themselves if the move event gets stopPropagated by a lower-level handler (e.g, marquee drag); + if (this.active && e.button === 0 && !this.Document.lockedPosition && !this.Document.inOverlay) e.stopPropagation(); // events stop at the lowest document that is active. if right dragging, we let it go through though to allow for context menu clicks. PointerMove callbacks should remove themselves if the move event gets stopPropagated by a lower-level handler (e.g, marquee drag); document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); document.addEventListener("pointermove", this.onPointerMove); document.addEventListener("pointerup", this.onPointerUp); + if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); } } onPointerMove = (e: PointerEvent): void => { + if ((e as any).formattedHandled) { e.stopPropagation(); return; } if (e.cancelBubble && this.active) { document.removeEventListener("pointermove", this.onPointerMove); // stop listening to pointerMove if something else has stopPropagated it (e.g., the MarqueeView) } - else if (!e.cancelBubble && (SelectionManager.IsSelected(this) || this.props.parentActive()) && !this.Document.lockedPosition) { + else if (!e.cancelBubble && (SelectionManager.IsSelected(this) || this.props.parentActive()) && !this.Document.lockedPosition && !this.Document.inOverlay) { if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) { if (!e.altKey && !this.topMost && e.buttons === 1) { document.removeEventListener("pointermove", this.onPointerMove); @@ -352,15 +338,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu if (de.data instanceof DragManager.AnnotationDragData) { /// this whole section for handling PDF annotations looks weird. Need to rethink this to make it cleaner e.stopPropagation(); - let sourceDoc = de.data.annotationDocument; - let targetDoc = this.props.Document; - let annotations = await DocListCastAsync(sourceDoc.annotations); - sourceDoc.linkedToDoc = true; - de.data.targetContext = this.props.ContainingCollectionDoc; - targetDoc.targetContext = de.data.targetContext; - annotations && annotations.forEach(anno => anno.target = targetDoc); - - DocUtils.MakeLink(sourceDoc, targetDoc, this.props.ContainingCollectionDoc, `Link from ${StrCast(sourceDoc.title)}`); + (de.data as any).linkedToDoc = true; + + DocUtils.MakeLink({ doc: de.data.annotationDocument }, { doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, `Link from ${StrCast(de.data.annotationDocument.title)}`); } if (de.data instanceof DragManager.DocumentDragData && de.data.applyAsTemplate) { Doc.ApplyTemplateTo(de.data.draggedDocuments[0], this.props.Document); @@ -371,7 +351,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu // const docs = await SearchUtil.Search(`data_l:"${destDoc[Id]}"`, true); // const views = docs.map(d => DocumentManager.Instance.getDocumentView(d)).filter(d => d).map(d => d as DocumentView); de.data.linkSourceDocument !== this.props.Document && - (de.data.linkDocument = DocUtils.MakeLink(de.data.linkSourceDocument, this.props.Document, this.props.ContainingCollectionDoc, undefined, "in-text link being created")); // TODODO this is where in text links get passed + (de.data.linkDocument = DocUtils.MakeLink({ doc: de.data.linkSourceDocument }, { doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, "in-text link being created")); // TODODO this is where in text links get passed } } @@ -407,7 +387,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu let portalID = (this.Document.title + ".portal").replace(/^-/, "").replace(/\([0-9]*\)$/, ""); DocServer.GetRefField(portalID).then(existingPortal => { let portal = existingPortal instanceof Doc ? existingPortal : Docs.Create.FreeformDocument([], { width: (this.Document.width || 0) + 10, height: this.Document.height || 0, title: portalID }); - DocUtils.MakeLink(this.props.Document, portal, undefined, portalID); + DocUtils.MakeLink({ doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, { doc: portal }, portalID, "portal link"); this.Document.isButton = true; }); } @@ -635,6 +615,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu DataDoc={this.props.DataDoc} />); } render() { + let animDims = this.props.Document.animateToDimensions ? Array.from(Cast(this.props.Document.animateToDimensions, listSpec("number"))!) : undefined; const ruleColor = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleColor_" + this.Document.heading]) : undefined; const ruleRounding = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleRounding_" + this.Document.heading]) : undefined; const colorSet = this.setsLayoutProp("backgroundColor"); @@ -644,7 +625,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu ruleColor && !colorSet ? ruleColor : StrCast(this.layoutDoc.backgroundColor) || this.props.backgroundColor(this.Document); const nativeWidth = this.props.Document.fitWidth ? this.props.PanelWidth() : this.nativeWidth > 0 && !this.Document.ignoreAspect ? `${this.nativeWidth}px` : "100%"; - const nativeHeight = this.props.Document.fitWidth ? this.props.PanelHeight() : this.Document.ignoreAspect || this.props.Document.fitWidth ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : nativeWidth !== "100%" ? nativeWidth : "100%"; + const nativeHeight = this.props.Document.fitWidth ? this.props.PanelHeight() : this.Document.ignoreAspect ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%"; const showOverlays = this.props.showOverlays ? this.props.showOverlays(this.Document) : undefined; const showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : this.getLayoutPropStr("showTitle"); const showCaption = showOverlays && "caption" in showOverlays ? showOverlays.caption : this.getLayoutPropStr("showCaption"); @@ -678,6 +659,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu SetValue={(value: string) => (Doc.GetProto(this.Document)[showTitle] = value) ? true : true} /> </div>); + let animheight = animDims ? animDims[1] : nativeHeight; + let animwidth = animDims ? animDims[0] : nativeWidth; + + const highlightColors = ["transparent", "maroon", "maroon", "yellow", "magenta", "cyan", "orange"]; + const highlightStyles = ["solid", "dashed", "solid", "solid", "solid", "solid", "solid", "solid"]; return ( <div className={`documentView-node${this.topMost ? "-topmost" : ""}`} ref={this._mainCont} @@ -685,13 +671,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu transition: this.props.Document.isAnimating !== undefined ? ".5s linear" : StrCast(this.Document.transition), pointerEvents: this.Document.isBackground && !this.isSelected() ? "none" : "all", color: StrCast(this.Document.color), - 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, + outline: fullDegree && !borderRounding ? `${highlightColors[fullDegree]} ${highlightStyles[fullDegree]} ${localScale}px` : "solid 0px", + border: fullDegree && borderRounding ? `${highlightStyles[fullDegree]} ${highlightColors[fullDegree]} ${localScale}px` : undefined, background: backgroundColor, - width: nativeWidth, - height: nativeHeight, + width: animwidth, + height: animheight, transform: `scale(${this.props.Document.fitWidth ? 1 : this.props.ContentScaling()})`, opacity: this.Document.opacity }} diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss index 45e516015..29e8b14a8 100644 --- a/src/client/views/nodes/FormattedTextBox.scss +++ b/src/client/views/nodes/FormattedTextBox.scss @@ -28,7 +28,7 @@ } .formattedTextBox-cont-hidden { - pointer-events: none; + // pointer-events: none; } .formattedTextBox-inner-rounded { @@ -40,9 +40,10 @@ left: 10%; } -.formattedTextBox-inner-rounded div, -.formattedTextBox-inner div { +.formattedTextBox-inner-rounded , +.formattedTextBox-inner { padding: 10px 10px; + height: 100%; } .menuicon { @@ -153,17 +154,18 @@ footnote::after { content: "..."; } -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 } +.ProseMirror { +ol { counter-reset: deci1 0; padding-left: 0px; } +.decimal1-ol {counter-reset: deci1; p { display: inline }; font-size: 24 ; ul, ol { padding-left:30px; } } +.decimal2-ol {counter-reset: deci2; p { display: inline }; font-size: 18 ; ul, ol { padding-left:30px; } } +.decimal3-ol {counter-reset: deci3; p { display: inline }; font-size: 14 ; ul, ol { padding-left:30px; } } +.decimal4-ol {counter-reset: deci4; p { display: inline }; font-size: 10 ; ul, ol { padding-left:30px; } } +.decimal5-ol {counter-reset: deci5; p { display: inline }; font-size: 10 ; ul, ol { padding-left:30px; } } +.decimal6-ol {counter-reset: deci6; p { display: inline }; font-size: 10 ; ul, ol { padding-left:30px; } } +.decimal7-ol {counter-reset: deci7; p { display: inline }; font-size: 10 ; ul, ol { padding-left:30px; } } +.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;} +.lower-alpha-ol {counter-reset: lalpha; p { display: inline }; font-size: 10; } .decimal1:before { content: counter(deci1) ") "; counter-increment: deci1; display:inline-block; min-width: 30;} .decimal2:before { content: counter(deci1) "." counter(deci2) ") "; counter-increment: deci2; display:inline-block; min-width: 35} .decimal3:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) ") "; counter-increment: deci3; display:inline-block; min-width: 35} @@ -174,3 +176,4 @@ ol { counter-reset: deci1 0;} .upper-alpha:before { content: counter(deci1) "." counter(ualph, upper-alpha) ") "; counter-increment: ualph; display:inline-block; min-width: 35 } .lower-roman:before { content: counter(deci1) "." counter(ualph, upper-alpha) "." counter(lroman, lower-roman) ") "; counter-increment: lroman;display:inline-block; min-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; min-width: 35} +} diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 449f56b16..67d1bc42c 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -1,53 +1,54 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faEdit, faSmile, faTextHeight, faUpload } from '@fortawesome/free-solid-svg-icons'; +import _ from "lodash"; import { action, computed, IReactionDisposer, Lambda, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import { baseKeymap } from "prosemirror-commands"; import { history } from "prosemirror-history"; +import { inputRules } from 'prosemirror-inputrules'; import { keymap } from "prosemirror-keymap"; -import { Fragment, Node, Node as ProsNode, NodeType, Slice, Mark, ResolvedPos } from "prosemirror-model"; -import { EditorState, Plugin, Transaction, TextSelection, NodeSelection } from "prosemirror-state"; +import { Fragment, Mark, Node, Node as ProsNode, NodeType, Slice } from "prosemirror-model"; +import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from "prosemirror-state"; +import { ReplaceStep } from 'prosemirror-transform'; import { EditorView } from "prosemirror-view"; import { DateField } from '../../../new_fields/DateField'; -import { Doc, DocListCast, Opt, WidthSym } from "../../../new_fields/Doc"; +import { Doc, DocListCastAsync, Opt, WidthSym, HeightSym } from "../../../new_fields/Doc"; import { Copy, Id } from '../../../new_fields/FieldSymbols'; -import { List } from '../../../new_fields/List'; import { RichTextField } from "../../../new_fields/RichTextField"; -import { BoolCast, Cast, NumCast, StrCast, DateCast, PromiseValue } from "../../../new_fields/Types"; +import { RichTextUtils } from '../../../new_fields/RichTextUtils'; import { createSchema, makeInterface } from "../../../new_fields/Schema"; -import { Utils, numberRange, timenow } from '../../../Utils'; +import { Cast, DateCast, NumCast, StrCast } from "../../../new_fields/Types"; +import { numberRange, Utils, addStyleSheet, addStyleSheetRule, clearStyleSheetRules } from '../../../Utils'; +import { GoogleApiClientUtils, Pulls, Pushes } from '../../apis/google_docs/GoogleApiClientUtils'; import { DocServer } from "../../DocServer"; import { Docs, DocUtils } from '../../documents/Documents'; +import { DocumentType } from '../../documents/DocumentTypes'; +import { DictationManager } from '../../util/DictationManager'; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager } from "../../util/DragManager"; import buildKeymap from "../../util/ProsemirrorExampleTransfer"; import { inpRules } from "../../util/RichTextRules"; -import { ImageResizeView, schema, SummarizedView, OrderedListView, FootnoteView } from "../../util/RichTextSchema"; +import { FootnoteView, ImageResizeView, DashDocView, OrderedListView, 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 { DocComponent } from "../DocComponent"; +import { DocumentButtonBar } from '../DocumentButtonBar'; +import { DocumentDecorations } from '../DocumentDecorations'; import { InkingControl } from "../InkingControl"; import { FieldView, FieldViewProps } from "./FieldView"; import "./FormattedTextBox.scss"; +import { FormattedTextBoxComment, formattedTextBoxCommentPlugin } from './FormattedTextBoxComment'; import React = require("react"); -import { GoogleApiClientUtils, Pulls, Pushes } from '../../apis/google_docs/GoogleApiClientUtils'; -import { DocumentDecorations } from '../DocumentDecorations'; -import { DictationManager } from '../../util/DictationManager'; -import { ReplaceStep } from 'prosemirror-transform'; -import { DocumentType } from '../../documents/DocumentTypes'; -import { RichTextUtils } from '../../../new_fields/RichTextUtils'; -import _ from "lodash"; -import { formattedTextBoxCommentPlugin, FormattedTextBoxComment } from './FormattedTextBoxComment'; -import { inputRules } from 'prosemirror-inputrules'; -import { DocumentButtonBar } from '../DocumentButtonBar'; +import { ContextMenuProps } from '../ContextMenuItem'; +import { ContextMenu } from '../ContextMenu'; +import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; library.add(faEdit); library.add(faSmile, faTextHeight, faUpload); export interface FormattedTextBoxProps { - isOverlay?: boolean; hideOnLeave?: boolean; height?: string; color?: string; @@ -78,7 +79,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe private _proseRef?: HTMLDivElement; private _editorView: Opt<EditorView>; private _applyingChange: boolean = false; - private _linkClicked = ""; private _nodeClicked: any; private _undoTyping?: UndoManager.Batch; private _searchReactionDisposer?: Lambda; @@ -96,9 +96,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe @observable private _fontFamily = "Arial"; @observable private _fontAlign = ""; @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; } @@ -120,8 +118,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe return ""; } - public static getToolTip() { - return this._toolTipTextMenu; + public static getToolTip(ev: EditorView) { + return this._toolTipTextMenu ? this._toolTipTextMenu : this._toolTipTextMenu = new TooltipTextMenu(ev); } @undoBatch @@ -138,55 +136,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe constructor(props: FieldViewProps) { super(props); - if (this.props.isOverlay) { - DragManager.StartDragFunctions.push(() => FormattedTextBox.InputBoxOverlay = undefined); - } FormattedTextBox.Instance = this; - this._scrollToRegionReactionDisposer = reaction( - () => StrCast(this.props.Document.scrollToLinkID), - async (scrollToLinkID) => { - let findLinkFrag = (frag: Fragment, editor: EditorView) => { - const nodes: Node[] = []; - frag.forEach((node, index) => { - let examinedNode = findLinkNode(node, editor); - if (examinedNode && examinedNode.textContent) { - nodes.push(examinedNode); - start += index; - } - }); - return { frag: Fragment.fromArray(nodes), start: start }; - }; - let findLinkNode = (node: Node, editor: EditorView) => { - if (!node.isText) { - const content = findLinkFrag(node.content, editor); - return node.copy(content.frag); - } - const marks = [...node.marks]; - const linkIndex = marks.findIndex(mark => mark.type === editor.state.schema.marks.link); - return linkIndex !== -1 && scrollToLinkID === marks[linkIndex].attrs.href.replace(/.*\/doc\//, "") ? node : undefined; - }; - - let start = -1; - if (this._editorView && scrollToLinkID) { - let editor = this._editorView; - let ret = findLinkFrag(editor.state.doc.content, editor); - - if (ret.frag.size > 2 && ((!this.props.isOverlay && !this.props.isSelected()) || (this.props.isSelected() && this.props.isOverlay))) { - let selection = TextSelection.near(editor.state.doc.resolve(ret.start)); // default to near the start - if (ret.frag.firstChild) { - selection = TextSelection.between(editor.state.doc.resolve(ret.start + 2), editor.state.doc.resolve(ret.start + ret.frag.firstChild.nodeSize)); // bcz: looks better to not have the target selected - } - editor.dispatch(editor.state.tr.setSelection(new TextSelection(selection.$from, selection.$from)).scrollIntoView()); - const mark = editor.state.schema.mark(this._editorView.state.schema.marks.search_highlight); - setTimeout(() => editor.dispatch(editor.state.tr.addMark(selection.from, selection.to, mark)), 0); - setTimeout(() => this.unhighlightSearchTerms(), 2000); - } - this.props.Document.scrollToLinkID = undefined; - } - - }, - { fireImmediately: true } - ); } public get CurrentDiv(): HTMLDivElement { return this._ref.current!; } @@ -195,29 +145,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplate ? this.props.DataDoc : Doc.GetProto(this.props.Document); } - // 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; - } - } - } - linkOnDeselect: Map<string, string> = new Map(); doLinkOnDeselect() { @@ -230,7 +157,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this.dataDoc[key] = doc || Docs.Create.FreeformDocument([], { title: value, width: 500, height: 500 }, value); DocUtils.Publish(this.dataDoc[key] as Doc, value, this.props.addDocument, this.props.removeDocument); if (linkDoc) { (linkDoc as Doc).anchor2 = this.dataDoc[key] as Doc; } - else DocUtils.MakeLink(this.dataDoc, this.dataDoc[key] as Doc, undefined, "Ref:" + value, undefined, undefined, id, true); + else DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: this.dataDoc[key] as Doc }, "Ref:" + value, "link to named target", id); }); }); }); @@ -263,7 +190,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } const state = this._editorView.state.apply(tx); this._editorView.updateState(state); - 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); } @@ -335,14 +261,19 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe @action drop = async (e: Event, de: DragManager.DropEvent) => { // We're dealing with a link to a document - if (de.data instanceof DragManager.EmbedDragData && de.data.urlField) { + if (de.data instanceof DragManager.EmbedDragData) { let target = de.data.embeddableSourceDoc; // We're dealing with an internal document drop - let url = de.data.urlField.url.href; - let model: NodeType = [".mov", ".mp4"].includes(url) ? schema.nodes.video : schema.nodes.image; + const link = DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: target }, "ImgRef:" + target.title); + let alias = Doc.MakeAlias(target); + alias.fitToBox = true; + let node = schema.nodes.dashDoc.create({ + width: target[WidthSym](), height: target[HeightSym](), + title: "dashDoc", docid: alias[Id], + float: "right" + }); let pos = this._editorView!.posAtCoords({ left: de.x, top: de.y }); - this._editorView!.dispatch(this._editorView!.state.tr.insert(pos!.pos, model.create({ src: url, docid: target[Id] }))); - DocUtils.MakeLink(this.dataDoc, target, undefined, "ImgRef:" + target.title, undefined, undefined, target[Id]); + link && this._editorView!.dispatch(this._editorView!.state.tr.insert(pos!.pos, node)); this.tryUpdateHeight(); e.stopPropagation(); } else if (de.data instanceof DragManager.DocumentDragData) { @@ -353,7 +284,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new RichTextField(draggedDoc.data.Data); e.stopPropagation(); } - } else { + } else if (de.mods === "CtrlKey") { draggedDoc.isTemplate = true; if (typeof (draggedDoc.layout) === "string") { let layoutDelegateToOverrideFieldKey = Doc.MakeDelegate(draggedDoc); @@ -368,16 +299,58 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } } - recordKeyHandler = (e: KeyboardEvent) => { - if (SelectionManager.SelectedDocuments().length && this.props.Document === SelectionManager.SelectedDocuments()[0].props.Document) { - if (e.key === "R" && e.altKey) { - e.stopPropagation(); - e.preventDefault(); - this.recordBullet(); - } + + static _highlights: string[] = []; + + updateHighlights = () => { + clearStyleSheetRules(FormattedTextBox._userStyleSheet); + if (FormattedTextBox._highlights.indexOf("My Text") !== -1) { + addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { background: "yellow" }); + } + if (FormattedTextBox._highlights.indexOf("Todo Items") !== -1) { + addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "todo", { outline: "black solid 1px" }); + } + if (FormattedTextBox._highlights.indexOf("Important Items") !== -1) { + addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "important", { "font-size": "larger" }); + } + if (FormattedTextBox._highlights.indexOf("Disagree Items") !== -1) { + addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "disagree", { "text-decoration": "line-through" }); + } + if (FormattedTextBox._highlights.indexOf("Ignore Items") !== -1) { + addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "ignore", { "font-size": "0" }); + } + if (FormattedTextBox._highlights.indexOf("By Recent Minute") !== -1) { + addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { opacity: "0.1" }); + let min = Math.round(Date.now() / 1000 / 60); + numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-min-" + (min - i), { opacity: ((10 - i - 1) / 10).toString() })); + setTimeout(() => this.updateHighlights()); + } + if (FormattedTextBox._highlights.indexOf("By Recent Hour") !== -1) { + addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { opacity: "0.1" }); + let hr = Math.round(Date.now() / 1000 / 60 / 60); + numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-hr-" + (hr - i), { opacity: ((10 - i - 1) / 10).toString() })); } } + specificContextMenu = (e: React.MouseEvent): void => { + let funcs: ContextMenuProps[] = []; + funcs.push({ description: "Dictate", event: () => { e.stopPropagation(); this.recordBullet(); }, icon: "expand-arrows-alt" }); + ["My Text", "Todo Items", "Important Items", "Ignore Items", "Disagree Items", "By Recent Minute", "By Recent Hour"].forEach(option => + funcs.push({ + description: (FormattedTextBox._highlights.indexOf(option) === -1 ? "Highlight " : "Unhighlight ") + option, event: () => { + e.stopPropagation(); + if (FormattedTextBox._highlights.indexOf(option) === -1) { + FormattedTextBox._highlights.push(option); + } else { + FormattedTextBox._highlights.splice(FormattedTextBox._highlights.indexOf(option), 1); + } + this.updateHighlights(); + }, icon: "expand-arrows-alt" + })); + + ContextMenu.Instance.addItem({ description: "Text Funcs...", subitems: funcs, icon: "asterisk" }); + } + recordBullet = async () => { let completedCue = "end session"; let results = await DictationManager.Controls.listen({ @@ -408,12 +381,14 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe nextBullet = (pos: number) => { if (this._editorView) { let frag = Fragment.fromArray(this.newListItems(2)); - let slice = new Slice(frag, 2, 2); - let state = this._editorView.state; - this._editorView.dispatch(state.tr.step(new ReplaceStep(pos, pos, slice))); - pos += 4; - state = this._editorView.state; - this._editorView.dispatch(state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos, pos))); + if (this._editorView.state.doc.resolve(pos).depth >= 2) { + let slice = new Slice(frag, 2, 2); + let state = this._editorView.state; + this._editorView.dispatch(state.tr.step(new ReplaceStep(pos, pos, slice))); + pos += 4; + state = this._editorView.state; + this._editorView.dispatch(state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos, pos))); + } } } @@ -424,9 +399,10 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe _keymap: any = undefined; @computed get config() { this._keymap = buildKeymap(schema); + (schema as any).Document = this.props.Document; return { schema, - plugins: this.props.isOverlay ? [ + plugins: [ inputRules(inpRules), this.tooltipTextMenuPlugin(), history(), @@ -439,26 +415,11 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } }), formattedTextBoxCommentPlugin - ] : [ - history(), - keymap(this._keymap), - keymap(baseKeymap), - ] + ] }; } componentDidMount() { - - 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.pullFromGoogleDoc(this.checkState); this.dataDoc[GoogleRef] && this.dataDoc.unchanged && runInAction(() => DocumentDecorations.Instance.isAnimatingFetch = true); @@ -519,11 +480,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this._searchReactionDisposer = reaction(() => { return StrCast(this.props.Document.search_string); }, searchString => { - const fieldkey = 'preview'; - let preview = false; - // if (!this._editorView && Object.keys(this.props.Document).indexOf(fieldkey) !== -1) { - // preview = true; - // } if (searchString) { this.highlightSearchTerms([searchString]); } @@ -547,7 +503,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe }, action((rules: any) => { this._fontFamily = rules ? rules.font : "Arial"; - this._fontSize = rules ? rules.size : 13; + this._fontSize = rules ? rules.size : NumCast(this.props.Document.fontSize, 13); rules && setTimeout(() => { const view = this._editorView!; if (this._proseRef) { @@ -563,6 +519,51 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe }, 0); }), { fireImmediately: true } ); + this._scrollToRegionReactionDisposer = reaction( + () => StrCast(this.props.Document.scrollToLinkID), + async (scrollToLinkID) => { + let findLinkFrag = (frag: Fragment, editor: EditorView) => { + const nodes: Node[] = []; + frag.forEach((node, index) => { + let examinedNode = findLinkNode(node, editor); + if (examinedNode && examinedNode.textContent) { + nodes.push(examinedNode); + start += index; + } + }); + return { frag: Fragment.fromArray(nodes), start: start }; + }; + let findLinkNode = (node: Node, editor: EditorView) => { + if (!node.isText) { + const content = findLinkFrag(node.content, editor); + return node.copy(content.frag); + } + const marks = [...node.marks]; + const linkIndex = marks.findIndex(mark => mark.type === editor.state.schema.marks.link); + return linkIndex !== -1 && scrollToLinkID === marks[linkIndex].attrs.href.replace(/.*\/doc\//, "") ? node : undefined; + }; + + let start = -1; + if (this._editorView && scrollToLinkID) { + let editor = this._editorView; + let ret = findLinkFrag(editor.state.doc.content, editor); + + if (ret.frag.size > 2 && ret.start >= 0) { + let selection = TextSelection.near(editor.state.doc.resolve(ret.start)); // default to near the start + if (ret.frag.firstChild) { + selection = TextSelection.between(editor.state.doc.resolve(ret.start + 2), editor.state.doc.resolve(ret.start + ret.frag.firstChild.nodeSize)); // bcz: looks better to not have the target selected + } + editor.dispatch(editor.state.tr.setSelection(new TextSelection(selection.$from, selection.$from)).scrollIntoView()); + const mark = editor.state.schema.mark(this._editorView.state.schema.marks.search_highlight); + setTimeout(() => editor.dispatch(editor.state.tr.addMark(selection.from, selection.to, mark)), 0); + setTimeout(() => this.unhighlightSearchTerms(), 2000); + } + this.props.Document.scrollToLinkID = undefined; + } + + }, + { fireImmediately: true } + ); setTimeout(() => this.tryUpdateHeight(), 0); } @@ -667,55 +668,42 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe handlePaste = (view: EditorView, event: Event, slice: Slice): boolean => { let cbe = event as ClipboardEvent; - let docId: string; - let regionId: string; - if (!cbe.clipboardData) { - return false; - } - let linkId: string; - docId = cbe.clipboardData.getData("dash/pdfOrigin"); - regionId = cbe.clipboardData.getData("dash/pdfRegion"); - if (!docId || !regionId) { - return false; - } - - DocServer.GetRefField(docId).then(doc => { - DocServer.GetRefField(regionId).then(region => { - if (!(doc instanceof Doc) || !(region instanceof Doc)) { - return; - } - - let annotations = DocListCast(region.annotations); - annotations.forEach(anno => anno.target = this.props.Document); - let fieldExtDoc = Doc.fieldExtensionDoc(doc, "data"); - let targetAnnotations = DocListCast(fieldExtDoc.annotations); - if (targetAnnotations) { - targetAnnotations.push(region); - fieldExtDoc.annotations = new List<Doc>(targetAnnotations); - } + const pdfDocId = cbe.clipboardData && cbe.clipboardData.getData("dash/pdfOrigin"); + const pdfRegionId = cbe.clipboardData && cbe.clipboardData.getData("dash/pdfRegion"); + if (pdfDocId && pdfRegionId) { + DocServer.GetRefField(pdfDocId).then(pdfDoc => { + DocServer.GetRefField(pdfRegionId).then(pdfRegion => { + if ((pdfDoc instanceof Doc) && (pdfRegion instanceof Doc)) { + setTimeout(async () => { + let targetAnnotations = await DocListCastAsync(Doc.fieldExtensionDoc(pdfDoc, "data").annotations);// bcz: NO... this assumes the pdf is using its 'data' field. need to have the PDF's view handle updating its own annotations + targetAnnotations && targetAnnotations.push(pdfRegion); + }); - let link = DocUtils.MakeLink(this.props.Document, region, doc); - if (link) { - cbe.clipboardData!.setData("dash/linkDoc", link[Id]); - linkId = link[Id]; - let frag = addMarkToFrag(slice.content, (node: Node) => addLinkMark(node, StrCast(doc.title))); - slice = new Slice(frag, slice.openStart, slice.openEnd); - var tr = view.state.tr.replaceSelection(slice); - view.dispatch(tr.scrollIntoView().setMeta("paste", true).setMeta("uiEvent", "paste")); - } + let link = DocUtils.MakeLink({ doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, { doc: pdfRegion, ctx: pdfDoc }, "note on " + pdfDoc.title, "pasted PDF link"); + if (link) { + cbe.clipboardData!.setData("dash/linkDoc", link[Id]); + let linkId = link[Id]; + let frag = addMarkToFrag(slice.content, (node: Node) => addLinkMark(node, StrCast(pdfDoc.title), linkId)); + slice = new Slice(frag, slice.openStart, slice.openEnd); + var tr = view.state.tr.replaceSelection(slice); + view.dispatch(tr.scrollIntoView().setMeta("paste", true).setMeta("uiEvent", "paste")); + } + } + }); }); - }); + return true; + } + return false; - return true; function addMarkToFrag(frag: Fragment, marker: (node: Node) => Node) { const nodes: Node[] = []; frag.forEach(node => nodes.push(marker(node))); return Fragment.fromArray(nodes); } - function addLinkMark(node: Node, title: string) { + function addLinkMark(node: Node, title: string, linkId: string) { if (!node.isText) { - const content = addMarkToFrag(node.content, (node: Node) => addLinkMark(node, title)); + const content = addMarkToFrag(node.content, (node: Node) => addLinkMark(node, title, linkId)); return node.copy(content); } const marks = [...node.marks]; @@ -754,6 +742,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe }, dispatchTransaction: this.dispatchTransaction, nodeViews: { + dashDoc(node, view, getPos) { return new DashDocView(node, view, getPos, self); }, image(node, view, getPos) { return new ImageResizeView(node, view, getPos, self.props.addDocTab); }, star(node, view, getPos) { return new SummarizedView(node, view, getPos); }, ordered_list(node, view, getPos) { return new OrderedListView(); }, @@ -762,7 +751,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe 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)); @@ -774,9 +762,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe FormattedTextBox.SelectOnLoad = ""; this.props.select(false); } - else if (this.props.isOverlay) this._editorView!.focus(); + this._editorView!.focus(); // add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet. - this._editorView!.state.storedMarks = [...(this._editorView!.state.storedMarks ? this._editorView!.state.storedMarks : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() })]; + this._editorView!.state.storedMarks = [...(this._editorView!.state.storedMarks ? this._editorView!.state.storedMarks : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.round(Date.now() / 1000 / 5) })]; } getFont(font: string) { switch (font) { @@ -803,8 +791,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this._searchReactionDisposer && this._searchReactionDisposer(); this._editorView && this._editorView.destroy(); } - - onPointerDown = (e: React.PointerEvent): void => { let pos = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY }); pos && (this._nodeClicked = this._editorView!.state.doc.nodeAt(pos.pos)); @@ -814,31 +800,25 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe if (e.button === 0 && this.props.isSelected() && !e.altKey && !e.ctrlKey && !e.metaKey) { e.stopPropagation(); } - let ctrlKey = e.ctrlKey; if (e.button === 2 || (e.button === 0 && e.ctrlKey)) { e.preventDefault(); } } onPointerUp = (e: React.PointerEvent): void => { - FormattedTextBoxComment.textBox = this; + if (!(e.nativeEvent as any).formattedHandled) { FormattedTextBoxComment.textBox = this; } + (e.nativeEvent as any).formattedHandled = true; + if (e.buttons === 1 && this.props.isSelected() && !e.altKey) { e.stopPropagation(); } } + static InputBoxOverlay: FormattedTextBox | undefined; @action onFocused = (e: React.FocusEvent): void => { - document.removeEventListener("keypress", this.recordKeyHandler); - document.addEventListener("keypress", this.recordKeyHandler); + FormattedTextBox.InputBoxOverlay = this; this.tryUpdateHeight(); - if (!this.props.isOverlay) { - FormattedTextBox.InputBoxOverlay = this; - } else { - if (this._ref.current) { - this._ref.current.scrollTop = FormattedTextBox.InputBoxOverlayScroll; - } - } } onPointerWheel = (e: React.WheelEvent): void => { // if a text note is not selected and scrollable, this prevents us from being able to scroll and zoom out at the same time @@ -847,17 +827,18 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } } + static _bulletStyleSheet: any = addStyleSheet(); + static _userStyleSheet: any = addStyleSheet(); + onClick = (e: React.MouseEvent): void => { - let ctrlKey = e.ctrlKey; + if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); return; } + (e.nativeEvent as any).formattedHandled = true; if (e.button === 0 && ((!this.props.isSelected() && !e.ctrlKey) || (this.props.isSelected() && e.ctrlKey)) && !e.metaKey && e.target) { let href = (e.target as any).href; let location: string; if ((e.target as any).attributes.location) { location = (e.target as any).attributes.location.value; } - for (let parent = (e.target as any).parentNode; !href && parent; parent = parent.parentNode) { - href = parent.childNodes[0].href ? parent.childNodes[0].href : parent.href; - } let pcords = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY }); let node = pcords && this._editorView!.state.doc.nodeAt(pcords.pos); if (node) { @@ -869,57 +850,42 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } if (href) { if (href.indexOf(Utils.prepend("/doc/")) === 0) { - this._linkClicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0]; - if (this._linkClicked) { - DocServer.GetRefField(this._linkClicked).then(async linkDoc => { - if (linkDoc instanceof Doc) { - let proto = Doc.GetProto(linkDoc); - let targetContext = await Cast(proto.targetContext, Doc); - let jumpToDoc = await Cast(linkDoc.anchor2, Doc); - - if (jumpToDoc) { - if (DocumentManager.Instance.getDocumentView(jumpToDoc)) { - DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, undefined, undefined, NumCast((jumpToDoc === linkDoc.anchor2 ? linkDoc.anchor2Page : linkDoc.anchor1Page))); - return; - } - } - if (targetContext && (!jumpToDoc || targetContext !== await jumpToDoc.annotationOn)) { - DocumentManager.Instance.jumpToDocument(jumpToDoc || targetContext, ctrlKey, false, document => this.props.addDocTab(document, undefined, location ? location : "inTab"), undefined, targetContext); - } else if (jumpToDoc) { - DocumentManager.Instance.jumpToDocument(jumpToDoc, ctrlKey, false, document => this.props.addDocTab(document, undefined, location ? location : "inTab")); - } else { - DocumentManager.Instance.jumpToDocument(linkDoc, ctrlKey, false, document => this.props.addDocTab(document, undefined, location ? location : "inTab")); - } - } + let linkClicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0]; + if (linkClicked) { + DocServer.GetRefField(linkClicked).then(async linkDoc => { + (linkDoc instanceof Doc) && + DocumentManager.Instance.FollowLink(linkDoc, this.props.Document, document => this.props.addDocTab(document, undefined, location ? location : "inTab"), false); }); - e.stopPropagation(); - e.preventDefault(); } } else { let webDoc = Docs.Create.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(); } - } - // this hackiness handles clicking on the list item bullets to do expand/collapse. the bullets are ::before pseudo elements so there's no real way to hit test against them. - if (this.props.isSelected() && e.nativeEvent.offsetX < 40) { - let pos = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY }); + + this.hitBulletTargets(e.clientX, e.clientY, e.nativeEvent.offsetX, e.shiftKey); + } + + // this hackiness handles clicking on the list item bullets to do expand/collapse. the bullets are ::before pseudo elements so there's no real way to hit test against them. + hitBulletTargets(x: number, y: number, offsetX: number, select: boolean = false) { + clearStyleSheetRules(FormattedTextBox._bulletStyleSheet); + if (this.props.isSelected() && offsetX < 40) { + let pos = this._editorView!.posAtCoords({ left: x, top: y }); if (pos && pos.pos > 0) { let node = this._editorView!.state.doc.nodeAt(pos.pos); let node2 = node && node.type === schema.nodes.paragraph ? this._editorView!.state.doc.nodeAt(pos.pos - 1) : undefined; if (node === this._nodeClicked && node2 && (node2.type === schema.nodes.ordered_list || node2.type === schema.nodes.list_item)) { - let hit = this._editorView!.domAtPos(pos.pos).node as any; - let beforeEle = document.querySelector("." + hit.className) as Element; - let before = beforeEle ? window.getComputedStyle(beforeEle, ':before') : undefined; + let hit = this._editorView!.domAtPos(pos.pos).node as any; // let beforeEle = document.querySelector("." + hit.className) as Element; + let before = hit ? window.getComputedStyle(hit, ':before') : undefined; let beforeWidth = before ? Number(before.getPropertyValue('width').replace("px", "")) : undefined; - if (beforeWidth && e.nativeEvent.offsetX < beforeWidth) { + if (beforeWidth && offsetX < beforeWidth) { let ol = this._editorView!.state.doc.nodeAt(pos.pos - 2) ? this._editorView!.state.doc.nodeAt(pos.pos - 2) : undefined; - if (ol && ol.type === schema.nodes.ordered_list && !e.shiftKey) { + if (ol && ol.type === schema.nodes.ordered_list && select) { this._editorView!.dispatch(this._editorView!.state.tr.setSelection(new NodeSelection(this._editorView!.state.doc.resolve(pos.pos - 2)))); + addStyleSheetRule(FormattedTextBox._bulletStyleSheet, hit.className + ":before", { background: "gray" }); } else { this._editorView!.dispatch(this._editorView!.state.tr.setNodeMarkup(pos.pos - 1, node2.type, { ...node2.attrs, visibility: !node2.attrs.visibility })); } @@ -927,25 +893,28 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } } } - 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 - e.preventDefault(); // bcz: this would normally be in OnPointerDown - however, if done there, no mouse move events will be generated which makes transititioning to GoldenLayout's drag interactions impossible + onMouseUp = (e: React.MouseEvent): void => { + e.stopPropagation(); + + // this interposes on prosemirror's upHandler to prevent prosemirror's up from invoked multiple times when there are nested prosemirrors. We only want the lowest level prosemirror to be invoked. + if ((this._editorView as any).mouseDown) { + let originalUpHandler = (this._editorView as any).mouseDown.up; + (this._editorView as any).root.removeEventListener("mouseup", originalUpHandler); + (this._editorView as any).mouseDown.up = (e: MouseEvent) => { + !(e as any).formattedHandled && originalUpHandler(e); + e.stopPropagation(); + (e as any).formattedHandled = true; + }; + (this._editorView as any).root.addEventListener("mouseup", (this._editorView as any).mouseDown.up); } } tooltipTextMenuPlugin() { - let myprops = this.props; let self = FormattedTextBox; return new Plugin({ - view(_editorView) { - return self._toolTipTextMenu = new TooltipTextMenu(_editorView, myprops); + view(newView) { + return self._toolTipTextMenu = FormattedTextBox.getToolTip(newView); } }); } @@ -959,7 +928,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe }); } onBlur = (e: any) => { - document.removeEventListener("keypress", this.recordKeyHandler); + DictationManager.Controls.stop(false); if (this._undoTyping) { this._undoTyping.end(); this._undoTyping = undefined; @@ -974,7 +943,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe if (e.key === "Tab" || e.key === "Enter") { e.preventDefault(); } - this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() }))); + let mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.round(Date.now() / 1000 / 5) }); + this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(mark)); if (!this._undoTyping) { this._undoTyping = UndoManager.StartBatch("undoTyping"); @@ -985,7 +955,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe tryUpdateHeight() { const ChromeHeight = this.props.ChromeHeight; let sh = this._ref.current ? this._ref.current.scrollHeight : 0; - if (!this.props.isOverlay && !this.props.Document.isAnimating && this.props.Document.autoHeight && sh !== 0) { + if (!this.props.Document.isAnimating && this.props.Document.autoHeight && sh !== 0 && getComputedStyle(this._ref.current!.parentElement!).top === "0px") { let nh = this.props.Document.isTemplate ? 0 : NumCast(this.dataDoc.nativeHeight, 0); let dh = NumCast(this.props.Document.height, 0); this.props.Document.height = Math.max(10, (nh ? dh / nh * sh : sh) + (ChromeHeight ? ChromeHeight() : 0)); @@ -994,30 +964,34 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } render() { - let style = this.props.isOverlay ? "scroll" : "hidden"; + let style = "hidden"; let rounded = StrCast(this.props.Document.borderRounding) === "100%" ? "-rounded" : ""; let interactive: "all" | "none" = InkingControl.Instance.selectedTool || this.props.Document.isBackground ? "none" : "all"; Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey); + if (this.props.isSelected()) { + FormattedTextBox._toolTipTextMenu!.updateFromDash(this._editorView!, undefined, this.props); + } return ( <div className={`formattedTextBox-cont-${style}`} ref={this._ref} style={{ overflowY: this.props.Document.autoHeight ? "hidden" : "auto", height: this.props.Document.autoHeight ? "max-content" : 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() || Doc.IsBrushed(this.props.Document) ? 1 : 0.1) : 1, + opacity: this.props.hideOnLeave ? (this._entered ? 1 : 0.1) : 1, color: this.props.color ? this.props.color : this.props.hideOnLeave ? "white" : "inherit", pointerEvents: interactive, fontSize: this._fontSize, fontFamily: this._fontFamily, }} + onContextMenu={this.specificContextMenu} onKeyDown={this.onKeyPress} onFocus={this.onFocused} onClick={this.onClick} onBlur={this.onBlur} onPointerUp={this.onPointerUp} onPointerDown={this.onPointerDown} - onMouseDown={this.onMouseDown} + onMouseUp={this.onMouseUp} onWheel={this.onPointerWheel} onPointerEnter={action(() => this._entered = true)} onPointerLeave={action(() => this._entered = false)} diff --git a/src/client/views/nodes/FormattedTextBoxComment.tsx b/src/client/views/nodes/FormattedTextBoxComment.tsx index 2d2fff06d..a75bfd373 100644 --- a/src/client/views/nodes/FormattedTextBoxComment.tsx +++ b/src/client/views/nodes/FormattedTextBoxComment.tsx @@ -7,6 +7,7 @@ import { schema } from "../../util/RichTextSchema"; import { DocServer } from "../../DocServer"; import { Utils } from "../../../Utils"; import { StrCast } from "../../../new_fields/Types"; +import { FormattedTextBox } from "./FormattedTextBox"; export let formattedTextBoxCommentPlugin = new Plugin({ view(editorView) { return new FormattedTextBoxComment(editorView); } @@ -49,7 +50,7 @@ export class FormattedTextBoxComment { static end: number; static mark: Mark; static opened: boolean; - static textBox: any; + static textBox: FormattedTextBox | undefined; constructor(view: any) { if (!FormattedTextBoxComment.tooltip) { const root = document.getElementById("root"); @@ -62,9 +63,9 @@ export class FormattedTextBoxComment { FormattedTextBoxComment.tooltip.style.pointerEvents = "all"; FormattedTextBoxComment.tooltip.appendChild(input); FormattedTextBoxComment.tooltip.onpointerdown = (e: PointerEvent) => { - let keep = e.target && (e.target as any).type === "checkbox"; + let keep = e.target && (e.target as any).type === "checkbox" ? true : false; FormattedTextBoxComment.opened = keep || !FormattedTextBoxComment.opened; - FormattedTextBoxComment.textBox && FormattedTextBoxComment.textBox.setAnnotation( + FormattedTextBoxComment.textBox && FormattedTextBoxComment.start !== undefined && FormattedTextBoxComment.textBox.setAnnotation( FormattedTextBoxComment.start, FormattedTextBoxComment.end, FormattedTextBoxComment.mark, FormattedTextBoxComment.opened, keep); }; @@ -92,8 +93,9 @@ export class FormattedTextBoxComment { if (lastState && lastState.doc.eq(state.doc) && lastState.selection.eq(state.selection)) return; + if (!FormattedTextBoxComment.textBox || !FormattedTextBoxComment.textBox.props.isSelected()) return; let set = "none"; - if (state.selection.$from) { + if (FormattedTextBoxComment.textBox && 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; diff --git a/src/client/views/nodes/IconBox.tsx b/src/client/views/nodes/IconBox.tsx index 63a504d1a..f3adade58 100644 --- a/src/client/views/nodes/IconBox.tsx +++ b/src/client/views/nodes/IconBox.tsx @@ -77,9 +77,9 @@ export class IconBox extends React.Component<FieldViewProps> { <div className="iconBox-container" onContextMenu={this.specificContextMenu}> {this.minimizedIcon} <Measure offset onResize={(r) => runInAction(() => { - if (r.offset!.width || BoolCast(this.props.Document.hideLabel)) { - this.props.Document.nativeWidth = (r.offset!.width + Number(MINIMIZED_ICON_SIZE)); - if (this.props.Document.height === Number(MINIMIZED_ICON_SIZE)) this.props.Document.width = this.props.Document.nativeWidth; + if (r.offset!.width || this.props.Document.hideLabel) { + this.props.Document.iconWidth = (r.offset!.width + Number(MINIMIZED_ICON_SIZE)); + if (this.props.Document.height === Number(MINIMIZED_ICON_SIZE)) this.props.Document.width = this.props.Document.iconWidth; } })}> {({ measureRef }) => diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 9e9fe1324..a198a0764 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -299,7 +299,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD let rotation = NumCast(this.dataDoc.rotation) % 180; let realsize = rotation === 90 || rotation === 270 ? { height: size.width, width: size.height } : size; let aspect = realsize.height / realsize.width; - if (layoutdoc.nativeHeight !== 0 && layoutdoc.nativeWidth !== 0 && (Math.abs(NumCast(layoutdoc.height) - realsize.height) > 1 || Math.abs(NumCast(layoutdoc.width) - realsize.width) > 1)) { + if (layoutdoc.width && (Math.abs(1 - NumCast(layoutdoc.height) / NumCast(layoutdoc.width) / (realsize.height / realsize.width)) > 0.1)) { setTimeout(action(() => { layoutdoc.height = layoutdoc[WidthSym]() * aspect; layoutdoc.nativeHeight = realsize.height; diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss index 3f5f47e05..1c1d6ec95 100644 --- a/src/client/views/nodes/PDFBox.scss +++ b/src/client/views/nodes/PDFBox.scss @@ -10,12 +10,12 @@ } .pdfBox-title-outer { - + z-index: 0; position: absolute; width: 100%; height: 100%; background: lightslategray; - .pdfBox-title-cont { + .pdfBox-title-cont, .pdfBox-cont-interactive{ width: 150%; height: 100%; position: relative; diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 3ad0b4010..1f3887608 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -57,20 +57,17 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen } loaded = (nw: number, nh: number, np: number) => { this.dataDoc.numPages = np; - if (!this.Document.nativeWidth || !this.Document.nativeHeight || !this.Document.scrollHeight) { - let oldaspect = (this.Document.nativeHeight || 0) / (this.Document.nativeWidth || 1); - this.Document.nativeWidth = nw * 96 / 72; - this.Document.nativeHeight = this.Document.nativeHeight ? nw * 96 / 72 * oldaspect : nh * 96 / 72; - } + this.Document.nativeWidth = nw * 96 / 72; + this.Document.nativeHeight = nh * 96 / 72; !this.Document.fitWidth && !this.Document.ignoreAspect && (this.Document.height = this.Document[WidthSym]() * (nh / nw)); } public search(string: string, fwd: boolean) { this._pdfViewer && this._pdfViewer.search(string, fwd); } public prevAnnotation() { this._pdfViewer && this._pdfViewer.prevAnnotation(); } public nextAnnotation() { this._pdfViewer && this._pdfViewer.nextAnnotation(); } - public backPage() { this._pdfViewer!.gotoPage(NumCast(this.props.Document.curPage) - 1); } + public backPage() { this._pdfViewer!.gotoPage((this.Document.curPage || 1) - 1); } public gotoPage = (p: number) => { this._pdfViewer!.gotoPage(p); }; - public forwardPage() { this._pdfViewer!.gotoPage(NumCast(this.props.Document.curPage) + 1); } + public forwardPage() { this._pdfViewer!.gotoPage((this.Document.curPage || 1) + 1); } @undoBatch @action @@ -131,7 +128,7 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen <div className="pdfBox-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}> <FontAwesomeIcon style={{ color: "white", padding: 5 }} icon={this._searching ? "times" : "search"} size="3x" /></div> </button> - <input value={`${NumCast(this.props.Document.curPage)}`} + <input value={`${(this.Document.curPage || 1)}`} onChange={e => this.gotoPage(Number(e.currentTarget.value))} style={{ left: 20, top: 5, height: "30px", width: "30px", position: "absolute", pointerEvents: "all" }} onClick={action(() => this._pageControls = !this._pageControls)} /> @@ -177,16 +174,22 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen ContextMenu.Instance.addItem({ description: "Pdf Funcs...", subitems: funcs, icon: "asterisk" }); } + _initialScale: number | undefined; render() { const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField); let classname = "pdfBox-cont" + (InkingControl.Instance.selectedTool || !this.active ? "" : "-interactive"); let noPdf = !(pdfUrl instanceof PdfField) || !this._pdf; - if (!noPdf && (this.props.isSelected() || this.props.ScreenToLocalTransform().Scale < 2.5)) this._everActive = true; - return (!this._everActive ? - <div className="pdfBox-title-outer"><div className="pdfBox-title-cont" > - <strong className="pdfBox-title" >{` ${this.props.Document.title}`}</strong> </div></div> : + if (this._initialScale === undefined) this._initialScale = this.props.ScreenToLocalTransform().Scale; + if (this.props.isSelected() || this.props.Document.scrollY !== undefined) this._everActive = true; + return (noPdf || (!this._everActive && this.props.ScreenToLocalTransform().Scale > 2.5) ? + <div className="pdfBox-title-outer" > + <div className={classname} > + <strong className="pdfBox-title" >{` ${this.props.Document.title}`}</strong> + </div> + </div> : <div className={classname} style={{ - transformOrigin: "top left", width: this.props.Document.fitWidth ? `${100 / this.props.ContentScaling()}%` : undefined, + transformOrigin: "top left", + width: this.props.Document.fitWidth ? `${100 / this.props.ContentScaling()}%` : undefined, height: this.props.Document.fitWidth ? `${100 / this.props.ContentScaling()}%` : undefined, transform: `scale(${this.props.Document.fitWidth ? this.props.ContentScaling() : 1})` }} onContextMenu={this.specificContextMenu} onPointerDown={(e: React.PointerEvent) => { @@ -199,11 +202,11 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen setPdfViewer={this.setPdfViewer} ContainingCollectionView={this.props.ContainingCollectionView} renderDepth={this.props.renderDepth} PanelHeight={this.props.PanelHeight} PanelWidth={this.props.PanelWidth} Document={this.props.Document} DataDoc={this.dataDoc} ContentScaling={this.props.ContentScaling} - addDocTab={this.props.addDocTab} GoToPage={this.gotoPage} + addDocTab={this.props.addDocTab} GoToPage={this.gotoPage} focus={this.props.focus} pinToPres={this.props.pinToPres} addDocument={this.props.addDocument} ScreenToLocalTransform={this.props.ScreenToLocalTransform} select={this.props.select} isSelected={this.props.isSelected} whenActiveChanged={this.whenActiveChanged} - fieldKey={this.props.fieldKey} fieldExtensionDoc={this.extensionDoc} startupLive={this.props.ScreenToLocalTransform().Scale < 2.5 ? true : false} /> + fieldKey={this.props.fieldKey} fieldExtensionDoc={this.extensionDoc} startupLive={this._initialScale < 2.5 ? true : false} /> {this.settingsPanel()} </div>); } diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx index f44ca99b9..15fafb022 100644 --- a/src/client/views/nodes/PresBox.tsx +++ b/src/client/views/nodes/PresBox.tsx @@ -43,8 +43,8 @@ export class PresBox extends React.Component<FieldViewProps> { value.forEach((item, i) => { if (item instanceof Doc && item.type !== DocumentType.PRESELEMENT) { let pinDoc = Docs.Create.PresElementBoxDocument({ backgroundColor: "transparent" }); - Doc.GetProto(pinDoc).target = item; - Doc.GetProto(pinDoc).title = ComputedField.MakeFunction('(this.target instanceof Doc) && this.target.title.toString()'); + Doc.GetProto(pinDoc).presentationTargetDoc = item; + Doc.GetProto(pinDoc).title = ComputedField.MakeFunction('(this.presentationTargetDoc instanceof Doc) && this.presentationTargetDoc.title.toString()'); value.splice(i, 1, pinDoc); } }); @@ -124,13 +124,13 @@ export class PresBox extends React.Component<FieldViewProps> { this.childDocs.forEach((doc, ind) => { //the order of cases is aligned based on priority if (doc.hideTillShownButton && ind <= index) { - (doc.target as Doc).opacity = 1; + (doc.presentationTargetDoc as Doc).opacity = 1; } if (doc.hideAfterButton && ind < index) { - (doc.target as Doc).opacity = 0; + (doc.presentationTargetDoc as Doc).opacity = 0; } if (doc.fadeButton && ind < index) { - (doc.target as Doc).opacity = 0.5; + (doc.presentationTargetDoc as Doc).opacity = 0.5; } }); } @@ -145,13 +145,13 @@ export class PresBox extends React.Component<FieldViewProps> { //the order of cases is aligned based on priority if (key.hideAfterButton && ind >= index) { - (key.target as Doc).opacity = 1; + (key.presentationTargetDoc as Doc).opacity = 1; } if (key.fadeButton && ind >= index) { - (key.target as Doc).opacity = 1; + (key.presentationTargetDoc as Doc).opacity = 1; } if (key.hideTillShownButton && ind > index) { - (key.target as Doc).opacity = 0; + (key.presentationTargetDoc as Doc).opacity = 0; } }); } @@ -162,7 +162,7 @@ export class PresBox extends React.Component<FieldViewProps> { * te option open, navigates to that element. */ navigateToElement = async (curDoc: Doc, fromDocIndex: number) => { - let fromDoc = this.childDocs[fromDocIndex].target as Doc; + let fromDoc = this.childDocs[fromDocIndex].presentationTargetDoc as Doc; let docToJump = curDoc; let willZoom = false; @@ -190,7 +190,7 @@ export class PresBox extends React.Component<FieldViewProps> { //docToJump stayed same meaning, it was not in the group or was the last element in the group if (docToJump === curDoc) { //checking if curDoc has navigation open - let target = await curDoc.target as Doc; + let target = await curDoc.presentationTargetDoc as Doc; if (curDoc.navButton) { DocumentManager.Instance.jumpToDocument(target, false); } else if (curDoc.showButton) { @@ -210,8 +210,8 @@ export class PresBox extends React.Component<FieldViewProps> { let curScale = DocumentManager.Instance.getScaleOfDocView(fromDoc); //awaiting jump so that new scale can be found, since jumping is async - await DocumentManager.Instance.jumpToDocument(await docToJump.target as Doc, willZoom); - let newScale = DocumentManager.Instance.getScaleOfDocView(await curDoc.target as Doc); + await DocumentManager.Instance.jumpToDocument(await docToJump.presentationTargetDoc as Doc, willZoom); + let newScale = DocumentManager.Instance.getScaleOfDocView(await curDoc.presentationTargetDoc as Doc); curDoc.viewScale = newScale; //saving the scale that user was on if (curScale !== 1) { @@ -314,12 +314,11 @@ export class PresBox extends React.Component<FieldViewProps> { } toggleMinimize = undoBatch(action((e: React.PointerEvent) => { - if (this.props.Document.minimizedView) { - this.props.Document.minimizedView = false; + if (this.props.Document.inOverlay) { Doc.RemoveDocFromList((CurrentUserUtils.UserDocument.overlays as Doc), this.props.fieldKey, this.props.Document); CollectionDockingView.AddRightSplit(this.props.Document, this.props.DataDoc); + this.props.Document.inOverlay = false; } else { - this.props.Document.minimizedView = true; this.props.Document.x = e.clientX + 25; this.props.Document.y = e.clientY - 25; this.props.addDocTab && this.props.addDocTab(this.props.Document, this.props.DataDoc, "close"); @@ -370,9 +369,9 @@ export class PresBox extends React.Component<FieldViewProps> { <FontAwesomeIcon icon={this.props.Document.presStatus ? "stop" : "play"} /> </button> <button className="presBox-button" title="Next" onClick={this.next}><FontAwesomeIcon icon={"arrow-right"} /></button> - <button className="presBox-button" title={this.props.Document.minimizedView ? "Expand" : "Minimize"} onClick={this.toggleMinimize}><FontAwesomeIcon icon={"eye"} /></button> + <button className="presBox-button" title={this.props.Document.inOverlay ? "Expand" : "Minimize"} onClick={this.toggleMinimize}><FontAwesomeIcon icon={"eye"} /></button> </div> - {this.props.Document.minimizedView ? (null) : + {this.props.Document.inOverlay ? (null) : <div className="presBox-listCont" > <CollectionView {...this.props} focus={this.selectElement} ScreenToLocalTransform={this.getTransform} /> </div> diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index b7d9a1eab..e83aa8bea 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -3,7 +3,7 @@ import { action, computed, IReactionDisposer, observable, reaction, runInAction, import { observer } from "mobx-react"; import * as rp from 'request-promise'; import { InkTool } from "../../../new_fields/InkField"; -import { makeInterface } from "../../../new_fields/Schema"; +import { makeInterface, createSchema } from "../../../new_fields/Schema"; import { Cast, FieldValue, NumCast, BoolCast } from "../../../new_fields/Types"; import { VideoField } from "../../../new_fields/URLField"; import { RouteStore } from "../../../server/RouteStore"; @@ -16,16 +16,19 @@ import { DocumentDecorations } from "../DocumentDecorations"; import { InkingControl } from "../InkingControl"; import { documentSchema } from "./DocumentView"; import { FieldView, FieldViewProps } from './FieldView'; -import { pageSchema } from "./ImageBox"; import "./VideoBox.scss"; import { library } from "@fortawesome/fontawesome-svg-core"; import { faVideo } from "@fortawesome/free-solid-svg-icons"; import { Doc } from "../../../new_fields/Doc"; import { ScriptField } from "../../../new_fields/ScriptField"; +import { positionSchema } from "./CollectionFreeFormDocumentView"; var path = require('path'); -type VideoDocument = makeInterface<[typeof documentSchema, typeof pageSchema]>; -const VideoDocument = makeInterface(documentSchema, pageSchema); +export const timeSchema = createSchema({ + currentTimecode: "number", +}); +type VideoDocument = makeInterface<[typeof documentSchema, typeof positionSchema, typeof timeSchema]>; +const VideoDocument = makeInterface(documentSchema, positionSchema, timeSchema); library.add(faVideo); @@ -97,11 +100,11 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD } @action public Snapshot() { - let width = NumCast(this.props.Document.width); - let height = NumCast(this.props.Document.height); + let width = this.Document.width || 0; + let height = this.Document.height || 0; var canvas = document.createElement('canvas'); canvas.width = 640; - canvas.height = 640 * NumCast(this.props.Document.nativeHeight) / NumCast(this.props.Document.nativeWidth); + canvas.height = 640 * (this.Document.nativeHeight || 0) / (this.Document.nativeWidth || 1); var ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions if (ctx) { ctx.rect(0, 0, canvas.width, canvas.height); @@ -112,24 +115,25 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD if (!this._videoRef) { // can't find a way to take snapshots of videos let b = Docs.Create.ButtonDocument({ - x: NumCast(this.props.Document.x) + width, y: NumCast(this.props.Document.y), - width: 150, height: 50, title: NumCast(this.props.Document.curPage).toString() + x: (this.Document.x || 0) + width, y: (this.Document.y || 0), + width: 150, height: 50, title: (this.Document.currentTimecode || 0).toString() }); - b.onClick = ScriptField.MakeScript(`this.curPage = ${NumCast(this.props.Document.curPage)}`); + b.onClick = ScriptField.MakeScript(`this.currentTimecode = ${(this.Document.currentTimecode || 0)}`); } else { //convert to desired file format var dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png' // if you want to preview the captured image, - let filename = encodeURIComponent("snapshot" + this.props.Document.title + "_" + this.props.Document.curPage).replace(/\./g, ""); - VideoBox.convertDataUri(dataUrl, filename).then(returnedFilename => { + let filename = path.basename(encodeURIComponent("snapshot" + this.Document.title + "_" + (this.Document.currentTimecode || 0).toString())); + VideoBox.convertDataUri(dataUrl, filename.replace(/\..*$/, "")).then(returnedFilename => { if (returnedFilename) { let url = this.choosePath(Utils.prepend(returnedFilename)); let imageSummary = Docs.Create.ImageDocument(url, { - x: NumCast(this.props.Document.x) + width, y: NumCast(this.props.Document.y), - width: 150, height: height / width * 150, title: "--snapshot" + NumCast(this.props.Document.curPage) + " image-" + x: (this.Document.x || 0) + width, y: (this.Document.y || 0), + width: 150, height: height / width * 150, title: "--snapshot" + (this.Document.currentTimecode || 0) + " image-" }); + imageSummary.isButton = true; this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.addDocument && this.props.ContainingCollectionView.props.addDocument(imageSummary, false); - DocUtils.MakeLink(imageSummary, this.props.Document); + DocUtils.MakeLink({ doc: imageSummary }, { doc: this.props.Document }, "snapshot from " + this.Document.title, "video frame snapshot"); } }); } @@ -137,8 +141,8 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD @action updateTimecode = () => { - this.player && (this.props.Document.curPage = this.player.currentTime); - this._youtubePlayer && (this.props.Document.curPage = this._youtubePlayer.getCurrentTime()); + this.player && (this.Document.currentTimecode = this.player.currentTime); + this._youtubePlayer && (this.Document.currentTimecode = this._youtubePlayer.getCurrentTime()); } componentDidMount() { @@ -146,12 +150,12 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD if (this.youtubeVideoId) { let youtubeaspect = 400 / 315; - var nativeWidth = FieldValue(this.Document.nativeWidth, 0); - var nativeHeight = FieldValue(this.Document.nativeHeight, 0); + var nativeWidth = (this.Document.nativeWidth || 0); + var nativeHeight = (this.Document.nativeHeight || 0); if (!nativeWidth || !nativeHeight) { if (!this.Document.nativeWidth) this.Document.nativeWidth = 600; this.Document.nativeHeight = this.Document.nativeWidth / youtubeaspect; - this.Document.height = FieldValue(this.Document.width, 0) / youtubeaspect; + this.Document.height = (this.Document.width || 0) / youtubeaspect; } } } @@ -168,10 +172,9 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD if (vref) { this._videoRef!.ontimeupdate = this.updateTimecode; vref.onfullscreenchange = action((e) => this._fullScreen = vref.webkitDisplayingFullscreen); - if (this._reactionDisposer) this._reactionDisposer(); - this._reactionDisposer = reaction(() => this.props.Document.curPage, () => - !this.Playing && (vref.currentTime = this.Document.curPage || 0) - , { fireImmediately: true }); + this._reactionDisposer && this._reactionDisposer(); + this._reactionDisposer = reaction(() => this.Document.currentTimecode || 0, + time => !this.Playing && (vref.currentTime = time), { fireImmediately: true }); } } @@ -242,7 +245,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD let onYoutubePlayerReady = (event: any) => { this._reactionDisposer && this._reactionDisposer(); this._youtubeReactionDisposer && this._youtubeReactionDisposer(); - this._reactionDisposer = reaction(() => this.props.Document.curPage, () => !this.Playing && this.Seek(this.Document.curPage || 0)); + this._reactionDisposer = reaction(() => this.Document.currentTimecode, () => !this.Playing && this.Seek(this.Document.currentTimecode || 0)); this._youtubeReactionDisposer = reaction(() => [this.props.isSelected(), DocumentDecorations.Instance.Interacting, InkingControl.Instance.selectedTool], () => { let interactive = InkingControl.Instance.selectedTool === InkTool.None && this.props.isSelected() && !DocumentDecorations.Instance.Interacting; iframe.style.pointerEvents = interactive ? "all" : "none"; @@ -263,9 +266,9 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD this._youtubeIframeId = VideoBox._youtubeIframeCounter++; this._youtubeContentCreated = this._forceCreateYouTubeIFrame ? true : true; let style = "videoBox-content-YouTube" + (this._fullScreen ? "-fullScreen" : ""); - let start = untracked(() => Math.round(NumCast(this.props.Document.curPage))); + let start = untracked(() => Math.round(this.Document.currentTimecode || 0)); return <iframe key={this._youtubeIframeId} id={`${this.youtubeVideoId + this._youtubeIframeId}-player`} - onLoad={this.youtubeIframeLoaded} className={`${style}`} width={NumCast(this.props.Document.nativeWidth, 640)} height={NumCast(this.props.Document.nativeHeight, 390)} + onLoad={this.youtubeIframeLoaded} className={`${style}`} width={(this.Document.nativeWidth || 640)} height={(this.Document.nativeHeight || 390)} src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=1&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._showControls ? 1 : 0}`} ></iframe>; } |
