diff options
Diffstat (limited to 'src/client/views/nodes')
36 files changed, 1755 insertions, 612 deletions
diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss index 53b54d7e4..c959b79f5 100644 --- a/src/client/views/nodes/AudioBox.scss +++ b/src/client/views/nodes/AudioBox.scss @@ -1,141 +1,169 @@ -.audiobox-container, .audiobox-container-interactive { +.audiobox-container, +.audiobox-container-interactive { width: 100%; height: 100%; position: inherit; - display:flex; + display: flex; pointer-events: all; - cursor:default; + cursor: default; + .audiobox-buttons { display: flex; width: 100%; align-items: center; } + .audiobox-handle { - width:20px; - height:100%; - display:inline-block; + width: 20px; + height: 100%; + display: inline-block; } - .audiobox-control, .audiobox-control-interactive { - top:0; + + .audiobox-control, + .audiobox-control-interactive { + top: 0; max-height: 32px; width: 100%; - display:inline-block; + display: inline-block; pointer-events: none; } + .audiobox-control-interactive { pointer-events: all; } + .audiobox-record { pointer-events: all; - width:100%; - height:100%; + width: 100%; + height: 100%; position: relative; pointer-events: none; } + .audiobox-record-interactive { pointer-events: all; - width:100%; - height:100%; + width: 100%; + height: 100%; position: relative; } + .audiobox-controls { - width:100%; - height:100%; + width: 100%; + height: 100%; position: relative; display: flex; padding-left: 2px; + .audiobox-player { - margin-top:auto; - margin-bottom:auto; - width:100%; + margin-top: auto; + margin-bottom: auto; + width: 100%; height: 80%; position: relative; padding-right: 5px; display: flex; - .audiobox-playhead, .audiobox-dictation { + + .audiobox-playhead, + .audiobox-dictation { position: relative; margin-top: auto; margin-bottom: auto; width: 25px; padding: 2px; } + .audiobox-dictation { align-items: center; display: inherit; background: dimgray; } + .audiobox-timeline { - position:relative; - height:100%; - width:100%; + position: relative; + height: 100%; + width: 100%; background: white; border: gray solid 1px; border-radius: 3px; + .audiobox-current { width: 1px; - height:100%; + height: 100%; background-color: red; position: absolute; } - .audiobox-linker, .audiobox-linker-mini { - position:absolute; - width:15px; - min-height:10px; - height:15px; - margin-left:-2.55px; - background:gray; + + .audiobox-linker, + .audiobox-linker-mini { + position: absolute; + width: 15px; + min-height: 10px; + height: 15px; + margin-left: -2.55px; + background: gray; border-radius: 100%; - opacity:0.9; + opacity: 0.9; background-color: transparent; box-shadow: black 2px 2px 1px; + .linkAnchorBox-cont { position: relative !important; - height: 100% !important; + height: 100% !important; width: 100% !important; - left:unset !important; - top:unset !important; + left: unset !important; + top: unset !important; } } + .audiobox-linker-mini { - width:8px; - min-height:8px; - height:8px; + width: 8px; + min-height: 8px; + height: 8px; box-shadow: black 1px 1px 1px; margin-left: -1; margin-top: -2; + .linkAnchorBox-cont { position: relative !important; - height: 100% !important; + height: 100% !important; width: 100% !important; - left:unset !important; - top:unset !important; + left: unset !important; + top: unset !important; } } - .audiobox-linker:hover, .audiobox-linker-mini:hover { - opacity:1; + + .audiobox-linker:hover, + .audiobox-linker-mini:hover { + opacity: 1; } - .audiobox-marker-container, .audiobox-marker-minicontainer { - position:absolute; - width:10px; - height:90%; - top:2.5%; - background:gray; + + .audiobox-marker-container, + .audiobox-marker-minicontainer { + position: absolute; + width: 10px; + height: 90%; + top: 2.5%; + background: gray; border-radius: 5px; box-shadow: black 2px 2px 1px; + .audiobox-marker { - position:relative; + position: relative; height: calc(100% - 15px); margin-top: 15px; } + .audio-marker:hover { border: orange 2px solid; } } + .audiobox-marker-minicontainer { - width:5px; + width: 5px; border-radius: 1px; + .audiobox-marker { - position:relative; + position: relative; height: calc(100% - 8px); margin-top: 8px; } @@ -143,4 +171,27 @@ } } } +} + + +@media only screen and (max-device-width: 480px) { + .audiobox-dictation { + font-size: 5em; + display: flex; + width: 100; + justify-content: center; + flex-direction: column; + align-items: center; + } + + .audiobox-container .audiobox-record, + .audiobox-container-interactive .audiobox-record { + font-size: 3em; + } + + .audiobox-container .audiobox-controls .audiobox-player .audiobox-playhead, + .audiobox-container .audiobox-controls .audiobox-player .audiobox-dictation, + .audiobox-container-interactive .audiobox-controls .audiobox-player .audiobox-playhead { + width: 70px; + } }
\ No newline at end of file diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index 1a935d9b0..d5288fff6 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -161,7 +161,7 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument const funcs: ContextMenuProps[] = []; funcs.push({ description: (this.layoutDoc.playOnSelect ? "Don't play" : "Play") + " when document selected", event: () => this.layoutDoc.playOnSelect = !this.layoutDoc.playOnSelect, icon: "expand-arrows-alt" }); - ContextMenu.Instance.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); + ContextMenu.Instance?.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); } stopRecording = action(() => { @@ -193,7 +193,7 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument const newDoc = Docs.Create.TextDocument("", { title: "", _chromeStatus: "disabled", x: NumCast(this.props.Document.x), y: NumCast(this.props.Document.y) + NumCast(this.props.Document._height) + 10, - _width: NumCast(this.props.Document._width), _height: 3 * NumCast(this.props.Document._height) + _width: NumCast(this.props.Document._width), _height: 2 * NumCast(this.props.Document._height) }); Doc.GetProto(newDoc).recordingSource = this.dataDoc; Doc.GetProto(newDoc).recordingStart = ComputedField.MakeFunction(`self.recordingSource["${this.props.fieldKey}-recordingStart"]`); @@ -228,7 +228,7 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument {!this.path ? <div className="audiobox-buttons"> <div className="audiobox-dictation" onClick={this.onFile}> - <FontAwesomeIcon style={{ width: "30px", background: this.layoutDoc.playOnSelect ? "yellow" : "dimGray" }} icon="file-alt" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /> + <FontAwesomeIcon style={{ width: "30px", background: this.layoutDoc.playOnSelect ? "yellow" : "rgba(0,0,0,0)" }} icon="file-alt" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /> </div> <button className={`audiobox-record${interactive}`} style={{ backgroundColor: this.audioState === "recording" ? "red" : "black" }}> {this.audioState === "recording" ? "STOP" : "RECORD"} diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 404d69730..a3020f912 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -22,7 +22,7 @@ export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { zIndex?: number; highlight?: boolean; jitterRotation: number; - transition?: string; + dataTransition?: string; fitToBox?: boolean; replica: string; } @@ -60,10 +60,10 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF const someView = Cast(this.props.Document.someView, Doc); const minimap = Cast(this.props.Document.minimap, Doc); if (someView instanceof Doc && minimap instanceof Doc) { - const x = (NumCast(someView._panX) - NumCast(someView._width) / 2 / NumCast(someView.scale) - (NumCast(minimap.fitX) - NumCast(minimap.fitW) / 2)) / NumCast(minimap.fitW) * NumCast(minimap._width) - NumCast(minimap._width) / 2; - const y = (NumCast(someView._panY) - NumCast(someView._height) / 2 / NumCast(someView.scale) - (NumCast(minimap.fitY) - NumCast(minimap.fitH) / 2)) / NumCast(minimap.fitH) * NumCast(minimap._height) - NumCast(minimap._height) / 2; - const w = NumCast(someView._width) / NumCast(someView.scale) / NumCast(minimap.fitW) * NumCast(minimap.width); - const h = NumCast(someView._height) / NumCast(someView.scale) / NumCast(minimap.fitH) * NumCast(minimap.height); + const x = (NumCast(someView._panX) - NumCast(someView._width) / 2 / NumCast(someView._viewScale) - (NumCast(minimap.fitX) - NumCast(minimap.fitW) / 2)) / NumCast(minimap.fitW) * NumCast(minimap._width) - NumCast(minimap._width) / 2; + const y = (NumCast(someView._panY) - NumCast(someView._height) / 2 / NumCast(someView._viewScale) - (NumCast(minimap.fitY) - NumCast(minimap.fitH) / 2)) / NumCast(minimap.fitH) * NumCast(minimap._height) - NumCast(minimap._height) / 2; + const w = NumCast(someView._width) / NumCast(someView._viewScale) / NumCast(minimap.fitW) * NumCast(minimap.width); + const h = NumCast(someView._height) / NumCast(someView._viewScale) / NumCast(minimap.fitH) * NumCast(minimap.height); return { x: x, y: y, width: w, height: h }; } } @@ -81,9 +81,15 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF public static setValues(time: number, d: Doc, x?: number, y?: number, opacity?: number) { const timecode = Math.round(time); - Cast(d["x-indexed"], listSpec("number"), [])[timecode] = x as any as number; - Cast(d["y-indexed"], listSpec("number"), [])[timecode] = y as any as number; - Cast(d["opacity-indexed"], listSpec("number"), null)[timecode] = opacity as any as number; + const xindexed = Cast(d["x-indexed"], listSpec("number"), []).slice(); + const yindexed = Cast(d["y-indexed"], listSpec("number"), []).slice(); + const oindexed = Cast(d["opacity-indexed"], listSpec("number"), []).slice(); + xindexed[timecode] = x as any as number; + yindexed[timecode] = y as any as number; + oindexed[timecode] = opacity as any as number; + d["x-indexed"] = new List<number>(xindexed); + d["y-indexed"] = new List<number>(yindexed); + d["opacity-indexed"] = new List<number>(oindexed); } public static updateKeyframe(docs: Doc[], time: number) { const timecode = Math.round(time); @@ -94,14 +100,14 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF xindexed?.length <= timecode + 1 && xindexed.push(undefined as any as number); yindexed?.length <= timecode + 1 && yindexed.push(undefined as any as number); opacityindexed?.length <= timecode + 1 && opacityindexed.push(undefined as any as number); - doc.transition = "all 1s"; + doc.dataTransition = "all 1s"; }); - setTimeout(() => docs.forEach(doc => doc.transition = "inherit"), 1010); + setTimeout(() => docs.forEach(doc => doc.dataTransition = "inherit"), 1010); } public static gotoKeyframe(docs: Doc[]) { - docs.forEach(doc => doc.transition = "all 1s"); - setTimeout(() => docs.forEach(doc => doc.transition = "inherit"), 1010); + docs.forEach(doc => doc.dataTransition = "all 1s"); + setTimeout(() => docs.forEach(doc => doc.dataTransition = "inherit"), 1010); } public static setupKeyframes(docs: Doc[], timecode: number, progressivize: boolean = false) { @@ -119,7 +125,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF doc.x = ComputedField.MakeInterpolated("x", "activeFrame"); doc.y = ComputedField.MakeInterpolated("y", "activeFrame"); doc.opacity = ComputedField.MakeInterpolated("opacity", "activeFrame"); - doc.transition = "inherit"; + doc.dataTransition = "inherit"; }); } @@ -150,7 +156,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF borderRadius: StrCast(Doc.Layout(this.layoutDoc).borderRounding), outline: this.Highlight ? "orange solid 2px" : "", transform: this.transform, - transition: this.props.transition ? this.props.transition : this.dataProvider ? this.dataProvider.transition : StrCast(this.layoutDoc.transition), + transition: this.props.dataTransition ? this.props.dataTransition : this.dataProvider ? this.dataProvider.transition : StrCast(this.layoutDoc.dataTransition), width: this.props.Document.isInkMask ? 5000 : this.width, height: this.props.Document.isInkMask ? 5000 : this.height, zIndex: this.ZInd, diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx index d6d9a8cfd..b3a9b6fee 100644 --- a/src/client/views/nodes/ColorBox.tsx +++ b/src/client/views/nodes/ColorBox.tsx @@ -39,7 +39,7 @@ export class ColorBox extends ViewBoxBaseComponent<FieldViewProps, ColorDocument if (view.props.LayoutTemplate?.() || view.props.LayoutTemplateString) { // this situation typically occurs when you have a link dot targetDoc.backgroundColor = Doc.UserDoc().backgroundColor; // bcz: don't know how to change the color of an inline template... } - else if (StrCast(Doc.Layout(view.props.Document).layout).includes("FormattedTextBox") && FormattedTextBox.HadSelection) { + else if (StrCast(Doc.Layout(view.props.Document).layout).includes("FormattedTextBox") && window.getSelection()?.toString() !== "") { Doc.Layout(view.props.Document)[Doc.LayoutFieldKey(view.props.Document) + "-color"] = Doc.UserDoc().backgroundColor; } else { Doc.Layout(view.props.Document)._backgroundColor = Doc.UserDoc().backgroundColor; // '_backgroundColor' is template specific. 'backgroundColor' would apply to all templates, but has no UI at the moment @@ -71,4 +71,4 @@ export class ColorBox extends ViewBoxBaseComponent<FieldViewProps, ColorDocument </div> </div>; } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/ComparisonBox.scss b/src/client/views/nodes/ComparisonBox.scss index 810a824cf..acf6b1636 100644 --- a/src/client/views/nodes/ComparisonBox.scss +++ b/src/client/views/nodes/ComparisonBox.scss @@ -3,7 +3,7 @@ border-radius: inherit; width: 100%; height: 100%; - position: absolute; + position: relative; z-index: 0; pointer-events: none; diff --git a/src/client/views/nodes/DocumentLinksButton.scss b/src/client/views/nodes/DocumentLinksButton.scss index 484f8c469..b58603978 100644 --- a/src/client/views/nodes/DocumentLinksButton.scss +++ b/src/client/views/nodes/DocumentLinksButton.scss @@ -23,7 +23,7 @@ &:hover { background: deepskyblue; transform: scale(1.05); - cursor: default; + cursor: grab; } } .documentLinksButton { diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index f35b48ce4..bfd860f65 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -10,6 +10,7 @@ import React = require("react"); import { DocUtils } from "../../documents/Documents"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { LinkDocPreview } from "./LinkDocPreview"; +import { LinkCreatedBox } from "./LinkCreatedBox"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -18,11 +19,14 @@ interface DocumentLinksButtonProps { View: DocumentView; Offset?: number[]; AlwaysOn?: boolean; + InMenu?: boolean; } @observer export class DocumentLinksButton extends React.Component<DocumentLinksButtonProps, {}> { private _linkButton = React.createRef<HTMLDivElement>(); + @observable public static StartLink: DocumentView | undefined; + @action onLinkButtonMoved = (e: PointerEvent) => { if (this._linkButton.current !== null) { @@ -50,30 +54,73 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp return false; } - @observable static StartLink: DocumentView | undefined; onLinkButtonDown = (e: React.PointerEvent): void => { setupMoveUpEvents(this, e, this.onLinkButtonMoved, emptyFunction, action((e, doubleTap) => { - if (doubleTap) { + if (doubleTap && this.props.InMenu) { + //action(() => Doc.BrushDoc(this.props.View.Document)); DocumentLinksButton.StartLink = this.props.View; - } else { + } else if (!!!this.props.InMenu) { DocumentLinksButton.EditLink = this.props.View; DocumentLinksButton.EditLinkLoc = [e.clientX + 10, e.clientY]; } })); } + + @action + onLinkClick = (e: React.MouseEvent): void => { + if (this.props.InMenu) { + DocumentLinksButton.StartLink = this.props.View; + //action(() => Doc.BrushDoc(this.props.View.Document)); + } else if (!!!this.props.InMenu) { + DocumentLinksButton.EditLink = this.props.View; + DocumentLinksButton.EditLinkLoc = [e.clientX + 10, e.clientY]; + } + } + + @action completeLink = (e: React.PointerEvent): void => { setupMoveUpEvents(this, e, returnFalse, emptyFunction, action((e, doubleTap) => { if (doubleTap) { if (DocumentLinksButton.StartLink === this.props.View) { DocumentLinksButton.StartLink = undefined; + // action((e: React.PointerEvent<HTMLDivElement>) => { + // Doc.UnBrushDoc(this.props.View.Document); + // }); } else { DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View && DocUtils.MakeLink({ doc: DocumentLinksButton.StartLink.props.Document }, { doc: this.props.View.props.Document }, "long drag"); + + runInAction(() => { + LinkCreatedBox.popupX = e.screenX; + LinkCreatedBox.popupY = e.screenY - 120; + LinkCreatedBox.linkCreated = true; + setTimeout(action(() => { LinkCreatedBox.linkCreated = false; }), 2500); + }); } } })); } + @action + finishLinkClick = (e: React.MouseEvent) => { + if (DocumentLinksButton.StartLink === this.props.View) { + DocumentLinksButton.StartLink = undefined; + // action((e: React.PointerEvent<HTMLDivElement>) => { + // Doc.UnBrushDoc(this.props.View.Document); + // }); + } else { + DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View && + DocUtils.MakeLink({ doc: DocumentLinksButton.StartLink.props.Document }, { doc: this.props.View.props.Document }, "long drag"); + + runInAction(() => { + LinkCreatedBox.popupX = e.screenX; + LinkCreatedBox.popupY = e.screenY - 120; + LinkCreatedBox.linkCreated = true; + setTimeout(action(() => { LinkCreatedBox.linkCreated = false; }), 2500); + }); + } + } + @observable public static EditLink: DocumentView | undefined; public static EditLinkLoc: number[] = [0, 0]; @@ -82,23 +129,30 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp get linkButton() { const links = DocListCast(this.props.View.props.Document.links); return (!links.length || links[0].hidden) && !this.props.AlwaysOn ? (null) : - <div title="Drag(create link) Tap(view links)" ref={this._linkButton} style={{ position: "absolute", left: this.props.Offset?.[0] }}> - <div className={"documentLinksButton"} style={{ backgroundColor: DocumentLinksButton.StartLink ? "transparent" : "" }} - onPointerDown={this.onLinkButtonDown} - onPointerLeave={action(() => LinkDocPreview.LinkInfo = undefined)} - onPointerEnter={action(e => LinkDocPreview.LinkInfo = { - addDocTab: this.props.View.props.addDocTab, - linkSrc: this.props.View.props.Document, - linkDoc: links[0], - Location: [e.clientX, e.clientY + 20] - })} > - {links.length ? links.length : <FontAwesomeIcon className="documentdecorations-icon" icon="link" size="sm" />} + <div title="Drag(create link) Tap(view links)" ref={this._linkButton} style={{ minWidth: 20, minHeight: 20, position: "absolute", left: this.props.Offset?.[0] }}> + <div className={"documentLinksButton"} style={{ + backgroundColor: DocumentLinksButton.StartLink ? "transparent" : "", + width: this.props.InMenu ? "20px" : "30px", height: this.props.InMenu ? "20px" : "30px", fontWeight: "bold" + }} + onPointerDown={this.onLinkButtonDown} onClick={this.onLinkClick} + // onPointerLeave={action(() => LinkDocPreview.LinkInfo = undefined)} + // onPointerEnter={action(e => links.length && (LinkDocPreview.LinkInfo = { + // addDocTab: this.props.View.props.addDocTab, + // linkSrc: this.props.View.props.Document, + // linkDoc: links[0], + // Location: [e.clientX, e.clientY + 20] + // }))} + > + {links.length && !!!this.props.InMenu ? links.length : <FontAwesomeIcon className="documentdecorations-icon" icon="link" size="sm" />} </div> - {DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View ? <div className={"documentLinksButton-endLink"} onPointerDown={this.completeLink} /> : (null)} - {DocumentLinksButton.StartLink === this.props.View ? <div className={"documentLinksButton-startLink"} /> : (null)} + {DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View ? <div className={"documentLinksButton-endLink"} + style={{ width: this.props.InMenu ? "20px" : "30px", height: this.props.InMenu ? "20px" : "30px" }} + onPointerDown={this.completeLink} onClick={e => this.finishLinkClick(e)} /> : (null)} + {DocumentLinksButton.StartLink === this.props.View ? <div className={"documentLinksButton-startLink"} + style={{ width: this.props.InMenu ? "20px" : "30px", height: this.props.InMenu ? "20px" : "30px" }} /> : (null)} </div>; } render() { return this.linkButton; } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index b7726f7ba..b978f6245 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -42,6 +42,17 @@ width:10px !important; } } + .documentView-treeView { + max-height: 1.5em; + text-overflow: ellipsis; + display: inline-block; + white-space: pre; + width: 100%; + overflow: hidden; + > .documentView-node { + position: absolute; + } + } .documentView-lock { width: 20; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 8f139e39e..b38db9a1e 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -29,7 +29,6 @@ import { SelectionManager } from "../../util/SelectionManager"; import SharingManager from '../../util/SharingManager'; import { Transform } from "../../util/Transform"; import { undoBatch, UndoManager } from "../../util/UndoManager"; -import { CollectionDockingView } from "../collections/CollectionDockingView"; import { CollectionView, CollectionViewType } from '../collections/CollectionView'; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from '../ContextMenuItem'; @@ -42,6 +41,8 @@ import { LinkAnchorBox } from './LinkAnchorBox'; import { RadialMenu } from './RadialMenu'; import React = require("react"); import { DocumentLinksButton } from './DocumentLinksButton'; +import { MobileInterface } from '../../../mobile/MobileInterface'; +import { LinkCreatedBox } from './LinkCreatedBox'; library.add(fa.faEdit, fa.faTrash, fa.faShare, fa.faDownload, fa.faExpandArrowsAlt, fa.faCompressArrowsAlt, fa.faLayerGroup, fa.faExternalLinkAlt, fa.faAlignCenter, fa.faCaretSquareRight, fa.faSquare, fa.faConciergeBell, fa.faWindowRestore, fa.faFolder, fa.faMapPin, fa.faLink, fa.faFingerprint, fa.faCrosshairs, fa.faDesktop, fa.faUnlock, fa.faLock, fa.faLaptopCode, fa.faMale, @@ -69,7 +70,7 @@ export interface DocumentViewProps { onDoubleClick?: ScriptField; onPointerDown?: ScriptField; onPointerUp?: ScriptField; - treeViewId?: string; + treeViewDoc?: Doc; dropAction?: dropActionType; dragDivName?: string; nudge?: (x: number, y: number) => void; @@ -116,6 +117,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; private holdDisposer?: InteractionUtils.MultiTouchEventDisposer; + public get title() { return this.props.Document.title; } public get displayName() { return "DocumentView(" + this.props.Document.title + ")"; } // this makes mobx trace() statements more descriptive public get ContentDiv() { return this._mainCont.current; } get active() { return SelectionManager.IsSelected(this, true) || this.props.parentActive(true); } @@ -123,7 +125,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @computed get freezeDimensions() { return this.props.FreezeDimensions; } @computed get nativeWidth() { return NumCast(this.layoutDoc._nativeWidth, this.props.NativeWidth() || (this.freezeDimensions ? this.layoutDoc[WidthSym]() : 0)); } @computed get nativeHeight() { return NumCast(this.layoutDoc._nativeHeight, this.props.NativeHeight() || (this.freezeDimensions ? this.layoutDoc[HeightSym]() : 0)); } - @computed get onClickHandler() { return this.props.onClick || Cast(this.layoutDoc.onClick, ScriptField, null); } + @computed get onClickHandler() { return this.props.onClick || Cast(this.Document.onClick, ScriptField, Cast(this.layoutDoc.onClick, ScriptField, null)); } @computed get onDoubleClickHandler() { return this.props.onDoubleClick || Cast(this.layoutDoc.onDoubleClick, ScriptField, null) || this.Document.onDoubleClick; } @computed get onPointerDownHandler() { return this.props.onPointerDown ? this.props.onPointerDown : this.Document.onPointerDown; } @computed get onPointerUpHandler() { return this.props.onPointerUp ? this.props.onPointerUp : this.Document.onPointerUp; } @@ -181,10 +183,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1]; RadialMenu.Instance.openMenu(pt.pageX - 15, pt.pageY - 15); - RadialMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "onRight"), icon: "map-pin", selected: -1 }); - RadialMenu.Instance.addItem({ description: "Delete this document", event: () => { this.props.ContainingCollectionView?.removeDocument(this.props.Document), RadialMenu.Instance.closeMenu(); }, icon: "layer-group", selected: -1 }); - RadialMenu.Instance.addItem({ description: "Open in a new tab", event: () => this.props.addDocTab(this.props.Document, "onRight"), icon: "trash", selected: -1 }); - RadialMenu.Instance.addItem({ description: "Pin to Presentation", event: () => this.props.pinToPres(this.props.Document), icon: "folder", selected: -1 }); + // RadialMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "onRight"), icon: "map-pin", selected: -1 }); + RadialMenu.Instance.addItem({ description: "Delete", event: () => { this.props.ContainingCollectionView?.removeDocument(this.props.Document), RadialMenu.Instance.closeMenu(); }, icon: "external-link-square-alt", selected: -1 }); + // RadialMenu.Instance.addItem({ description: "Open in a new tab", event: () => this.props.addDocTab(this.props.Document, "onRight"), icon: "trash", selected: -1 }); + RadialMenu.Instance.addItem({ description: "Pin", event: () => this.props.pinToPres(this.props.Document), icon: "map-pin", selected: -1 }); + RadialMenu.Instance.addItem({ description: "Open", event: () => MobileInterface.Instance.handleClick(this.props.Document), icon: "trash", selected: -1 }); SelectionManager.DeselectAll(); } @@ -237,7 +240,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu dragData.removeDocument = this.props.removeDocument; dragData.moveDocument = this.props.moveDocument;// this.layoutDoc.onDragStart ? undefined : this.props.moveDocument; dragData.dragDivName = this.props.dragDivName; - dragData.treeViewId = this.props.treeViewId; + dragData.treeViewDoc = this.props.treeViewDoc; DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { hideSource: !dropAction && !this.layoutDoc.onDragStart }); } } @@ -345,12 +348,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu // depending on the followLinkLocation property of the source (or the link itself as a fallback); followLinkClick = async (altKey: boolean, ctrlKey: boolean, shiftKey: boolean) => { const batch = UndoManager.StartBatch("follow link click"); - // open up target if it's not already in view ... + // open up target if it's not already in view ... const createViewFunc = (doc: Doc, followLoc: string, finished: Opt<() => void>) => { const targetFocusAfterDocFocus = () => { const where = StrCast(this.Document.followLinkLocation) || followLoc; const hackToCallFinishAfterFocus = () => { - finished && setTimeout(finished, 0); // finished() needs to be called right after hackToCallFinishAfterFocus(), but there's no callback for that so we use the hacky timeout. + finished && setTimeout(finished, 0); // finished() needs to be called right after hackToCallFinishAfterFocus(), but there's no callback for that so we use the hacky timeout. return false; // we must return false here so that the zoom to the document is not reversed. If it weren't for needing to call finished(), we wouldn't need this function at all since not having it is equivalent to returning false }; this.props.addDocTab(doc, where) && this.props.focus(doc, BoolCast(this.Document.followLinkZoom, true), undefined, hackToCallFinishAfterFocus); // add the target and focus on it. @@ -374,7 +377,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu this._downX = touch.clientX; this._downY = touch.clientY; if (!e.nativeEvent.cancelBubble) { - if ((this.active || this.layoutDoc.onDragStart || this.layoutDoc.onClick) && !e.ctrlKey && !this.layoutDoc.lockedPosition && !this.layoutDoc.inOverlay) e.stopPropagation(); + if ((this.active || this.layoutDoc.onDragStart || this.onClickHandler) && !e.ctrlKey && !this.layoutDoc.lockedPosition && !this.layoutDoc.inOverlay) e.stopPropagation(); this.removeMoveListeners(); this.addMoveListeners(); this.removeEndListeners(); @@ -389,11 +392,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu if (e.cancelBubble && this.active) { this.removeMoveListeners(); } - else if (!e.cancelBubble && (SelectionManager.IsSelected(this, true) || this.props.parentActive(true) || this.layoutDoc.onDragStart || this.layoutDoc.onClick) && !this.layoutDoc.lockedPosition && !this.layoutDoc.inOverlay) { + else if (!e.cancelBubble && (SelectionManager.IsSelected(this, true) || this.props.parentActive(true) || this.layoutDoc.onDragStart || this.onClickHandler) && !this.layoutDoc.lockedPosition && !this.layoutDoc.inOverlay) { const touch = me.touchEvent.changedTouches.item(0); if (touch && (Math.abs(this._downX - touch.clientX) > 3 || Math.abs(this._downY - touch.clientY) > 3)) { - if (!e.altKey && (!this.topMost || this.layoutDoc.onDragStart || this.layoutDoc.onClick)) { + if (!e.altKey && (!this.topMost || this.layoutDoc.onDragStart || this.onClickHandler)) { this.cleanUpInteractions(); this.startDragging(this._downX, this._downY, this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined); } @@ -582,12 +585,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @undoBatch toggleLinkButtonBehavior = (): void => { - if (this.Document.isLinkButton || this.layoutDoc.onClick || this.Document.ignoreClick) { + if (this.Document.isLinkButton || this.onClickHandler || this.Document.ignoreClick) { this.Document.isLinkButton = false; const first = DocListCast(this.Document.links).find(d => d instanceof Doc); first && (first.hidden = false); this.Document.ignoreClick = false; - this.layoutDoc.onClick = undefined; + this.Document.onClick = this.layoutDoc.onClick = undefined; } else { this.Document.isLinkButton = true; const first = DocListCast(this.Document.links).find(d => d instanceof Doc); @@ -639,12 +642,27 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu e.stopPropagation(); de.complete.annoDragData.linkedToDoc = true; + runInAction(() => { + LinkCreatedBox.popupX = de.x; + LinkCreatedBox.popupY = de.y; + LinkCreatedBox.linkCreated = true; + setTimeout(action(() => { LinkCreatedBox.linkCreated = false; }), 2500); + }); + DocUtils.MakeLink({ doc: de.complete.annoDragData.annotationDocument }, { doc: this.props.Document }, "link"); } if (de.complete.linkDragData) { e.stopPropagation(); // 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); + + runInAction(() => { + LinkCreatedBox.popupX = de.x; + LinkCreatedBox.popupY = de.y; + LinkCreatedBox.linkCreated = true; + setTimeout(action(() => { LinkCreatedBox.linkCreated = false; }), 2500); + }); + de.complete.linkDragData.linkSourceDocument !== this.props.Document && (de.complete.linkDragData.linkDocument = DocUtils.MakeLink({ doc: de.complete.linkDragData.linkSourceDocument }, { doc: this.props.Document }, `link`)); // TODODO this is where in text links get passed @@ -689,7 +707,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @undoBatch @action - setAcl = (acl: "readOnly" | "addOnly" | "ownerOnly") => { + setAcl = (acl: "readOnly" | "addOnly" | "ownerOnly" | "write") => { this.dataDoc.ACL = this.props.Document.ACL = acl; DocListCast(this.dataDoc[Doc.LayoutFieldKey(this.dataDoc)]).map(d => { if (d.author === Doc.CurrentUserEmail) d.ACL = acl; @@ -699,7 +717,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } @undoBatch @action - testAcl = (acl: "readOnly" | "addOnly" | "ownerOnly") => { + testAcl = (acl: "readOnly" | "addOnly" | "ownerOnly" | "write") => { this.dataDoc.author = this.props.Document.author = "ADMIN"; this.dataDoc.ACL = this.props.Document.ACL = acl; DocListCast(this.dataDoc[Doc.LayoutFieldKey(this.dataDoc)]).map(d => { @@ -724,17 +742,18 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu return; } e.persist(); - e?.stopPropagation(); + e.stopPropagation(); - if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3 || - e.isDefaultPrevented()) { - e.preventDefault(); + if (Math.abs(this._downX - e?.clientX) > 3 || Math.abs(this._downY - e?.clientY) > 3 || + e?.isDefaultPrevented()) { + e?.preventDefault(); return; } e.preventDefault(); } const cm = ContextMenu.Instance; + if (!cm) return; const customScripts = Cast(this.props.Document.contextMenuScripts, listSpec(ScriptField), []); Cast(this.props.Document.contextMenuLabels, listSpec("string"), []).forEach((label, i) => @@ -742,7 +761,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu this.props.contextMenuItems?.().forEach(item => cm.addItem({ description: item.label, event: () => item.script.script.run({ this: this.layoutDoc, self: this.rootDoc }), icon: "sticky-note" })); - let options = cm.findByDescription("Options..."); + const options = cm.findByDescription("Options..."); const optionItems: ContextMenuProps[] = options && "subitems" in options ? options.subitems : []; const templateDoc = Cast(this.props.Document[StrCast(this.props.Document.layoutKey)], Doc, null); templateDoc && optionItems.push({ description: "Open Template ", event: () => this.props.addDocTab(templateDoc, "onRight"), icon: "eye" }); @@ -752,11 +771,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const existingOnClick = cm.findByDescription("OnClick..."); const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; onClicks.push({ description: "Enter Portal", event: this.makeIntoPortal, icon: "window-restore" }); - onClicks.push({ description: "Toggle Detail", event: () => this.layoutDoc.onClick = ScriptField.MakeScript(`toggleDetail(self, "${this.Document.layoutKey}")`), icon: "window-restore" }); + onClicks.push({ description: "Toggle Detail", event: () => this.Document.onClick = ScriptField.MakeScript(`toggleDetail(self, "${this.Document.layoutKey}")`), icon: "window-restore" }); onClicks.push({ description: this.Document.ignoreClick ? "Select" : "Do Nothing", event: () => this.Document.ignoreClick = !this.Document.ignoreClick, icon: this.Document.ignoreClick ? "unlock" : "lock" }); onClicks.push({ description: this.Document.isLinkButton ? "Remove Follow Behavior" : "Follow Link in Place", event: this.toggleFollowInPlace, icon: "concierge-bell" }); onClicks.push({ description: this.Document.isLinkButton ? "Remove Follow Behavior" : "Follow Link on Right", event: this.toggleFollowOnRight, icon: "concierge-bell" }); - onClicks.push({ description: this.Document.isLinkButton || this.layoutDoc.onClick ? "Remove Click Behavior" : "Follow Link", event: this.toggleLinkButtonBehavior, icon: "concierge-bell" }); + onClicks.push({ description: this.Document.isLinkButton || this.onClickHandler ? "Remove Click Behavior" : "Follow Link", event: this.toggleLinkButtonBehavior, icon: "concierge-bell" }); onClicks.push({ description: "Edit onClick Script", event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick"), icon: "edit" }); !existingOnClick && cm.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" }); @@ -811,6 +830,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu aclItems.push({ description: "Make Add Only", event: () => this.setAcl("addOnly"), icon: "concierge-bell" }); aclItems.push({ description: "Make Read Only", event: () => this.setAcl("readOnly"), icon: "concierge-bell" }); aclItems.push({ description: "Make Private", event: () => this.setAcl("ownerOnly"), icon: "concierge-bell" }); + aclItems.push({ description: "Make Editable", event: () => this.setAcl("write"), icon: "concierge-bell" }); aclItems.push({ description: "Test Private", event: () => this.testAcl("ownerOnly"), icon: "concierge-bell" }); aclItems.push({ description: "Test Readonly", event: () => this.testAcl("readOnly"), icon: "concierge-bell" }); !existingAcls && cm.addItem({ description: "Privacy...", subitems: aclItems, icon: "question" }); @@ -874,7 +894,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu // DocumentViews should stop propagation of this event e.stopPropagation(); } - ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); + cm.displayMenu(e.pageX - 15, e.pageY - 15); !SelectionManager.IsSelected(this, true) && SelectionManager.SelectDoc(this, false); }); const path = this.props.LibraryPath.reduce((p: string, d: Doc) => p + "/" + (Doc.AreProtosEqual(d, (Doc.UserDoc()["tabs-button-library"] as Doc).sourcePanel as Doc) ? "" : d.title), ""); @@ -942,7 +962,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu recommendations.documentIconHeight = 150; recommendations.sourceDoc = this.props.Document; recommendations.sourceDocContext = this.props.ContainingCollectionView!.props.Document; - CollectionDockingView.AddRightSplit(recommendations, undefined); + this.props.addDocTab(recommendations, "onRight"); // RecommendationsBox.Instance.displayRecommendations(e.pageX + 100, e.pageY); } @@ -974,13 +994,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu body.href = urls[i]; bodies.push(body); } - CollectionDockingView.AddRightSplit(Docs.Create.SchemaDocument(headers, bodies, { title: `Showing External Recommendations for "${StrCast(doc.title)}"` }), undefined); + this.props.addDocTab(Docs.Create.SchemaDocument(headers, bodies, { title: `Showing External Recommendations for "${StrCast(doc.title)}"` }), "onRight"); this._showKPQuery = true; this._queries = kps.toString(); } // does Document set a layout prop - // does Document set a layout prop + // does Document set a layout prop setsLayoutProp = (prop: string) => this.props.Document[prop] !== this.props.Document["default" + prop[0].toUpperCase() + prop.slice(1)] && this.props.Document["default" + prop[0].toUpperCase() + prop.slice(1)]; // get the a layout prop by first choosing the prop from Document, then falling back to the layout doc otherwise. getLayoutPropStr = (prop: string) => StrCast(this.setsLayoutProp(prop) ? this.props.Document[prop] : this.layoutDoc[prop]); @@ -1071,30 +1091,34 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu anchorPanelHeight = () => this.props.PanelHeight() || 1; @computed get anchors() { TraceMobx(); - return this.props.forcedBackgroundColor?.(this.Document) === "transparent" || this.layoutDoc.presBox || this.props.dontRegisterView ? (null) : DocListCast(this.Document.links).filter(d => !d.hidden && this.isNonTemporalLink).map((d, i) => - <DocumentView {...this.props} key={i + 1} - Document={d} - ContainingCollectionView={this.props.ContainingCollectionView} - ContainingCollectionDoc={this.props.Document} // bcz: hack this.props.Document is not a collection Need a better prop for passing the containing document to the LinkAnchorBox - PanelWidth={this.anchorPanelWidth} - PanelHeight={this.anchorPanelHeight} - ContentScaling={returnOne} - dontRegisterView={false} - forcedBackgroundColor={returnTransparent} - removeDocument={this.hideLinkAnchor} - pointerEvents={false} - LayoutTemplate={undefined} - LayoutTemplateString={LinkAnchorBox.LayoutString(`anchor${Doc.LinkEndpoint(d, this.props.Document)}`)} - />); + return (this.props.treeViewDoc && this.props.LayoutTemplateString) || // render nothing for: tree view anchor dots + this.layoutDoc.presBox || // presentationbox nodes + this.props.dontRegisterView ? (null) : // view that are not registered + DocListCast(this.Document.links).filter(d => !d.hidden && this.isNonTemporalLink).map((d, i) => + <DocumentView {...this.props} key={i + 1} + Document={d} + ContainingCollectionView={this.props.ContainingCollectionView} + ContainingCollectionDoc={this.props.Document} // bcz: hack this.props.Document is not a collection Need a better prop for passing the containing document to the LinkAnchorBox + PanelWidth={this.anchorPanelWidth} + PanelHeight={this.anchorPanelHeight} + ContentScaling={returnOne} + dontRegisterView={false} + forcedBackgroundColor={returnTransparent} + removeDocument={this.hideLinkAnchor} + pointerEvents={false} + LayoutTemplate={undefined} + LayoutTemplateString={LinkAnchorBox.LayoutString(`anchor${Doc.LinkEndpoint(d, this.props.Document)}`)} + />); } @computed get innards() { TraceMobx(); - if (!this.props.PanelWidth()) { // this happens when the document is a tree view label - return <div className="documentView-linkAnchorBoxAnchor" > + if (this.props.treeViewDoc && !this.props.LayoutTemplateString) { // this happens when the document is a tree view label (but not an anchor dot) + return <div className="documentView-treeView" style={{ maxWidth: this.props.PanelWidth() || undefined }}> {StrCast(this.props.Document.title)} {this.anchors} </div>; } + const showTitle = StrCast(this.layoutDoc._showTitle); const showTitleHover = StrCast(this.layoutDoc._showTitleHover); const showCaption = StrCast(this.layoutDoc._showCaption); @@ -1156,7 +1180,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu renderLock() { return (this.Document.isBackground !== undefined || this.isSelected(false)) && ((this.Document.type === DocumentType.COL && this.Document._viewType !== CollectionViewType.Pile) || this.Document.type === DocumentType.IMG) && - this.props.renderDepth > 0 && this.props.PanelWidth() > 0 ? + this.props.renderDepth > 0 && !this.props.treeViewDoc ? <div className="documentView-lock" onClick={() => this.toggleBackground(true)}> <FontAwesomeIcon icon={this.Document.isBackground ? "unlock" : "lock"} style={{ color: this.Document.isBackground ? "red" : undefined }} size="lg" /> </div> @@ -1164,8 +1188,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } render() { - if (this.props.Document[AclSym] && this.props.Document[AclSym] === AclPrivate) return (null); if (!(this.props.Document instanceof Doc)) return (null); + if (this.props.Document[AclSym] && this.props.Document[AclSym] === AclPrivate) return (null); + if (this.props.Document.hidden) return (null); const backgroundColor = Doc.UserDoc().renderStyle === "comic" ? undefined : this.props.forcedBackgroundColor?.(this.Document) || StrCast(this.layoutDoc._backgroundColor) || StrCast(this.layoutDoc.backgroundColor) || StrCast(this.Document.backgroundColor) || this.props.backgroundColor?.(this.Document); const opacity = Cast(this.layoutDoc._opacity, "number", Cast(this.layoutDoc.opacity, "number", Cast(this.Document.opacity, "number", null))); const finalOpacity = this.props.opacity ? this.props.opacity() : opacity; @@ -1184,7 +1209,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu id={this.props.Document[Id]} ref={this._mainCont} onKeyDown={this.onKeyDown} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick} - onPointerEnter={action(() => Doc.BrushDoc(this.props.Document))} + onPointerEnter={action(() => { Doc.BrushDoc(this.props.Document); })} onPointerLeave={action((e: React.PointerEvent<HTMLDivElement>) => { let entered = false; const target = document.elementFromPoint(e.nativeEvent.x, e.nativeEvent.y); @@ -1193,12 +1218,15 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu entered = true; } } + // if (this.props.Document !== DocumentLinksButton.StartLink?.Document) { !entered && Doc.UnBrushDoc(this.props.Document); + //} + })} style={{ transformOrigin: this._animateScalingTo ? "center center" : undefined, transform: this._animateScalingTo ? `scale(${this._animateScalingTo})` : undefined, - transition: !this._animateScalingTo ? StrCast(this.Document.transition) : this._animateScalingTo < 1 ? "transform 0.5s ease-in" : "transform 0.5s ease-out", + transition: !this._animateScalingTo ? StrCast(this.Document.dataTransition) : this._animateScalingTo < 1 ? "transform 0.5s ease-in" : "transform 0.5s ease-out", pointerEvents: this.ignorePointerEvents ? "none" : undefined, color: StrCast(this.layoutDoc.color, "inherit"), outline: highlighting && !borderRounding ? `${highlightColors[fullDegree]} ${highlightStyles[fullDegree]} ${localScale}px` : "solid 0px", @@ -1227,4 +1255,4 @@ Scripting.addGlobal(function toggleDetail(doc: any, layoutKey: string, otherKey: const dv = DocumentManager.Instance.getDocumentView(doc); if (dv?.props.Document.layoutKey === layoutKey) dv?.switchViews(otherKey !== "layout", otherKey.replace("layout_", "")); else dv?.switchViews(true, layoutKey.replace("layout_", "")); -});
\ No newline at end of file +}); diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx index cf0b16c7c..5e8dd2497 100644 --- a/src/client/views/nodes/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox.tsx @@ -5,7 +5,7 @@ import { createSchema, makeInterface } from '../../../fields/Schema'; import { DocComponent } from '../DocComponent'; import './FontIconBox.scss'; import { FieldView, FieldViewProps } from './FieldView'; -import { StrCast, Cast } from '../../../fields/Types'; +import { StrCast, Cast, NumCast } from '../../../fields/Types'; import { Utils } from "../../../Utils"; import { runInAction, observable, reaction, IReactionDisposer } from 'mobx'; import { Doc } from '../../../fields/Doc'; @@ -59,13 +59,14 @@ export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>( render() { const referenceDoc = (this.layoutDoc.dragFactory instanceof Doc ? this.layoutDoc.dragFactory : this.layoutDoc); - const referenceLayout = Doc.Layout(referenceDoc); + const refLayout = Doc.Layout(referenceDoc); return <button className="fontIconBox-outerDiv" title={StrCast(this.layoutDoc.title)} ref={this._ref} onContextMenu={this.specificContextMenu} style={{ - background: StrCast(referenceLayout.backgroundColor), + padding: Cast(this.layoutDoc._xPadding, "number", null), + background: StrCast(refLayout._backgroundColor, StrCast(refLayout.backgroundColor)), boxShadow: this.layoutDoc.ischecked ? `4px 4px 12px black` : undefined }}> - <FontAwesomeIcon className="fontIconBox-icon" icon={this.dataDoc.icon as any} color={this._foregroundColor} size="sm" /> + <FontAwesomeIcon className="fontIconBox-icon" icon={this.dataDoc.icon as any} color={StrCast(this.layoutDoc.color, this._foregroundColor)} size="sm" /> {!this.rootDoc.label ? (null) : <div className="fontIconBox-label"> {StrCast(this.rootDoc.label).substring(0, 5)} </div>} </button>; } diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index 15148d01d..c1b95b308 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -2,7 +2,7 @@ border-radius: inherit; width: 100%; height: 100%; - position: absolute; + position: relative; transform-origin: top left; .imageBox-fader { diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index c1c6f6baf..d16aa528c 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -120,7 +120,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD }); const files = await res.json(); const url = Utils.prepend(files[0].path); - // upload to server with known URL + // upload to server with known URL const audioDoc = Docs.Create.AudioDocument(url, { title: "audio test", _width: 200, _height: 32 }); audioDoc.treeViewExpandedView = "layout"; const audioAnnos = Cast(this.dataDoc[this.fieldKey + "-audioAnnotations"], listSpec(Doc)); @@ -174,14 +174,14 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD // }), icon: "expand-arrows-alt" // }); - const existingAnalyze = ContextMenu.Instance.findByDescription("Analyzers..."); + const existingAnalyze = ContextMenu.Instance?.findByDescription("Analyzers..."); const modes: ContextMenuProps[] = existingAnalyze && "subitems" in existingAnalyze ? existingAnalyze.subitems : []; modes.push({ description: "Generate Tags", event: this.generateMetadata, icon: "tag" }); modes.push({ description: "Find Faces", event: this.extractFaces, icon: "camera" }); //modes.push({ description: "Recommend", event: this.extractText, icon: "brain" }); - !existingAnalyze && ContextMenu.Instance.addItem({ description: "Analyzers...", subitems: modes, icon: "hand-point-right" }); + !existingAnalyze && ContextMenu.Instance?.addItem({ description: "Analyzers...", subitems: modes, icon: "hand-point-right" }); - ContextMenu.Instance.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); + ContextMenu.Instance?.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); } } @@ -236,6 +236,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD if (this._curSuffix === "_m") this._mediumRetryCount++; if (this._curSuffix === "_l") this._largeRetryCount++; } + @action onError = (error: any) => { const timeout = this._curSuffix === "_s" ? this._smallRetryCount : this._curSuffix === "_m" ? this._mediumRetryCount : this._largeRetryCount; if (timeout < 5) { @@ -490,4 +491,4 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD </CollectionFreeFormView> </div >); } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index d375466c9..b732f5f83 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -169,8 +169,8 @@ export class KeyValueBox extends React.Component<FieldViewProps> { getTemplate = async () => { const parent = Docs.Create.StackingDocument([], { _width: 800, _height: 800, title: "Template" }); - parent.singleColumn = false; - parent.columnWidth = 100; + parent._columnsStack = false; + parent._columnWidth = 100; for (const row of this.rows.filter(row => row.isChecked)) { await this.createTemplateField(parent, row); row.uncheck(); diff --git a/src/client/views/nodes/LinkAnchorBox.scss b/src/client/views/nodes/LinkAnchorBox.scss index 710f2178b..42ef2958e 100644 --- a/src/client/views/nodes/LinkAnchorBox.scss +++ b/src/client/views/nodes/LinkAnchorBox.scss @@ -1,4 +1,5 @@ -.linkAnchorBox-cont, .linkAnchorBox-cont-small { +.linkAnchorBox-cont, +.linkAnchorBox-cont-small { cursor: default; position: absolute; width: 15; @@ -24,6 +25,6 @@ } .linkAnchorBox-cont-small { - width:5px; - height:5px; + width: 5px; + height: 5px; }
\ No newline at end of file diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx index 2bcc6168b..d4ab70200 100644 --- a/src/client/views/nodes/LinkAnchorBox.tsx +++ b/src/client/views/nodes/LinkAnchorBox.tsx @@ -115,8 +115,9 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch render() { TraceMobx(); - const x = this.props.PanelWidth() > 1 ? NumCast(this.rootDoc[this.fieldKey + "_x"], 100) : 0; - const y = this.props.PanelWidth() > 1 ? NumCast(this.rootDoc[this.fieldKey + "_y"], 100) : 0; + const small = this.props.PanelWidth() <= 1; // this happens when rendered in a treeView + const x = NumCast(this.rootDoc[this.fieldKey + "_x"], 100); + const y = NumCast(this.rootDoc[this.fieldKey + "_y"], 100); const c = StrCast(this.layoutDoc._backgroundColor, StrCast(this.layoutDoc.backgroundColor, StrCast(this.dataDoc.backgroundColor, "lightBlue"))); // note this is not where the typical lightBlue default color comes from. See Documents.Create.LinkDocument() const anchor = this.fieldKey === "anchor1" ? "anchor2" : "anchor1"; const anchorScale = (x === 0 || x === 100 || y === 0 || y === 100) ? 1 : .25; @@ -131,7 +132,6 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch </div>} </div> ); - const small = this.props.PanelWidth() <= 1; return <div className={`linkAnchorBox-cont${small ? "-small" : ""} ${this.rootDoc[Id]}`} onPointerLeave={action(() => LinkDocPreview.LinkInfo = undefined)} onPointerEnter={action(e => LinkDocPreview.LinkInfo = { @@ -143,8 +143,8 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch onPointerDown={this.onPointerDown} onClick={this.onClick} title={targetTitle} onContextMenu={this.specificContextMenu} ref={this._ref} style={{ background: c, - left: !small ? `calc(${x}% - 7.5px)` : undefined, - top: !small ? `calc(${y}% - 7.5px)` : undefined, + left: `calc(${x}% - ${small ? 2.5 : 7.5}px)`, + top: `calc(${y}% - ${small ? 2.5 : 7.5}px)`, transform: `scale(${anchorScale / this.props.ContentScaling()})` }} > {!this._editing && !this._forceOpen ? (null) : diff --git a/src/client/views/nodes/LinkCreatedBox.scss b/src/client/views/nodes/LinkCreatedBox.scss new file mode 100644 index 000000000..3cbd38b55 --- /dev/null +++ b/src/client/views/nodes/LinkCreatedBox.scss @@ -0,0 +1,21 @@ +.linkCreatedBox-fade { + border: 1px solid rgb(100, 100, 100); + + + width: auto; + position: absolute; + + height: auto; + z-index: 10000; + border-radius: 13px; + font-size: 13px; + white-space: nowrap; + + color: rgb(100, 100, 100); + background-color: rgba(250, 250, 250, 0.85); + padding-top: 6.5px; + padding-bottom: 6.5px; + font-weight: bold; + padding-left: 9px; + padding-right: 9px; +}
\ No newline at end of file diff --git a/src/client/views/nodes/LinkCreatedBox.tsx b/src/client/views/nodes/LinkCreatedBox.tsx new file mode 100644 index 000000000..d157d3fca --- /dev/null +++ b/src/client/views/nodes/LinkCreatedBox.tsx @@ -0,0 +1,31 @@ +import React = require("react"); +import { observer } from "mobx-react"; +import { documentSchema } from "../../../fields/documentSchemas"; +import { makeInterface } from "../../../fields/Schema"; +import "./LinkCreatedBox.scss"; +import { observable, action } from "mobx"; +import { Fade } from "@material-ui/core"; + + +@observer +export class LinkCreatedBox extends React.Component<{}> { + + @observable public static linkCreated: boolean = false; + @observable public static popupX: number = 600; + @observable public static popupY: number = 250; + + @action + public static changeLinkCreated = () => { + LinkCreatedBox.linkCreated = !LinkCreatedBox.linkCreated; + } + + render() { + return <Fade in={LinkCreatedBox.linkCreated}> + <div className="linkCreatedBox-fade" + style={{ + left: LinkCreatedBox.popupX ? LinkCreatedBox.popupX : 600, + top: LinkCreatedBox.popupY ? LinkCreatedBox.popupY : 250, + }}>Link Created</div> + </Fade>; + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx index 126dc240a..197dc8df4 100644 --- a/src/client/views/nodes/LinkDocPreview.tsx +++ b/src/client/views/nodes/LinkDocPreview.tsx @@ -10,6 +10,11 @@ import { Transform } from "../../util/Transform"; import { ContentFittingDocumentView } from "./ContentFittingDocumentView"; import React = require("react"); import { DocumentView } from './DocumentView'; +import { sortAndDeduplicateDiagnostics } from 'typescript'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { LinkManager } from '../../util/LinkManager'; +import { DocumentLinksButton } from './DocumentLinksButton'; +import { ContextMenu } from '../ContextMenu'; interface Props { linkDoc?: Doc; @@ -24,9 +29,34 @@ export class LinkDocPreview extends React.Component<Props> { @observable public static LinkInfo: Opt<{ linkDoc?: Doc; addDocTab: (document: Doc, where: string) => boolean, linkSrc: Doc; href?: string; Location: number[] }>; @observable _targetDoc: Opt<Doc>; @observable _toolTipText = ""; + _editRef = React.createRef<HTMLDivElement>(); - componentDidUpdate() { this.updatePreview() } - componentDidMount() { this.updatePreview() } + @action + deleteLink = (): void => { + this.props.linkDoc ? LinkManager.Instance.deleteLink(this.props.linkDoc) : null; + //this.props.showLinks(); + LinkDocPreview.LinkInfo = undefined; + DocumentLinksButton.EditLink = undefined; + } + + @action + onContextMenu = (e: React.MouseEvent) => { + DocumentLinksButton.EditLink = undefined; + LinkDocPreview.LinkInfo = undefined; + e.preventDefault(); + ContextMenu.Instance.addItem({ description: "Follow Default Link", event: () => this.followDefault(), icon: "arrow-right" }); + ContextMenu.Instance.displayMenu(e.clientX, e.clientY); + } + + @action.bound + async followDefault() { + DocumentLinksButton.EditLink = undefined; + LinkDocPreview.LinkInfo = undefined; + this._targetDoc ? DocumentManager.Instance.FollowLink(this.props.linkDoc, this._targetDoc, doc => this.props.addDocTab(doc, "onRight"), false) : null; + } + + componentDidUpdate() { this.updatePreview(); } + componentDidMount() { this.updatePreview(); } async updatePreview() { const linkDoc = this.props.linkDoc; const linkSrc = this.props.linkSrc; @@ -56,15 +86,30 @@ export class LinkDocPreview extends React.Component<Props> { this.props.addDocTab(Docs.Create.WebDocument(this.props.href, { title: this.props.href, _width: 200, _height: 400, UseCors: true }), "onRight"); } } - width = () => Math.min(350, NumCast(this._targetDoc?.[WidthSym](), 350)); - height = () => Math.min(350, NumCast(this._targetDoc?.[HeightSym](), 350)); + width = () => Math.min(225, NumCast(this._targetDoc?.[WidthSym](), 225)); + height = () => Math.min(225, NumCast(this._targetDoc?.[HeightSym](), 225)); @computed get targetDocView() { return !this._targetDoc ? - <div style={{ pointerEvents: "all", maxWidth: 350, maxHeight: 250, width: "100%", height: "100%", overflow: "hidden" }}> + <div style={{ + pointerEvents: "all", maxWidth: 225, maxHeight: 225, width: "100%", height: "100%", + overflow: "hidden" + }}> <div style={{ width: "100%", height: "100%", textOverflow: "ellipsis", }} onPointerDown={this.pointerDown}> {this._toolTipText} </div> </div> : + // <div style={{ + // border: "6px solid white", + // }}> + // <div style={{ backgroundColor: "white" }}> {this._targetDoc.title} + // <div className="wrapper" style={{ float: "right" }}> + // <div title="Delete link" className="button" style={{ display: "inline" }} ref={this._editRef} onPointerDown={this.deleteLink}> + // <FontAwesomeIcon className="fa-icon" icon="trash" size="sm" /></div> + // <div title="Follow link" className="button" style={{ display: "inline" }} onClick={this.followDefault} onContextMenu={this.onContextMenu}> + // <FontAwesomeIcon className="fa-icon" icon="arrow-right" size="sm" /> + // </div> + // </div> + // </div> <ContentFittingDocumentView Document={this._targetDoc} LibraryPath={emptyPath} @@ -92,6 +137,7 @@ export class LinkDocPreview extends React.Component<Props> { NativeWidth={returnZero} NativeHeight={returnZero} />; + //</div>; } render() { @@ -99,9 +145,9 @@ export class LinkDocPreview extends React.Component<Props> { style={{ position: "absolute", left: this.props.location[0], top: this.props.location[1], width: this.width(), height: this.height(), - boxShadow: "black 2px 2px 1em" + boxShadow: "black 2px 2px 1em", zIndex: 1000 }}> {this.targetDocView} </div>; } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss index 3e09fe519..f2ab37984 100644 --- a/src/client/views/nodes/PDFBox.scss +++ b/src/client/views/nodes/PDFBox.scss @@ -1,28 +1,30 @@ .pdfBox, .pdfBox-interactive { display: inline-block; - position: absolute; + position: relative; height: 100%; width: 100%; overflow: hidden; - cursor:auto; + cursor: auto; transform-origin: top left; z-index: 0; + .pdfBox-ui { position: absolute; - width: 100%; - height: 100%; - z-index: 1; - pointer-events: none; + width: 100%; + height: 100%; + z-index: 1; + pointer-events: none; - .pdfBox-pageNums { + .pdfBox-pageNums { display: flex; flex-direction: row; height: 25px; position: absolute; left: 5px; top: 5px; - .pdfBox-overlayButton-fwd, + + .pdfBox-overlayButton-fwd, .pdfBox-overlayButton-back { background: #121721; height: 25px; @@ -34,29 +36,29 @@ border-radius: 3px; pointer-events: all; } - } - - .pdfBox-overlayButton { - border-bottom-left-radius: 50%; - display: flex; - justify-content: space-evenly; - align-items: center; - height: 20px; - background: none; - padding: 0; - position: absolute; - pointer-events: all; - - .pdfBox-overlayButton-arrow { - width: 0; - height: 0; - border-top: 10px solid transparent; - border-bottom: 10px solid transparent; - border-right: 15px solid #121721; - transition: all 0.5s; - } - - .pdfBox-overlayButton-iconCont { + } + + .pdfBox-overlayButton { + border-bottom-left-radius: 50%; + display: flex; + justify-content: space-evenly; + align-items: center; + height: 20px; + background: none; + padding: 0; + position: absolute; + pointer-events: all; + + .pdfBox-overlayButton-arrow { + width: 0; + height: 0; + border-top: 10px solid transparent; + border-bottom: 10px solid transparent; + border-right: 15px solid #121721; + transition: all 0.5s; + } + + .pdfBox-overlayButton-iconCont { background: #121721; height: 20px; width: 25px; @@ -66,11 +68,11 @@ justify-content: center; border-radius: 3px; pointer-events: all; - } + } } - .pdfBox-nextIcon, - .pdfBox-prevIcon { + .pdfBox-nextIcon, + .pdfBox-prevIcon { background: #121721; height: 20px; width: 25px; @@ -81,96 +83,97 @@ border-radius: 3px; pointer-events: all; padding: 0px; - } - - .pdfBox-overlayButton:hover { - background: none; - } - - - .pdfBox-settingsCont { - position: absolute; - right: 0; - top: 3; - pointer-events: all; - - .pdfBox-settingsButton { - border-bottom-left-radius: 50%; - display: flex; - justify-content: space-evenly; - align-items: center; - height: 20px; - background: none; - padding: 0; - - .pdfBox-settingsButton-arrow { - width: 0; - height: 0; - border-top: 10px solid transparent; - border-bottom: 10px solid transparent; - border-right: 15px solid #121721; - transition: all 0.5s; - } - - .pdfBox-settingsButton-iconCont { - background: #121721; - height: 20px; - width: 25px; - display: flex; - justify-content: center; - align-items: center; - margin-left: -2px; - border-radius: 3px; - } - } - - .pdfBox-settingsButton:hover { - background: none; - } - - .pdfBox-settingsFlyout { - position: absolute; - background: #323232; - box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25); - right: 20px; - border-radius: 7px; - padding: 20px; - display: flex; - flex-direction: column; - font-size: 14px; - transition: all 0.5s; - - .pdfBox-settingsFlyout-title { - color: white; - } - - .pdfBox-settingsFlyout-kvpInput { - margin-top: 20px; - display: grid; - grid-template-columns: 47.5% 5% 47.5%; - } - } - } - - .pdfBox-overlayCont { - position: absolute; - width: calc(100% - 40px); - height: 20px; - background: #121721; - bottom: 0; - display: flex; - justify-content: center; - align-items: center; - overflow: hidden; - transition: left .5s; - pointer-events: all; - - .pdfBox-searchBar { - width: 70%; - font-size: 14px; - } - } + } + + .pdfBox-overlayButton:hover { + background: none; + } + + + .pdfBox-settingsCont { + position: absolute; + right: 0; + top: 3; + pointer-events: all; + + .pdfBox-settingsButton { + border-bottom-left-radius: 50%; + display: flex; + justify-content: space-evenly; + align-items: center; + height: 20px; + background: none; + padding: 0; + + .pdfBox-settingsButton-arrow { + width: 0; + height: 0; + border-top: 10px solid transparent; + border-bottom: 10px solid transparent; + border-right: 15px solid #121721; + transition: all 0.5s; + } + + .pdfBox-settingsButton-iconCont { + background: #121721; + height: 20px; + width: 25px; + display: flex; + justify-content: center; + align-items: center; + margin-left: -2px; + border-radius: 3px; + } + } + + .pdfBox-settingsButton:hover { + background: none; + } + + .pdfBox-settingsFlyout { + position: absolute; + background: #323232; + box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25); + right: 20px; + border-radius: 7px; + padding: 20px; + display: flex; + flex-direction: column; + font-size: 14px; + transition: all 0.5s; + + .pdfBox-settingsFlyout-title { + color: white; + } + + .pdfBox-settingsFlyout-kvpInput { + margin-top: 20px; + display: grid; + grid-template-columns: 47.5% 5% 47.5%; + } + } + } + + .pdfBox-overlayCont { + position: absolute; + width: calc(100% - 40px); + height: 20px; + background: #121721; + bottom: 0; + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; + transition: left .5s; + pointer-events: all; + + .pdfBox-searchBar { + width: 70%; + font-size: 14px; + } + } } + .pdfBox-title-outer { width: 150%; height: 100%; @@ -179,9 +182,9 @@ z-index: 0; background: lightslategray; transform-origin: top left; - + .pdfBox-title { - color:lightgray; + color: lightgray; margin-top: auto; margin-bottom: auto; transform-origin: 42% 15%; @@ -199,7 +202,7 @@ } .pdfBox { - .pdfViewer-text { + .pdfViewerDash-text { .textLayer { span { user-select: none; @@ -210,11 +213,83 @@ .pdfBox-interactive { pointer-events: all; - .pdfViewer-text { + .pdfViewerDash-text { .textLayer { span { user-select: text; } } } -}
\ No newline at end of file +} + +// CSS adjusted for mobile devices +@media only screen and (max-device-width: 480px) { + + .pdfBox .pdfBox-ui .pdfBox-settingsCont .pdfBox-settingsButton, + .pdfBox-interactive .pdfBox-ui .pdfBox-settingsCont .pdfBox-settingsButton { + height: 60px; + + .pdfBox-settingsButton-iconCont { + height: 60px; + width: 75px; + font-size: 30px; + } + + .pdfBox-settingsButton-arrow { + height: 60; + border-top: 30px solid transparent; + border-bottom: 30px solid transparent; + border-right: 30px solid #121721; + } + } + + + + .pdfBox .pdfBox-ui .pdfBox-settingsCont .pdfBox-settingsFlyout, + .pdfBox-interactive .pdfBox-ui .pdfBox-settingsCont .pdfBox-settingsFlyout { + font-size: 30px; + } + + + + .pdfBox .pdfBox-ui .pdfBox-overlayCont, + .pdfBox-interactive .pdfBox-ui .pdfBox-overlayCont { + height: 60px; + + .pdfBox-searchBar { + font-size: 40px; + } + } + + .pdfBox .pdfBox-ui .pdfBox-overlayButton, + .pdfBox-interactive .pdfBox-ui .pdfBox-overlayButton { + height: 60px; + + .pdfBox-overlayButton-iconCont { + height: 60px; + width: 75px; + font-size: 30; + } + + .pdfBox-overlayButton-arrow { + border-top: 30px solid transparent; + border-bottom: 30px solid transparent; + border-right: 30px solid #121721; + } + } + + button.pdfBox-search { + font-size: 30px; + width: 50px; + height: 50px; + } + + .pdfBox .pdfBox-ui .pdfBox-nextIcon, + .pdfBox .pdfBox-ui .pdfBox-prevIcon, + .pdfBox-interactive .pdfBox-ui .pdfBox-nextIcon, + .pdfBox-interactive .pdfBox-ui .pdfBox-prevIcon { + height: 50px; + width: 50px; + font-size: 30px; + } +} diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 6b1c9fcde..1c5825a8f 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -55,25 +55,28 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum const backup = "oldPath"; const { Document } = this.props; - const { url: { href } } = Cast(this.dataDoc[this.props.fieldKey], PdfField)!; - const pathCorrectionTest = /upload\_[a-z0-9]{32}.(.*)/g; - const matches = pathCorrectionTest.exec(href); - console.log("\nHere's the { url } being fed into the outer regex:"); - console.log(href); - console.log("And here's the 'properPath' build from the captured filename:\n"); - if (matches !== null && href.startsWith(window.location.origin)) { - const properPath = Utils.prepend(`/files/pdfs/${matches[0]}`); - console.log(properPath); - if (!properPath.includes(href)) { - console.log(`The two (url and proper path) were not equal`); - const proto = Doc.GetProto(Document); - proto[this.props.fieldKey] = new PdfField(properPath); - proto[backup] = href; + const pdf = Cast(this.dataDoc[this.props.fieldKey], PdfField); + const href = pdf?.url?.href; + if (href) { + const pathCorrectionTest = /upload\_[a-z0-9]{32}.(.*)/g; + const matches = pathCorrectionTest.exec(href); + console.log("\nHere's the { url } being fed into the outer regex:"); + console.log(href); + console.log("And here's the 'properPath' build from the captured filename:\n"); + if (matches !== null && href.startsWith(window.location.origin)) { + const properPath = Utils.prepend(`/files/pdfs/${matches[0]}`); + console.log(properPath); + if (!properPath.includes(href)) { + console.log(`The two (url and proper path) were not equal`); + const proto = Doc.GetProto(Document); + proto[this.props.fieldKey] = new PdfField(properPath); + proto[backup] = href; + } else { + console.log(`The two (url and proper path) were equal`); + } } else { - console.log(`The two (url and proper path) were equal`); + console.log("Outer matches was null!"); } - } else { - console.log("Outer matches was null!"); } } @@ -154,7 +157,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum <div className="pdfBox-overlayCont" key="cont" onPointerDown={(e) => e.stopPropagation()} style={{ left: `${this._searching ? 0 : 100}%` }}> <button className="pdfBox-overlayButton" title={searchTitle} /> <input className="pdfBox-searchBar" placeholder="Search" ref={this._searchRef} onChange={this.searchStringChanged} onKeyDown={e => e.keyCode === KeyCodes.ENTER && this.search(this._searchString, !e.shiftKey)} /> - <button title="Search" onClick={e => this.search(this._searchString, !e.shiftKey)}> + <button className="pdfBox-search" title="Search" onClick={e => this.search(this._searchString, !e.shiftKey)}> <FontAwesomeIcon icon="search" size="sm" color="white" /></button> <button className="pdfBox-prevIcon " title="Previous Annotation" onClick={this.prevAnnotation} > <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-up"} size="lg" /> @@ -186,7 +189,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum <FontAwesomeIcon style={{ color: "white" }} icon="cog" size="lg" /> </div> </button> - <div className="pdfBox-settingsFlyout" style={{ right: `${this._flyout ? 20 : -600}px` }} > + <div className="pdfBox-settingsFlyout" style={{ right: `${this._flyout ? 20 : -1000}px` }} > <div className="pdfBox-settingsFlyout-title"> Annotation View Settings </div> @@ -226,7 +229,8 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum const classname = "pdfBox" + (this.active() ? "-interactive" : ""); return <div className={classname} style={{ width: !this.props.Document._fitWidth ? this.Document._nativeWidth || 0 : `${100 / this.contentScaling}%`, - height: !this.props.Document._fitWidth ? this.Document._nativeHeight || 0 : `${100 / this.contentScaling}%`, + //height adjusted for mobile (window.screen.width > 600) + height: !this.props.Document._fitWidth && (window.screen.width > 600) ? this.Document._nativeHeight || 0 : `${100 / this.contentScaling}%`, transform: `scale(${this.contentScaling})` }} > <div className="pdfBox-title-outer"> @@ -238,7 +242,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum isChildActive = (outsideReaction?: boolean) => this._isChildActive; @computed get renderPdfView() { const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField); - return <div className={"pdfBox"} onContextMenu={this.specificContextMenu} style={{ height: this.props.Document._scrollTop && !this.Document._fitWidth ? NumCast(this.Document._height) * this.props.PanelWidth() / NumCast(this.Document._width) : undefined }}> + return <div className={"pdfBox"} onContextMenu={this.specificContextMenu} style={{ height: this.props.Document._scrollTop && !this.Document._fitWidth && (window.screen.width > 600) ? NumCast(this.Document._height) * this.props.PanelWidth() / NumCast(this.Document._width) : undefined }}> <PDFViewer {...this.props} pdf={this._pdf!} url={pdfUrl!.url.pathname} active={this.props.active} loaded={this.loaded} setPdfViewer={this.setPdfViewer} ContainingCollectionView={this.props.ContainingCollectionView} renderDepth={this.props.renderDepth} PanelHeight={this.props.PanelHeight} PanelWidth={this.props.PanelWidth} diff --git a/src/client/views/nodes/PresBox.scss b/src/client/views/nodes/PresBox.scss index d48000e16..9f6af1bde 100644 --- a/src/client/views/nodes/PresBox.scss +++ b/src/client/views/nodes/PresBox.scss @@ -16,6 +16,7 @@ height: calc(100% - 25px); width: 100%; } + .presBox-buttons { width: 100%; background: gray; @@ -24,6 +25,7 @@ display: grid; grid-column-end: 4; grid-column-start: 1; + .presBox-viewPicker { height: 25; position: relative; @@ -31,10 +33,12 @@ grid-column: 1/2; min-width: 15px; } + select { background: #323232; color: white; } + .presBox-button { margin-right: 2.5%; margin-left: 2.5%; @@ -44,10 +48,12 @@ align-items: center; background: #323232; color: white; + svg { margin: auto; } } + .collectionViewBaseChrome-viewPicker { min-width: 50; width: 5%; @@ -56,17 +62,68 @@ display: inline-block; } } - .presBox-backward, .presBox-forward { + + .presBox-backward, + .presBox-forward { width: 25px; border-radius: 5px; - top:50%; + top: 50%; position: absolute; display: inline-block; } + .presBox-backward { - left:5; + left: 5; } + .presBox-forward { - right:5; + right: 5; + } +} + +// CSS adjusted for mobile devices +@media only screen and (max-device-width: 480px) { + .presBox-cont .presBox-buttons { + position: absolute; + top: 70%; + left: 50%; + transform: translate(-50%, 0); + width: max-content; + height: 15%; + z-index: 2; + align-items: center; + background: rgba(0, 0, 0, 0); + display: inline-flex; + + .presBox-button { + margin-top: 5%; + height: 250; + width: 300; + font-size: 100; + display: flex; + align-items: center; + background: #323232; + color: white; + } + + .presBox-viewPicker { + top: -70; + left: 2.5%; + height: 50; + width: 95%; + font-size: 30px; + position: absolute; + min-width: 50px; + } + } + + .presBox-cont .presBox-listCont { + top: 50; + height: calc(100% - 80px); + } + + .input, + .select { + font-size: 100%; } }
\ No newline at end of file diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx index dbc879920..8818d375e 100644 --- a/src/client/views/nodes/PresBox.tsx +++ b/src/client/views/nodes/PresBox.tsx @@ -62,8 +62,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> const lastFrame = Cast(presTargetDoc.lastFrame, "number", null); const curFrame = NumCast(presTargetDoc.currentFrame); if (lastFrame !== undefined && curFrame < lastFrame) { - presTargetDoc.transition = "all 1s"; - setTimeout(() => presTargetDoc.transition = undefined, 1010); + presTargetDoc._viewTransition = "all 1s"; + setTimeout(() => presTargetDoc._viewTransition = undefined, 1010); presTargetDoc.currentFrame = curFrame + 1; } else if (this.childDocs[this.itemIndex + 1] !== undefined) { diff --git a/src/client/views/nodes/RadialMenu.tsx b/src/client/views/nodes/RadialMenu.tsx index ddfdb67b4..a3ac09a11 100644 --- a/src/client/views/nodes/RadialMenu.tsx +++ b/src/client/views/nodes/RadialMenu.tsx @@ -1,7 +1,6 @@ import React = require("react"); import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; -import MobileInterface from "../../../mobile/MobileInterface"; import "./RadialMenu.scss"; import { RadialMenuItem, RadialMenuProps } from "./RadialMenuItem"; @@ -26,7 +25,6 @@ export class RadialMenu extends React.Component { catchTouch = (te: React.TouchEvent) => { - console.log("caught"); te.stopPropagation(); te.preventDefault(); } @@ -38,7 +36,6 @@ export class RadialMenu extends React.Component { this._mouseY = e.clientY; this.used = false; document.addEventListener("pointermove", this.onPointerMove); - } @observable @@ -176,7 +173,6 @@ export class RadialMenu extends React.Component { @action openMenu = (x: number, y: number) => { - this._pageX = x; this._pageY = y; this._shouldDisplay; @@ -216,7 +212,7 @@ export class RadialMenu extends React.Component { render() { - if (!this._display || MobileInterface.Instance) { + if (!this._display) { return null; } const style = this._yRelativeToTop ? { left: this._pageX - 130, top: this._pageY - 130 } : diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 71556bfd3..a5c6c4a48 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -19,6 +19,7 @@ import { FieldView, FieldViewProps } from './FieldView'; import "./VideoBox.scss"; import { documentSchema } from "../../../fields/documentSchemas"; import { Networking } from "../../Network"; +import { SnappingManager } from "../../util/SnappingManager"; const path = require('path'); export const timeSchema = createSchema({ @@ -58,21 +59,21 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD @action public Play = (update: boolean = true) => { this._playing = true; - update && this.player && this.player.play(); - update && this._youtubePlayer && this._youtubePlayer.playVideo(); + update && this.player?.play(); + update && this._youtubePlayer?.playVideo(); this._youtubePlayer && !this._playTimer && (this._playTimer = setInterval(this.updateTimecode, 5)); this.updateTimecode(); } @action public Seek(time: number) { - this._youtubePlayer && this._youtubePlayer.seekTo(Math.round(time), true); + this._youtubePlayer?.seekTo(Math.round(time), true); this.player && (this.player.currentTime = time); } @action public Pause = (update: boolean = true) => { this._playing = false; - update && this.player && this.player.pause(); - update && this._youtubePlayer && this._youtubePlayer.pauseVideo && this._youtubePlayer.pauseVideo(); + update && this.player?.pause(); + update && this._youtubePlayer?.pauseVideo(); this._youtubePlayer && this._playTimer && clearInterval(this._playTimer); this._playTimer = undefined; this.updateTimecode(); @@ -261,21 +262,20 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD const onYoutubePlayerStateChange = (event: any) => runInAction(() => { if (started && event.data === YT.PlayerState.PLAYING) { started = false; - this._youtubePlayer && this._youtubePlayer.unMute(); - this.Pause(); + this._youtubePlayer?.unMute(); + //this.Pause(); return; } if (event.data === YT.PlayerState.PLAYING && !this._playing) this.Play(false); if (event.data === YT.PlayerState.PAUSED && this._playing) this.Pause(false); }); const onYoutubePlayerReady = (event: any) => { - this._reactionDisposer && this._reactionDisposer(); - this._youtubeReactionDisposer && this._youtubeReactionDisposer(); + this._reactionDisposer?.(); + this._youtubeReactionDisposer?.(); this._reactionDisposer = reaction(() => this.layoutDoc.currentTimecode, () => !this._playing && this.Seek((this.layoutDoc.currentTimecode || 0))); - this._youtubeReactionDisposer = reaction(() => [this.props.isSelected(), DocumentDecorations.Instance.Interacting, Doc.GetSelectedTool()], () => { - const interactive = Doc.GetSelectedTool() === InkTool.None && this.props.isSelected(true) && !DocumentDecorations.Instance.Interacting; - iframe.style.pointerEvents = interactive ? "all" : "none"; - }, { fireImmediately: true }); + this._youtubeReactionDisposer = reaction( + () => Doc.GetSelectedTool() === InkTool.None && this.props.isSelected(true) && !SnappingManager.GetIsDragging() && !DocumentDecorations.Instance.Interacting, + (interactive) => iframe.style.pointerEvents = interactive ? "all" : "none", { fireImmediately: true }); }; this._youtubePlayer = new YT.Player(`${this.youtubeVideoId + this._youtubeIframeId}-player`, { events: { @@ -346,7 +346,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD const start = untracked(() => Math.round((this.layoutDoc.currentTimecode || 0))); return <iframe key={this._youtubeIframeId} id={`${this.youtubeVideoId + this._youtubeIframeId}-player`} onLoad={this.youtubeIframeLoaded} className={`${style}`} width={(this.layoutDoc._nativeWidth || 640)} height={(this.layoutDoc._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}`} />; + src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=0&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._showControls ? 1 : 0}`} />; } @action.bound diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx index 8c16f4a1a..8718bf329 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.tsx +++ b/src/client/views/nodes/formattedText/DashFieldView.tsx @@ -184,9 +184,9 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna if (container) { const alias = Doc.MakeAlias(container.props.Document); alias.viewType = CollectionViewType.Time; - let list = Cast(alias.schemaColumns, listSpec(SchemaHeaderField)); + let list = Cast(alias._columnHeaders, listSpec(SchemaHeaderField)); if (!list) { - alias.schemaColumns = list = new List<SchemaHeaderField>(); + alias._columnHeaders = list = new List<SchemaHeaderField>(); } list.map(c => c.heading).indexOf(this._fieldKey) === -1 && list.push(new SchemaHeaderField(this._fieldKey, "#f1efeb")); list.map(c => c.heading).indexOf("text") === -1 && list.push(new SchemaHeaderField("text", "#f1efeb")); diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss index 20e13a599..afdd8fea2 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss @@ -37,6 +37,7 @@ position: absolute; } } + .formattedTextBox-outer { position: relative; overflow: auto; @@ -72,7 +73,7 @@ .collectionfreeformview-container { position: relative; } - + >.formattedTextBox-sidebar-handle { right: unset; left: -5; @@ -95,7 +96,7 @@ .formattedTextBox-inner-rounded, .formattedTextBox-inner-rounded-selected, .formattedTextBox-inner, .formattedTextBox-inner-selected { height: 100%; - white-space: pre-wrap; + white-space: pre-wrap; .ProseMirror:hover { background: rgba(200,200,200,0.8); } @@ -262,19 +263,19 @@ footnote::after { border:unset; padding:0px; } - + .prosemirror-links a { float: left; color: white; text-decoration: none; border-radius: 3px; } - + .prosemirror-links a:hover { background-color: #eee; color: black; } - + .prosemirror-anchor:hover .prosemirror-links { display: grid; } @@ -302,40 +303,39 @@ footnote::after { font-family: inherit; } ol { - margin-left: 1em; font-family: inherit; } - .bullet { p {display: inline; font-family: inherit} margin-left: 0; } - .bullet1 { p {display: inline; font-family: inherit} } - .bullet2,.bullet3,.bullet4,.bullet5,.bullet6 { p {display: inline; font-family: inherit} font-size: smaller; } - - .decimal1-ol { counter-reset: deci1; p {display: inline; font-family: inherit} margin-left: 0; } - .decimal2-ol { counter-reset: deci2; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 1em;} - .decimal3-ol { counter-reset: deci3; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 2em;} - .decimal4-ol { counter-reset: deci4; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 3em;} - .decimal5-ol { counter-reset: deci5; p {display: inline; font-family: inherit} font-size: smaller; } - .decimal6-ol { counter-reset: deci6; p {display: inline; font-family: inherit} font-size: smaller; } - .decimal7-ol { counter-reset: deci7; p {display: inline; font-family: inherit} font-size: smaller; } - - .multi1-ol { counter-reset: multi1; p {display: inline; font-family: inherit} margin-left: 0; padding-left: 1.2em } - .multi2-ol { counter-reset: multi2; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 1.4em;} - .multi3-ol { counter-reset: multi3; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 2em;} - .multi4-ol { counter-reset: multi4; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 3.4em;} - - .bullet:before, .bullet1:before, .bullet2:before, .bullet3:before, .bullet4:before, .bullet5:before { transition: 0.5s; display: inline-block; margin-left: -1em; width: 1em; content:" " } - - .decimal1:before { transition: 0.5s;counter-increment: deci1; display: inline-block; margin-left: -1em; width: 1em; content: counter(deci1) ". "; } - .decimal2:before { transition: 0.5s;counter-increment: deci2; display: inline-block; margin-left: -2.1em; width: 2.1em; content: counter(deci1) "."counter(deci2) ". "; } - .decimal3:before { transition: 0.5s;counter-increment: deci3; display: inline-block; margin-left: -2.85em;width: 2.85em; content: counter(deci1) "."counter(deci2) "."counter(deci3) ". "; } - .decimal4:before { transition: 0.5s;counter-increment: deci4; display: inline-block; margin-left: -3.85em;width: 3.85em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) ". "; } - .decimal5:before { transition: 0.5s;counter-increment: deci5; display: inline-block; margin-left: -2em; width: 5em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) ". "; } - .decimal6:before { transition: 0.5s;counter-increment: deci6; display: inline-block; margin-left: -2em; width: 6em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) "."counter(deci6) ". "; } - .decimal7:before { transition: 0.5s;counter-increment: deci7; display: inline-block; margin-left: -2em; width: 7em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) "."counter(deci6) "."counter(deci7) ". "; } - - .multi1:before { transition: 0.5s;counter-increment: multi1; display: inline-block; margin-left: -1em; width: 1.2em; content: counter(multi1, upper-alpha) ". "; } - .multi2:before { transition: 0.5s;counter-increment: multi2; display: inline-block; margin-left: -2em; width: 2em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) ". "; } - .multi3:before { transition: 0.5s;counter-increment: multi3; display: inline-block; margin-left: -2.85em; width:2.85em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) "."counter(multi3, lower-alpha) ". "; } - .multi4:before { transition: 0.5s;counter-increment: multi4; display: inline-block; margin-left: -4.2em; width: 4.2em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) "."counter(multi3, lower-alpha) "."counter(multi4, lower-roman) ". "; } + .bullet { p { font-family: inherit} margin-left: 0; } + .bullet1 { p { font-family: inherit} } + .bullet2,.bullet3,.bullet4,.bullet5,.bullet6 { p { font-family: inherit} font-size: smaller; } + + .decimal1-ol { counter-reset: deci1; p {display: inline-block; font-family: inherit} margin-left: 0; } + .decimal2-ol { counter-reset: deci2; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 2.1em;} + .decimal3-ol { counter-reset: deci3; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 2.85em;} + .decimal4-ol { counter-reset: deci4; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 3.85em;} + .decimal5-ol { counter-reset: deci5; p {display: inline-block; font-family: inherit} font-size: smaller; } + .decimal6-ol { counter-reset: deci6; p {display: inline-block; font-family: inherit} font-size: smaller; } + .decimal7-ol { counter-reset: deci7; p {display: inline-block; font-family: inherit} font-size: smaller; } + + .multi1-ol { counter-reset: multi1; p {display: inline-block; font-family: inherit} margin-left: 0; padding-left: 1.2em } + .multi2-ol { counter-reset: multi2; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 2em;} + .multi3-ol { counter-reset: multi3; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 2.85em;} + .multi4-ol { counter-reset: multi4; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 3.85em;} + + //.bullet:before, .bullet1:before, .bullet2:before, .bullet3:before, .bullet4:before, .bullet5:before { transition: 0.5s; display: inline-block; vertical-align: top; margin-left: -1em; width: 1em; content:" " } + + .decimal1:before { transition: 0.5s;counter-increment: deci1; display: inline-block; vertical-align: top; margin-left: -1em; width: 1em; content: counter(deci1) ". "; } + .decimal2:before { transition: 0.5s;counter-increment: deci2; display: inline-block; vertical-align: top; margin-left: -2.1em; width: 2.1em; content: counter(deci1) "."counter(deci2) ". "; } + .decimal3:before { transition: 0.5s;counter-increment: deci3; display: inline-block; vertical-align: top; margin-left: -2.85em;width: 2.85em; content: counter(deci1) "."counter(deci2) "."counter(deci3) ". "; } + .decimal4:before { transition: 0.5s;counter-increment: deci4; display: inline-block; vertical-align: top; margin-left: -3.85em;width: 3.85em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) ". "; } + .decimal5:before { transition: 0.5s;counter-increment: deci5; display: inline-block; vertical-align: top; margin-left: -2em; width: 5em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) ". "; } + .decimal6:before { transition: 0.5s;counter-increment: deci6; display: inline-block; vertical-align: top; margin-left: -2em; width: 6em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) "."counter(deci6) ". "; } + .decimal7:before { transition: 0.5s;counter-increment: deci7; display: inline-block; vertical-align: top; margin-left: -2em; width: 7em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) "."counter(deci6) "."counter(deci7) ". "; } + + .multi1:before { transition: 0.5s;counter-increment: multi1; display: inline-block; vertical-align: top; margin-left: -1.3em; width: 1.2em; content: counter(multi1, upper-alpha) ". "; } + .multi2:before { transition: 0.5s;counter-increment: multi2; display: inline-block; vertical-align: top; margin-left: -2em; width: 2em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) ". "; } + .multi3:before { transition: 0.5s;counter-increment: multi3; display: inline-block; vertical-align: top; margin-left: -2.85em; width:2.85em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) "."counter(multi3, lower-alpha) ". "; } + .multi4:before { transition: 0.5s;counter-increment: multi4; display: inline-block; vertical-align: top; margin-left: -4.2em; width: 4.2em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) "."counter(multi3, lower-alpha) "."counter(multi4, lower-roman) ". "; } } .formattedTextBox-inner-rounded-selected, @@ -346,4 +346,284 @@ footnote::after { .ProseMirror:hover { background: unset; } -}
\ No newline at end of file +} + +@media only screen and (max-width: 1000px) { + @import "../../globalCssVariables"; + + .ProseMirror { + width: 100%; + height: 100%; + min-height: 100%; + } + + .ProseMirror:focus { + outline: none !important; + } + + .formattedTextBox-cont { + touch-action: none; + cursor: text; + background: inherit; + padding: 0; + border-width: 0px; + border-radius: inherit; + border-color: $intermediate-color; + box-sizing: border-box; + background-color: inherit; + border-style: solid; + overflow-y: auto; + overflow-x: hidden; + color: initial; + max-height: 100%; + display: flex; + flex-direction: row; + transition: opacity 1s; + + .formattedTextBox-dictation { + height: 12px; + width: 10px; + top: 0px; + left: 0px; + position: absolute; + } + } + + .formattedTextBox-outer { + position: relative; + overflow: auto; + display: inline-block; + width: 100%; + height: 100%; + } + + .formattedTextBox-sidebar-handle { + position: absolute; + top: calc(50% - 17.5px); + width: 10px; + height: 35px; + background: lightgray; + border-radius: 20px; + cursor:grabbing; + } + + .formattedTextBox-cont>.formattedTextBox-sidebar-handle { + right: 0; + left: unset; + } + + .formattedTextBox-sidebar, + .formattedTextBox-sidebar-inking { + border-left: dashed 1px black; + height: 100%; + display: inline-block; + position: absolute; + right: 0; + + .collectionfreeformview-container { + position: relative; + } + + >.formattedTextBox-sidebar-handle { + right: unset; + left: -5; + } + } + + .formattedTextBox-sidebar-inking { + pointer-events: all; + } + + .formattedTextBox-inner-rounded { + height: 70%; + width: 85%; + position: absolute; + overflow: auto; + top: 15%; + left: 10%; + } + + .formattedTextBox-inner-rounded, + .formattedTextBox-inner { + height: 100%; + white-space: pre-wrap; + hr { + display: block; + unicode-bidi: isolate; + margin-block-start: 0.5em; + margin-block-end: 0.5em; + margin-inline-start: auto; + margin-inline-end: auto; + overflow: hidden; + border-style: inset; + border-width: 1px; + } + } + + // .menuicon { + // display: inline-block; + // border-right: 1px solid rgba(0, 0, 0, 0.2); + // color: #888; + // line-height: 1; + // padding: 0 7px; + // margin: 1px; + // cursor: pointer; + // text-align: center; + // min-width: 1.4em; + // } + + .strong, + .heading { + font-weight: bold; + } + + .em { + font-style: italic; + } + + .userMarkOpen { + background: rgba(255, 255, 0, 0.267); + display: inline; + } + + .userMark { + background: rgba(255, 255, 0, 0.267); + font-size: 2px; + display: inline-grid; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: 10px; + min-height: 10px; + text-align: center; + align-content: center; + } + + footnote { + display: inline-block; + position: relative; + cursor: pointer; + + div { + padding: 0 !important; + } + } + + footnote::after { + content: counter(prosemirror-footnote); + vertical-align: super; + font-size: 75%; + counter-increment: prosemirror-footnote; + } + + .ProseMirror { + counter-reset: prosemirror-footnote; + } + + .footnote-tooltip { + cursor: auto; + font-size: 75%; + position: absolute; + left: -30px; + top: calc(100% + 10px); + background: silver; + padding: 3px; + border-radius: 2px; + max-width: 100px; + min-width: 50px; + width: max-content; + } + + .prosemirror-attribution { + font-size: 8px; + } + + .footnote-tooltip::before { + border: 5px solid silver; + border-top-width: 0px; + border-left-color: transparent; + border-right-color: transparent; + position: absolute; + top: -5px; + left: 27px; + content: " "; + height: 0; + width: 0; + } + + + .formattedTextBox-inlineComment { + position: relative; + width: 40px; + height: 20px; + &::before { + content: "→"; + } + &:hover { + background: orange; + } + } + + .formattedTextBox-summarizer { + opacity: 0.5; + position: relative; + width: 40px; + height: 20px; + &::after { + content: "←"; + } + } + + .formattedTextBox-summarizer-collapsed { + opacity: 0.5; + position: relative; + width: 40px; + height: 20px; + &::after { + content: "..."; + } + } + + .ProseMirror { + touch-action: none; + span { + font-family: inherit; + } + + ol, ul { + counter-reset: deci1 0 multi1 0; + padding-left: 1em; + font-family: inherit; + } + ol { + margin-left: 1em; + font-family: inherit; + } + + .decimal1-ol { counter-reset: deci1; p {display: inline; font-family: inherit} margin-left: 0; } + .decimal2-ol { counter-reset: deci2; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 1em;} + .decimal3-ol { counter-reset: deci3; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 2em;} + .decimal4-ol { counter-reset: deci4; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 3em;} + .decimal5-ol { counter-reset: deci5; p {display: inline; font-family: inherit} font-size: smaller; } + .decimal6-ol { counter-reset: deci6; p {display: inline; font-family: inherit} font-size: smaller; } + .decimal7-ol { counter-reset: deci7; p {display: inline; font-family: inherit} font-size: smaller; } + + .multi1-ol { counter-reset: multi1; p {display: inline; font-family: inherit} margin-left: 0; padding-left: 1.2em } + .multi2-ol { counter-reset: multi2; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 1.4em;} + .multi3-ol { counter-reset: multi3; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 2em;} + .multi4-ol { counter-reset: multi4; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 3.4em;} + + .decimal1:before { transition: 0.5s;counter-increment: deci1; display: inline-block; margin-left: -1em; width: 1em; content: counter(deci1) ". "; } + .decimal2:before { transition: 0.5s;counter-increment: deci2; display: inline-block; margin-left: -2.1em; width: 2.1em; content: counter(deci1) "."counter(deci2) ". "; } + .decimal3:before { transition: 0.5s;counter-increment: deci3; display: inline-block; margin-left: -2.85em;width: 2.85em; content: counter(deci1) "."counter(deci2) "."counter(deci3) ". "; } + .decimal4:before { transition: 0.5s;counter-increment: deci4; display: inline-block; margin-left: -3.85em;width: 3.85em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) ". "; } + .decimal5:before { transition: 0.5s;counter-increment: deci5; display: inline-block; margin-left: -2em; width: 5em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) ". "; } + .decimal6:before { transition: 0.5s;counter-increment: deci6; display: inline-block; margin-left: -2em; width: 6em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) "."counter(deci6) ". "; } + .decimal7:before { transition: 0.5s;counter-increment: deci7; display: inline-block; margin-left: -2em; width: 7em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) "."counter(deci6) "."counter(deci7) ". "; } + + .multi1:before { transition: 0.5s;counter-increment: multi1; display: inline-block; margin-left: -1em; width: 1.2em; content: counter(multi1, upper-alpha) ". "; } + .multi2:before { transition: 0.5s;counter-increment: multi2; display: inline-block; margin-left: -2em; width: 2em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) ". "; } + .multi3:before { transition: 0.5s;counter-increment: multi3; display: inline-block; margin-left: -2.85em; width:2.85em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) "."counter(multi3, lower-alpha) ". "; } + .multi4:before { transition: 0.5s;counter-increment: multi4; display: inline-block; margin-left: -4.2em; width: 4.2em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) "."counter(multi3, lower-alpha) "."counter(multi4, lower-roman) ". "; } + } +} diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index cdf231985..fc63dfbf5 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -4,11 +4,11 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { isEqual } from "lodash"; import { action, computed, IReactionDisposer, Lambda, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { baseKeymap } from "prosemirror-commands"; +import { baseKeymap, selectAll } from "prosemirror-commands"; import { history } from "prosemirror-history"; import { inputRules } from 'prosemirror-inputrules'; import { keymap } from "prosemirror-keymap"; -import { Fragment, Mark, Node, Slice } from "prosemirror-model"; +import { Fragment, Mark, Node, Slice, Schema } from "prosemirror-model"; import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from "prosemirror-state"; import { ReplaceStep } from 'prosemirror-transform'; import { EditorView } from "prosemirror-view"; @@ -16,13 +16,14 @@ import { DateField } from '../../../../fields/DateField'; import { DataSym, Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym, AclSym } from "../../../../fields/Doc"; import { documentSchema } from '../../../../fields/documentSchemas'; import applyDevTools = require("prosemirror-dev-tools"); +import { removeMarkWithAttrs } from "./prosemirrorPatches"; import { Id } from '../../../../fields/FieldSymbols'; import { InkTool } from '../../../../fields/InkField'; import { PrefetchProxy } from '../../../../fields/Proxy'; import { RichTextField } from "../../../../fields/RichTextField"; import { RichTextUtils } from '../../../../fields/RichTextUtils'; import { createSchema, makeInterface } from "../../../../fields/Schema"; -import { Cast, DateCast, NumCast, StrCast } from "../../../../fields/Types"; +import { Cast, DateCast, NumCast, StrCast, ScriptCast } from "../../../../fields/Types"; import { TraceMobx, OVERRIDE_ACL } from '../../../../fields/util'; import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnOne, returnZero, Utils, setupMoveUpEvents } from '../../../../Utils'; import { GoogleApiClientUtils, Pulls, Pushes } from '../../../apis/google_docs/GoogleApiClientUtils'; @@ -32,7 +33,7 @@ import { DocumentType } from '../../../documents/DocumentTypes'; import { DictationManager } from '../../../util/DictationManager'; import { DragManager } from "../../../util/DragManager"; import { makeTemplate } from '../../../util/DropConverter'; -import buildKeymap from "./ProsemirrorExampleTransfer"; +import buildKeymap, { updateBullets } from "./ProsemirrorExampleTransfer"; import RichTextMenu from './RichTextMenu'; import { RichTextRules } from "./RichTextRules"; @@ -56,8 +57,9 @@ import { DocumentButtonBar } from '../../DocumentButtonBar'; import { AudioBox } from '../AudioBox'; import { FieldView, FieldViewProps } from "../FieldView"; import "./FormattedTextBox.scss"; -import { FormattedTextBoxComment, formattedTextBoxCommentPlugin } from './FormattedTextBoxComment'; +import { FormattedTextBoxComment, formattedTextBoxCommentPlugin, findLinkMark } from './FormattedTextBoxComment'; import React = require("react"); +import { DocumentManager } from '../../../util/DocumentManager'; library.add(faEdit); library.add(faSmile, faTextHeight, faUpload); @@ -68,15 +70,10 @@ export interface FormattedTextBoxProps { xMargin?: number; // used to override document's settings for xMargin --- see CollectionCarouselView yMargin?: number; } - -const richTextSchema = createSchema({ - documentText: "string", -}); - export const GoogleRef = "googleDocId"; -type RichTextDocument = makeInterface<[typeof richTextSchema, typeof documentSchema]>; -const RichTextDocument = makeInterface(richTextSchema, documentSchema); +type RichTextDocument = makeInterface<[typeof documentSchema]>; +const RichTextDocument = makeInterface(documentSchema); type PullHandler = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => void; @@ -86,14 +83,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp public static blankState = () => EditorState.create(FormattedTextBox.Instance.config); public static Instance: FormattedTextBox; public ProseRef?: HTMLDivElement; + public get EditorView() { return this._editorView; } private _ref: React.RefObject<HTMLDivElement> = React.createRef(); private _scrollRef: React.RefObject<HTMLDivElement> = React.createRef(); private _editorView: Opt<EditorView>; private _applyingChange: boolean = false; private _searchIndex = 0; + private _cachedLinks: Doc[] = []; private _undoTyping?: UndoManager.Batch; private _disposers: { [name: string]: IReactionDisposer } = {}; - private dropDisposer?: DragManager.DragDropDisposer; + private _dropDisposer?: DragManager.DragDropDisposer; @computed get _recording() { return this.dataDoc.audioState === "recording"; } set _recording(value) { this.dataDoc.audioState = value ? "recording" : undefined; } @@ -102,6 +101,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp public static FocusedBox: FormattedTextBox | undefined; public static SelectOnLoad = ""; + public static PasteOnLoad: ClipboardEvent | undefined; public static SelectOnLoadChar = ""; public static IsFragment(html: string) { return html.indexOf("data-pm-slice") !== -1; @@ -144,6 +144,33 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp public get CurrentDiv(): HTMLDivElement { return this._ref.current!; } + // removes all hyperlink anchors for the removed linkDoc + // TODO: bcz: Argh... if a section of text has multiple anchors, this should just remove the intended one. + // but since removing one anchor from the list of attr anchors isn't implemented, this will end up removing nothing. + public RemoveLinkFromDoc(linkDoc?: Doc) { + const state = this._editorView?.state; + if (state && linkDoc && this._editorView) { + var allLinks: any[] = []; + state.doc.nodesBetween(0, state.doc.nodeSize - 2, (node: any, pos: number, parent: any) => { + const foundMark = findLinkMark(node.marks); + const newHrefs = foundMark?.attrs.allLinks.filter((a: any) => a.href.includes(linkDoc[Id])) || []; + allLinks = newHrefs.length ? newHrefs : allLinks; + return true; + }); + if (allLinks.length) { + this._editorView.dispatch(removeMarkWithAttrs(state.tr, 0, state.doc.nodeSize - 2, state.schema.marks.linkAnchor, { allLinks })); + } + } + } + // removes all the specified link referneces from the selection. + // NOTE: as above, this won't work correctly if there are marks with overlapping but not exact sets of link references. + public RemoveLinkFromSelection(allLinks: { href: string, title: string, linkId: string, targetId: string }[]) { + const state = this._editorView?.state; + if (state && this._editorView) { + this._editorView.dispatch(removeMarkWithAttrs(state.tr, state.selection.from, state.selection.to, state.schema.marks.link, { allLinks })); + } + } + linkOnDeselect: Map<string, string> = new Map(); doLinkOnDeselect() { @@ -180,8 +207,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this.linkOnDeselect.set(key, value); const id = Utils.GenerateDeterministicGuid(this.dataDoc[Id] + key); - const allHrefs = [{ href: Utils.prepend("/doc/" + id), title: value, targetId: id }]; - const link = this._editorView.state.schema.marks.link.create({ allHrefs, location: "onRight", title: value }); + const allLinks = [{ href: Utils.prepend("/doc/" + id), title: value, targetId: id }]; + const link = this._editorView.state.schema.marks.linkAnchor.create({ allLinks, location: "onRight", title: value }); const mval = this._editorView.state.schema.marks.metadataVal.create(); const offset = (tx.selection.to === range!.end - 1 ? -1 : 0); tx = tx.addMark(textEndSelection - value.length + offset, textEndSelection, link).addMark(textEndSelection - value.length + offset, textEndSelection, mval); @@ -202,13 +229,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp if (!this.dataDoc[AclSym]) { if (!this._applyingChange && json.replace(/"selection":.*/, "") !== curProto?.Data.replace(/"selection":.*/, "")) { this._applyingChange = true; - this.dataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now())); + (curText !== Cast(this.dataDoc[this.fieldKey], RichTextField)?.Text) && (this.dataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now()))); if ((!curTemp && !curProto) || curText || curLayout?.Data.includes("dash")) { // if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended) if (json !== curLayout?.Data) { !curText && tx.storedMarks?.map(m => m.type.name === "pFontSize" && (Doc.UserDoc().fontSize = this.layoutDoc._fontSize = m.attrs.fontSize)); !curText && tx.storedMarks?.map(m => m.type.name === "pFontFamily" && (Doc.UserDoc().fontFamily = this.layoutDoc._fontFamily = m.attrs.fontFamily)); this.dataDoc[this.props.fieldKey] = new RichTextField(json, curText); this.dataDoc[this.props.fieldKey + "-noTemplate"] = (curTemp?.Text || "") !== curText; // mark the data field as being split from the template if it has been edited + ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: curText }); } } else { // if we've deleted all the text in a note driven by a template, then restore the template data this.dataDoc[this.props.fieldKey] = undefined; @@ -248,8 +276,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const lastSel = Math.min(flattened.length - 1, this._searchIndex); this._searchIndex = ++this._searchIndex > flattened.length - 1 ? 0 : this._searchIndex; const alink = DocUtils.MakeLink({ doc: this.rootDoc }, { doc: target }, "automatic")!; - const allHrefs = [{ href: Utils.prepend("/doc/" + alink[Id]), title: "a link", targetId: target[Id], linkId: alink[Id] }]; - const link = this._editorView.state.schema.marks.link.create({ allHrefs, title: "a link", location }); + const allLinks = [{ href: Utils.prepend("/doc/" + alink[Id]), title: "a link", targetId: target[Id], linkId: alink[Id] }]; + const link = this._editorView.state.schema.marks.linkAnchor.create({ allLinks, title: "a link", location }); this._editorView.dispatch(tr.addMark(flattened[lastSel].from, flattened[lastSel].to, link)); } } @@ -269,12 +297,19 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } public unhighlightSearchTerms = () => { - if (this._editorView && (this._editorView as any).docView) { + if (window.screen.width < 600) null; + else if (this._editorView && (this._editorView as any).docView) { const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight); const activeMark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight, { selected: true }); const end = this._editorView.state.doc.nodeSize - 2; this._editorView.dispatch(this._editorView.state.tr.removeMark(0, end, mark).removeMark(0, end, activeMark)); } + if (FormattedTextBox.PasteOnLoad) { + const pdfDocId = FormattedTextBox.PasteOnLoad.clipboardData?.getData("dash/pdfOrigin"); + const pdfRegionId = FormattedTextBox.PasteOnLoad.clipboardData?.getData("dash/pdfRegion"); + FormattedTextBox.PasteOnLoad = undefined; + setTimeout(() => pdfDocId && pdfRegionId && this.addPdfReference(pdfDocId, pdfRegionId, undefined), 10); + } } adoptAnnotation = (start: number, end: number, mark: Mark) => { const view = this._editorView!; @@ -283,8 +318,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } protected createDropTarget = (ele: HTMLDivElement) => { this.ProseRef = ele; - this.dropDisposer?.(); - ele && (this.dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.layoutDoc)); + this._dropDisposer?.(); + ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.layoutDoc)); } @undoBatch @@ -610,9 +645,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp let tr = state.tr.addMark(sel.from, sel.to, splitter); sel.from !== sel.to && tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => { if (node.firstChild === null && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) { - const allHrefs = [{ href, title, targetId, linkId }]; - allHrefs.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.link.name)?.attrs.allHrefs ?? [])); - const link = state.schema.marks.link.create({ allHrefs, title, location, linkId }); + const allLinks = [{ href, title, targetId, linkId }]; + allLinks.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.linkAnchor.name)?.attrs.allLinks ?? [])); + const link = state.schema.marks.linkAnchor.create({ allLinks, title, location, linkId }); tr = tr.addMark(pos, pos + node.nodeSize, link); } }); @@ -622,6 +657,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } } componentDidMount() { + this._cachedLinks = DocListCast(this.Document.links); + this._disposers.links = reaction(() => DocListCast(this.Document.links), // if a link is deleted, then remove all hyperlinks that reference it from the text's marks + newLinks => { + this._cachedLinks.forEach(l => !newLinks.includes(l) && this.RemoveLinkFromDoc(l)); + this._cachedLinks = newLinks; + }); this._disposers.buttonBar = reaction( () => DocumentButtonBar.Instance, instance => { @@ -652,8 +693,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp incomingValue => { if (incomingValue !== undefined && this._editorView && !this._applyingChange) { const updatedState = JSON.parse(incomingValue); - this._editorView.updateState(EditorState.fromJSON(this.config, updatedState)); - this.tryUpdateHeight(); + if (JSON.stringify(this._editorView.state.toJSON()) !== JSON.stringify(updatedState)) { + this._editorView.updateState(EditorState.fromJSON(this.config, updatedState)); + this.tryUpdateHeight(); + } } } ); @@ -725,8 +768,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp return node.copy(content.frag); } const marks = [...node.marks]; - const linkIndex = marks.findIndex(mark => mark.type === editor.state.schema.marks.link); - return linkIndex !== -1 && marks[linkIndex].attrs.allRefs.find((item: { href: string }) => scrollToLinkID === item.href.replace(/.*\/doc\//, "")) ? node : undefined; + const linkIndex = marks.findIndex(mark => mark.type === editor.state.schema.marks.linkAnchor); + return linkIndex !== -1 && marks[linkIndex].attrs.allLinks.find((item: { href: string }) => scrollToLinkID === item.href.replace(/.*\/doc\//, "")) ? node : undefined; }; let start = 0; @@ -857,8 +900,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp handlePaste = (view: EditorView, event: Event, slice: Slice): boolean => { const cbe = event as ClipboardEvent; - const pdfDocId = cbe.clipboardData && cbe.clipboardData.getData("dash/pdfOrigin"); - const pdfRegionId = cbe.clipboardData && cbe.clipboardData.getData("dash/pdfRegion"); + const pdfDocId = cbe.clipboardData?.getData("dash/pdfOrigin"); + const pdfRegionId = cbe.clipboardData?.getData("dash/pdfRegion"); + return pdfDocId && pdfRegionId && this.addPdfReference(pdfDocId, pdfRegionId, slice) ? true : false; + } + + addPdfReference = (pdfDocId: string, pdfRegionId: string, slice?: Slice) => { + const view = this._editorView!; if (pdfDocId && pdfRegionId) { DocServer.GetRefField(pdfDocId).then(pdfDoc => { DocServer.GetRefField(pdfRegionId).then(pdfRegion => { @@ -866,17 +914,22 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp setTimeout(async () => { const targetField = Doc.LayoutFieldKey(pdfDoc); const targetAnnotations = await DocListCastAsync(pdfDoc[DataSym][targetField + "-annotations"]);// bcz: better to have the PDF's view handle updating its own annotations - targetAnnotations?.push(pdfRegion); + if (targetAnnotations) targetAnnotations.push(pdfRegion); + else Doc.AddDocToList(pdfDoc[DataSym], targetField + "-annotations", pdfRegion); }); const link = DocUtils.MakeLink({ doc: this.rootDoc }, { doc: pdfRegion }, "PDF pasted"); if (link) { - cbe.clipboardData!.setData("dash/linkDoc", link[Id]); const linkId = link[Id]; - const frag = addMarkToFrag(slice.content, (node: Node) => addLinkMark(node, StrCast(pdfDoc.title), linkId)); - slice = new Slice(frag, slice.openStart, slice.openEnd); - const tr = view.state.tr.replaceSelection(slice); - view.dispatch(tr.scrollIntoView().setMeta("paste", true).setMeta("uiEvent", "paste")); + const quote = view.state.schema.nodes.blockquote.create(); + quote.content = addMarkToFrag(slice?.content || view.state.doc.content, (node: Node) => addLinkMark(node, StrCast(pdfDoc.title), linkId)); + const newSlice = new Slice(Fragment.from(quote), slice?.openStart || 0, slice?.openEnd || 0); + if (slice) { + view.dispatch(view.state.tr.replaceSelection(newSlice).scrollIntoView().setMeta("paste", true).setMeta("uiEvent", "paste")); + } else { + selectAll(view.state, (tx: Transaction) => view.dispatch(tx.replaceSelection(newSlice).scrollIntoView())); + + } } } }); @@ -898,7 +951,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } const marks = [...node.marks]; const linkIndex = marks.findIndex(mark => mark.type.name === "link"); - const link = view.state.schema.mark(view.state.schema.marks.link, { href: Utils.prepend(`/doc/${linkId}`), location: "onRight", title: title, docref: true }); + const allLinks = [{ href: Utils.prepend(`/doc/${linkId}`), title, linkId }]; + const link = view.state.schema.mark(view.state.schema.marks.linkAnchor, { allLinks, location: "onRight", title, docref: true }); marks.splice(linkIndex === -1 ? 0 : linkIndex, 1, link); return node.mark(marks); } @@ -933,7 +987,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp clipboardTextSerializer: this.clipboardTextSerializer, handlePaste: this.handlePaste, }); - //applyDevTools.applyDevTools(this._editorView); + !Doc.UserDoc().noviceMode && applyDevTools.applyDevTools(this._editorView); const startupText = !rtfField && this._editorView && Field.toString(this.dataDoc[fieldKey] as Field); if (startupText) { const { state: { tr }, dispatch } = this._editorView; @@ -952,7 +1006,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } (selectOnLoad /* || !rtfField?.Text*/) && 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: Math.floor(Date.now() / 1000) })]; + if (!this._editorView!.state.storedMarks || !this._editorView!.state.storedMarks.some(mark => mark.type === schema.marks.user_mark)) { + this._editorView!.state.storedMarks = [...(this._editorView!.state.storedMarks ? this._editorView!.state.storedMarks : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })]; + } } getFont(font: string) { switch (font) { @@ -976,6 +1032,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp _downX = 0; _downY = 0; _break = false; + _collapsed = false; onPointerDown = (e: React.PointerEvent): void => { if (this._recording && !e.ctrlKey && e.button === 0) { this.stopDictation(true); @@ -1013,9 +1070,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const editor = this._editorView!; FormattedTextBoxComment.textBox = this; const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY }); - const node = pcords && editor.state.doc.nodeAt(pcords.pos); // get what prosemirror thinks the clicked node is (if it's null, then we didn't click on any text) - !this.props.isSelected(true) && editor.dispatch(editor.state.tr.setSelection(node && pcords ? - new NodeSelection(editor.state.doc.resolve(pcords.pos)) : new TextSelection(editor.state.doc.resolve(pcords?.pos || 0)))); + !this.props.isSelected(true) && editor.dispatch(editor.state.tr.setSelection(new TextSelection(editor.state.doc.resolve(pcords?.pos || 0)))); FormattedTextBoxComment.update(editor, undefined, (e.target as any)?.className === "prosemirror-dropdownlink" ? (e.target as any).href : ""); } (e.nativeEvent as any).formattedHandled = true; @@ -1026,6 +1081,40 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } @action + onDoubleClick = (e: React.MouseEvent): void => { + + this.doLinkOnDeselect(); + FormattedTextBox._downEvent = true; + FormattedTextBoxComment.textBox = this; + if (this.props.onClick && e.button === 0 && !this.props.isSelected(false)) { + e.preventDefault(); + } + if (e.button === 0 && this.props.isSelected(true) && !e.altKey && !e.ctrlKey && !e.metaKey) { + if (e.clientX < this.ProseRef!.getBoundingClientRect().right) { // stop propagation if not in sidebar + e.stopPropagation(); // if the text box is selected, then it consumes all down events + } + } + if (e.button === 2 || (e.button === 0 && e.ctrlKey)) { + e.preventDefault(); + } + FormattedTextBoxComment.Hide(); + if (FormattedTextBoxComment.linkDoc) { + if (FormattedTextBoxComment.linkDoc.type !== DocumentType.LINK) { + this.props.addDocTab(FormattedTextBoxComment.linkDoc, e.ctrlKey ? "inTab" : "onRight"); + } else { + DocumentManager.Instance.FollowLink(FormattedTextBoxComment.linkDoc, this.props.Document, + (doc: Doc, followLinkLocation: string) => this.props.addDocTab(doc, e.ctrlKey ? "inTab" : followLinkLocation)); + } + } + + (e.nativeEvent as any).formattedHandled = true; + + if (e.buttons === 1 && this.props.isSelected(true) && !e.altKey) { + e.stopPropagation(); + } + } + + @action onFocused = (e: React.FocusEvent): void => { FormattedTextBox.FocusedBox = this; this.tryUpdateHeight(); @@ -1058,9 +1147,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp static _bulletStyleSheet: any = addStyleSheet(); static _userStyleSheet: any = addStyleSheet(); - + _forceUncollapse = true; // if the cursor doesn't move between clicks, then the selection will disappear for some reason. This flags the 2nd click as happening on a selection which allows bullet points to toggle + _forceDownNode: Node | undefined; onClick = (e: React.MouseEvent): void => { - if ((this._editorView!.root as any).getSelection().isCollapsed) { // this is a hack to allow the cursor to be placed at the end of a document when the document ends in an inline dash comment. Apparently Chrome on Windows has a bug/feature which breaks this when clicking after the end of the text. + if (Math.abs(e.clientX - this._downX) > 4 || Math.abs(e.clientY - this._downY) > 4) { + this._forceDownNode = undefined; + return; + } + if (!this._forceUncollapse || (this._editorView!.root as any).getSelection().isCollapsed) { // this is a hack to allow the cursor to be placed at the end of a document when the document ends in an inline dash comment. Apparently Chrome on Windows has a bug/feature which breaks this when clicking after the end of the text. const pcords = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY }); const node = pcords && this._editorView!.state.doc.nodeAt(pcords.pos); // get what prosemirror thinks the clicked node is (if it's null, then we didn't click on any text) if (pcords && node?.type === this._editorView!.state.schema.nodes.dashComment) { @@ -1072,6 +1166,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp if (e.clientY > lastNode?.getBoundingClientRect().bottom) { // if we clicked below the last prosemirror div, then set the selection to be the end of the document this._editorView!.dispatch(this._editorView!.state.tr.setSelection(TextSelection.create(this._editorView!.state.doc, this._editorView!.state.doc.content.size))); } + } else if ([this._editorView!.state.schema.nodes.ordered_list, this._editorView!.state.schema.nodes.listItem].includes(node?.type) && + node !== (this._editorView!.state.selection as NodeSelection)?.node && pcords) { + this._editorView!.dispatch(this._editorView!.state.tr.setSelection(NodeSelection.create(this._editorView!.state.doc, pcords.pos!))); } } if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); return; } @@ -1079,12 +1176,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp if (this.props.isSelected(true)) { // if text box is selected, then it consumes all click events e.stopPropagation(); - this.hitBulletTargets(e.clientX, e.clientY, e.shiftKey, false); + this.hitBulletTargets(e.clientX, e.clientY, !this._editorView?.state.selection.empty || this._forceUncollapse, false, this._forceDownNode, e.shiftKey); } + this._forceUncollapse = !(this._editorView!.root as any).getSelection().isCollapsed; + this._forceDownNode = (this._editorView!.state.selection as NodeSelection)?.node; } // 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, collapse: boolean, highlightOnly: boolean) { + hitBulletTargets(x: number, y: number, collapse: boolean, highlightOnly: boolean, downNode: Node | undefined = undefined, selectOrderedList: boolean = false) { + this._forceUncollapse = false; clearStyleSheetRules(FormattedTextBox._bulletStyleSheet); const clickPos = this._editorView!.posAtCoords({ left: x, top: y }); let olistPos = clickPos?.pos; @@ -1100,20 +1200,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp $olistPos = this._editorView?.state.doc.resolve(($olistPos as any).path[($olistPos as any).path.length - 4]); } } + const listPos = this._editorView?.state.doc.resolve(clickPos.pos); const listNode = this._editorView?.state.doc.nodeAt(clickPos.pos); - if (olistNode && olistNode.type === this._editorView?.state.schema.nodes.ordered_list) { - if (!collapse) { - if (!highlightOnly) { - this._editorView.dispatch(this._editorView.state.tr.setSelection(new NodeSelection($olistPos!))); - } - addStyleSheetRule(FormattedTextBox._bulletStyleSheet, olistNode.attrs.mapStyle + olistNode.attrs.bulletStyle + ":hover:before", { background: "lightgray" }); - } else if (listNode && listNode.type === this._editorView.state.schema.nodes.list_item) { - if (!highlightOnly) { + if (olistNode && olistNode.type === this._editorView?.state.schema.nodes.ordered_list && listNode) { + if (!highlightOnly) { + if (selectOrderedList || (!collapse && listNode.attrs.visibility)) { + this._editorView.dispatch(this._editorView.state.tr.setSelection(new NodeSelection(selectOrderedList ? $olistPos! : listPos!))); + } else if (!listNode.attrs.visibility || downNode === listNode) { this._editorView.dispatch(this._editorView.state.tr.setNodeMarkup(clickPos.pos, listNode.type, { ...listNode.attrs, visibility: !listNode.attrs.visibility })); this._editorView.dispatch(this._editorView.state.tr.setSelection(TextSelection.create(this._editorView.state.doc, clickPos.pos))); } - addStyleSheetRule(FormattedTextBox._bulletStyleSheet, olistNode.attrs.mapStyle + olistNode.attrs.bulletStyle + ":hover:before", { background: "lightgray" }); } + addStyleSheetRule(FormattedTextBox._bulletStyleSheet, olistNode.attrs.mapStyle + olistNode.attrs.bulletStyle + ":hover:before", { background: "lightgray" }); } } } @@ -1121,7 +1219,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp e.stopPropagation(); const view = this._editorView as any; - // this interposes on prosemirror's upHandler to prevent prosemirror's up from invoked multiple times when there + // 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 (view.mouseDown) { const originalUpHandler = view.mouseDown.up; @@ -1144,18 +1242,27 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp }); } - public static HadSelection: boolean = false; - onBlur = (e: any) => { - FormattedTextBox.HadSelection = window.getSelection()?.toString() !== ""; - //DictationManager.Controls.stop(false); + public startUndoTypingBatch() { + this._undoTyping = UndoManager.StartBatch("undoTyping"); + } + + public endUndoTypingBatch() { + const wasUndoing = this._undoTyping; if (this._undoTyping) { this._undoTyping.end(); this._undoTyping = undefined; } + return wasUndoing; + } + public static HadSelection: boolean = false; + onBlur = (e: any) => { + FormattedTextBox.HadSelection = window.getSelection()?.toString() !== ""; + //DictationManager.Controls.stop(false); + this.endUndoTypingBatch(); this.doLinkOnDeselect(); // move the richtextmenu offscreen - if (!RichTextMenu.Instance.Pinned && !RichTextMenu.Instance.overMenu) RichTextMenu.Instance.jumpTo(-300, -300); + if (!RichTextMenu.Instance.Pinned) RichTextMenu.Instance.delayHide(); } _lastTimedMark: Mark | undefined = undefined; @@ -1189,10 +1296,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp // this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(mark)); if (!this._undoTyping) { - this._undoTyping = UndoManager.StartBatch("undoTyping"); + this.startUndoTypingBatch(); } } + ondrop = (eve: React.DragEvent) => { + this._editorView!.dispatch(updateBullets(this._editorView!.state.tr, this._editorView!.state.schema)); + eve.stopPropagation(); // drag n drop of text within text note will generate a new note if not caughst, as will dragging in from outside of Dash. + } onscrolled = (ev: React.UIEvent) => { this.layoutDoc._scrollTop = this._scrollRef.current!.scrollTop; } @@ -1200,13 +1311,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp tryUpdateHeight(limitHeight?: number) { let scrollHeight = this._ref.current?.scrollHeight; if (this.props.renderDepth && this.layoutDoc._autoHeight && !this.props.ignoreAutoHeight && scrollHeight) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation - scrollHeight = scrollHeight * NumCast(this.layoutDoc.scale, 1); + scrollHeight = scrollHeight * NumCast(this.layoutDoc._viewScale, 1); if (limitHeight && scrollHeight > limitHeight) { scrollHeight = limitHeight; this.layoutDoc.limitHeight = undefined; this.layoutDoc._autoHeight = false; } - const nh = this.layoutDoc.isTemplateForField ? 0 : NumCast(this.dataDoc._nativeHeight, 0); + const nh = this.layoutDoc.isTemplateForField ? 0 : NumCast(this.layoutDoc._nativeHeight, 0); const dh = NumCast(this.rootDoc._height, 0); const newHeight = Math.max(10, (nh ? dh / nh * scrollHeight : scrollHeight) + (this.props.ChromeHeight ? this.props.ChromeHeight() : 0)); if (this.rootDoc !== this.layoutDoc.doc && !this.layoutDoc.resolvedDataDoc) { @@ -1214,11 +1325,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp console.log("Delayed height adjustment..."); setTimeout(() => { this.rootDoc._height = newHeight; - this.dataDoc._nativeHeight = nh ? scrollHeight : undefined; + this.layoutDoc._nativeHeight = nh ? scrollHeight : undefined; }, 10); } else { - this.rootDoc._height = newHeight; - this.dataDoc._nativeHeight = nh ? scrollHeight : undefined; + this.layoutDoc._height = newHeight; + this.layoutDoc._nativeHeight = nh ? scrollHeight : undefined; } } } @@ -1229,7 +1340,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp @computed get sidebarColor() { return StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], "transparent")); } render() { TraceMobx(); - const scale = this.props.ContentScaling() * NumCast(this.layoutDoc.scale, 1); + const scale = this.props.ContentScaling() * NumCast(this.layoutDoc._viewScale, 1); const rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : ""; const interactive = Doc.GetSelectedTool() === InkTool.None && !this.layoutDoc.isBackground; if (this.props.isSelected()) { @@ -1258,7 +1369,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp color: this.props.color ? this.props.color : StrCast(this.layoutDoc[this.props.fieldKey + "-color"], this.props.hideOnLeave ? "white" : "inherit"), pointerEvents: interactive ? undefined : "none", fontSize: Cast(this.layoutDoc._fontSize, "number", null), - fontFamily: StrCast(this.layoutDoc._fontFamily, "inherit") + fontFamily: StrCast(this.layoutDoc._fontFamily, "inherit"), + transition: "opacity 1s" }} onContextMenu={this.specificContextMenu} onKeyDown={this.onKeyPress} @@ -1280,11 +1392,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } } })} + onDoubleClick={this.onDoubleClick} > - <div className={`formattedTextBox-outer`} style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: !this.props.isSelected() ? "none" : undefined }} onScroll={this.onscrolled} ref={this._scrollRef}> + <div className={`formattedTextBox-outer`} ref={this._scrollRef} + style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: !this.props.isSelected() ? "none" : undefined }} + onScroll={this.onscrolled} onDrop={this.ondrop} > <div className={`formattedTextBox-inner${rounded}${selclass}`} ref={this.createDropTarget} style={{ - padding: `${Math.max(0, NumCast(this.layoutDoc._yMargin, this.props.yMargin || 0) + selPad)}px ${NumCast(this.layoutDoc._xMargin, this.props.xMargin || 0) + selPad}px`, + padding: this.layoutDoc._textBoxPadding ? StrCast(this.layoutDoc._textBoxPadding) : `${Math.max(0, NumCast(this.layoutDoc._yMargin, this.props.yMargin || 0) + selPad)}px ${NumCast(this.layoutDoc._xMargin, this.props.xMargin || 0) + selPad}px`, pointerEvents: !this.props.isSelected() ? ((this.layoutDoc.isLinkButton || this.props.onClick) ? "none" : "all") : undefined }} /> @@ -1324,7 +1439,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp setTimeout(() => this._editorView!.focus(), 500); e.stopPropagation(); }} > - <FontAwesomeIcon className="formattedTExtBox-audioFont" + <FontAwesomeIcon className="formattedTextBox-audioFont" style={{ color: this._recording ? "red" : "blue", opacity: this._recording ? 1 : 0.5, display: this.props.isSelected() ? "" : "none" }} icon={"microphone"} size="sm" /> </div>} </div> diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss b/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss index 2dd63ec21..9089e7039 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss @@ -8,10 +8,12 @@ margin-bottom: 7px; -webkit-transform: translateX(-50%); transform: translateX(-50%); - } - .FormattedTextBox-tooltip:before { +} + +.FormattedTextBox-tooltip:before { content: ""; - height: 0; width: 0; + height: 0; + width: 0; position: absolute; left: 50%; margin-left: -5px; @@ -19,10 +21,12 @@ border: 5px solid transparent; border-bottom-width: 0; border-top-color: silver; - } - .FormattedTextBox-tooltip:after { +} + +.FormattedTextBox-tooltip:after { content: ""; - height: 0; width: 0; + height: 0; + width: 0; position: absolute; left: 50%; margin-left: -5px; @@ -30,4 +34,72 @@ border: 5px solid transparent; border-bottom-width: 0; border-top-color: white; - }
\ No newline at end of file +} + +.FormattedTextBoxComment-buttons { + display: none; + position: absolute; + top: 50%; + right: 0; + transform: translateY(-50%); + + .FormattedTextBoxComment-button { + width: 20px; + height: 20px; + margin: 0; + margin-right: 6px; + border-radius: 50%; + pointer-events: auto; + background-color: rgb(38, 40, 41); + color: rgb(178, 181, 184); + font-size: 65%; + transition: transform 0.2s; + text-align: center; + position: relative; + + // margin-top: "auto"; + // margin-bottom: "auto"; + // background: black; + // color: white; + // display: inline-block; + // border-radius: 18px; + // font-size: 12.5px; + // width: 18px; + // height: 18px; + // margin-top: auto; + // margin-bottom: auto; + // margin-right: 3px; + // cursor: pointer; + // transition: transform 0.2s; + + .FormattedTextBoxComment-fa-icon { + margin-top: "auto"; + margin-bottom: "auto"; + background: black; + color: white; + display: inline-block; + border-radius: 18px; + font-size: 12.5px; + width: 18px; + height: 18px; + margin-top: auto; + margin-bottom: auto; + margin-right: 3px; + cursor: pointer; + transition: transform 0.2s; + // position: absolute; + // top: 50%; + // left: 50%; + // transform: translate(-50%, -50%); + } + + &:last-child { + margin-right: 0; + } + + &:hover { + background: rgb(53, 146, 199); + ; + } + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx index 0d8e22251..56826e5c7 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx @@ -2,7 +2,7 @@ import { Mark, ResolvedPos } from "prosemirror-model"; import { EditorState, Plugin } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import * as ReactDOM from 'react-dom'; -import { Doc, DocCastAsync } from "../../../../fields/Doc"; +import { Doc, DocCastAsync, Opt } from "../../../../fields/Doc"; import { Cast, FieldValue, NumCast } from "../../../../fields/Types"; import { emptyFunction, returnEmptyString, returnFalse, Utils, emptyPath, returnZero, returnOne, returnEmptyFilter } from "../../../../Utils"; import { DocServer } from "../../../DocServer"; @@ -16,6 +16,11 @@ import React = require("react"); import { Docs } from "../../../documents/Documents"; import wiki from "wikijs"; import { DocumentType } from "../../../documents/DocumentTypes"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { action } from "mobx"; +import { LinkManager } from "../../../util/LinkManager"; +import { LinkDocPreview } from "../LinkDocPreview"; +import { DocumentLinksButton } from "../DocumentLinksButton"; export let formattedTextBoxCommentPlugin = new Plugin({ view(editorView) { return new FormattedTextBoxComment(editorView); } @@ -27,7 +32,7 @@ export function findUserMark(marks: Mark[]): Mark | undefined { return marks.find(m => m.attrs.userid); } export function findLinkMark(marks: Mark[]): Mark | undefined { - return marks.find(m => m.type === schema.marks.link); + return marks.find(m => m.type === schema.marks.linkAnchor); } export function findStartOfMark(rpos: ResolvedPos, view: EditorView, finder: (marks: Mark[]) => Mark | undefined) { let before = 0; @@ -62,6 +67,10 @@ export class FormattedTextBoxComment { static mark: Mark; static textBox: FormattedTextBox | undefined; static linkDoc: Doc | undefined; + + static _deleteRef: Opt<HTMLDivElement | null>; + static _followRef: Opt<HTMLDivElement | null>; + constructor(view: any) { if (!FormattedTextBoxComment.tooltip) { const root = document.getElementById("root"); @@ -75,8 +84,8 @@ export class FormattedTextBoxComment { FormattedTextBoxComment.tooltip.appendChild(FormattedTextBoxComment.tooltipText); FormattedTextBoxComment.tooltip.className = "FormattedTextBox-tooltip"; FormattedTextBoxComment.tooltip.style.pointerEvents = "all"; - FormattedTextBoxComment.tooltip.style.maxWidth = "350px"; - FormattedTextBoxComment.tooltip.style.maxHeight = "250px"; + FormattedTextBoxComment.tooltip.style.maxWidth = "200px"; + FormattedTextBoxComment.tooltip.style.maxHeight = "206px"; FormattedTextBoxComment.tooltip.style.width = "100%"; FormattedTextBoxComment.tooltip.style.height = "100%"; FormattedTextBoxComment.tooltip.style.overflow = "hidden"; @@ -87,12 +96,25 @@ export class FormattedTextBoxComment { const textBox = FormattedTextBoxComment.textBox; if (FormattedTextBoxComment.linkDoc && !keep && textBox) { if (FormattedTextBoxComment.linkDoc.author) { - if (FormattedTextBoxComment.linkDoc.type !== DocumentType.LINK) { - textBox.props.addDocTab(FormattedTextBoxComment.linkDoc, e.ctrlKey ? "inTab" : "onRight"); + + if (FormattedTextBoxComment._deleteRef && FormattedTextBoxComment._deleteRef.contains(e.target as any)) { + this.deleteLink(); + } else if (FormattedTextBoxComment._followRef && FormattedTextBoxComment._followRef.contains(e.target as any)) { + if (FormattedTextBoxComment.linkDoc.type !== DocumentType.LINK) { + textBox.props.addDocTab(FormattedTextBoxComment.linkDoc, e.ctrlKey ? "inTab" : "onRight"); + } else { + DocumentManager.Instance.FollowLink(FormattedTextBoxComment.linkDoc, textBox.props.Document, + (doc: Doc, followLinkLocation: string) => textBox.props.addDocTab(doc, e.ctrlKey ? "inTab" : followLinkLocation)); + } } else { - DocumentManager.Instance.FollowLink(FormattedTextBoxComment.linkDoc, textBox.props.Document, - (doc: Doc, followLinkLocation: string) => textBox.props.addDocTab(doc, e.ctrlKey ? "inTab" : followLinkLocation)); + if (FormattedTextBoxComment.linkDoc.type !== DocumentType.LINK) { + textBox.props.addDocTab(FormattedTextBoxComment.linkDoc, e.ctrlKey ? "inTab" : "onRight"); + } else { + DocumentManager.Instance.FollowLink(FormattedTextBoxComment.linkDoc, textBox.props.Document, + (doc: Doc, followLinkLocation: string) => textBox.props.addDocTab(doc, e.ctrlKey ? "inTab" : followLinkLocation)); + } } + } } else if (textBox && (FormattedTextBoxComment.tooltipText as any).href) { textBox.props.addDocTab(Docs.Create.WebDocument((FormattedTextBoxComment.tooltipText as any).href, { title: (FormattedTextBoxComment.tooltipText as any).href, _width: 200, _height: 400, UseCors: true }), "onRight"); @@ -106,6 +128,15 @@ export class FormattedTextBoxComment { } } + @action + deleteLink = () => { + FormattedTextBoxComment.linkDoc ? LinkManager.Instance.deleteLink(FormattedTextBoxComment.linkDoc) : null; + LinkDocPreview.LinkInfo = undefined; + DocumentLinksButton.EditLink = undefined; + //FormattedTextBoxComment.tooltipText = undefined; + FormattedTextBoxComment.Hide(); + } + public static Hide() { FormattedTextBoxComment.textBox = undefined; FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = "none"); @@ -177,11 +208,12 @@ export class FormattedTextBoxComment { // this checks if the selection is a hyperlink. If so, it displays the target doc's text for internal links, and the url of the target for external links. if (set === "none" && state.selection.$from) { nbef = findStartOfMark(state.selection.$from, view, findLinkMark); - const naft = findEndOfMark(state.selection.$from, view, findLinkMark); + const naft = findEndOfMark(state.selection.$from, view, findLinkMark) || nbef; let child: any = null; state.doc.nodesBetween(state.selection.from, state.selection.to, (node: any, pos: number, parent: any) => !child && node.marks.length && (child = node)); - const mark = child && findLinkMark(child.marks); - const href = mark?.attrs.allHrefs.find((item: { href: string }) => item.href)?.href || forceUrl; + child = child || (nbef && state.selection.$from.nodeBefore); + const mark = child ? findLinkMark(child.marks) : undefined; + const href = (!mark?.attrs.docref || naft === nbef) && mark?.attrs.allLinks.find((item: { href: string }) => item.href)?.href || forceUrl; if (forceUrl || (href && child && nbef && naft && mark?.attrs.showPreview)) { FormattedTextBoxComment.tooltipText.textContent = "external => " + href; (FormattedTextBoxComment.tooltipText as any).href = href; @@ -209,32 +241,87 @@ export class FormattedTextBoxComment { } if (target?.author) { FormattedTextBoxComment.showCommentbox("", view, nbef); - ReactDOM.render(<ContentFittingDocumentView - Document={target} - LibraryPath={emptyPath} - fitToBox={true} - moveDocument={returnFalse} - rootSelected={returnFalse} - ScreenToLocalTransform={Transform.Identity} - parentActive={returnFalse} - addDocument={returnFalse} - removeDocument={returnFalse} - addDocTab={returnFalse} - pinToPres={returnFalse} - dontRegisterView={true} - docFilters={returnEmptyFilter} - ContainingCollectionDoc={undefined} - ContainingCollectionView={undefined} - renderDepth={0} - PanelWidth={() => Math.min(350, NumCast(target._width, 350))} - PanelHeight={() => Math.min(250, NumCast(target._height, 250))} - focus={emptyFunction} - whenActiveChanged={returnFalse} - bringToFront={returnFalse} - ContentScaling={returnOne} - NativeWidth={returnZero} - NativeHeight={returnZero} - />, FormattedTextBoxComment.tooltipText); + const docPreview = <div style={{ backgroundColor: "white", border: "8px solid white" }}> + {target.title} + <div className="wrapper" style={{ float: "right" }}> + <div title="Delete link" className="FormattedTextBoxComment-button" style={{ + display: "inline", + paddingLeft: "6px", + paddingRight: "6px", + paddingTop: "2.5px", + paddingBottom: "2.5px", + width: "20px", + height: "20px", + margin: 0, + marginRight: "6px", + borderRadius: "50%", + pointerEvents: "auto", + backgroundColor: "rgb(38, 40, 41)", + color: "rgb(178, 181, 184)", + transition: "transform 0.2s", + textAlign: "center", + position: "relative" + }} ref={(r) => this._deleteRef = r}> + <FontAwesomeIcon className="FormattedTextBox-fa-icon" icon="trash" + size="sm" /></div> + <div title="Follow link" className="FormattedTextBoxComment-button" style={{ + display: "inline", + paddingLeft: "6px", + paddingRight: "6px", + paddingTop: "2.5px", + paddingBottom: "2.5px", + width: "20px", + height: "20px", + margin: 0, + marginRight: "6px", + borderRadius: "50%", + pointerEvents: "auto", + backgroundColor: "rgb(38, 40, 41)", + color: "rgb(178, 181, 184)", + transition: "transform 0.2s", + textAlign: "center", + position: "relative" + }} ref={(r) => this._followRef = r}> + <FontAwesomeIcon className="FormattedTextBox-fa-icon" icon="arrow-right" + size="sm" /> + </div> + </div> + <div className="wrapper" style={{ + maxWidth: "180px", maxHeight: "168px", overflow: "hidden", + overflowY: "hidden", paddingTop: "5px" + }}> + <ContentFittingDocumentView + Document={target} + LibraryPath={emptyPath} + fitToBox={true} + moveDocument={returnFalse} + rootSelected={returnFalse} + ScreenToLocalTransform={Transform.Identity} + parentActive={returnFalse} + addDocument={returnFalse} + removeDocument={returnFalse} + addDocTab={returnFalse} + pinToPres={returnFalse} + dontRegisterView={true} + docFilters={returnEmptyFilter} + ContainingCollectionDoc={undefined} + ContainingCollectionView={undefined} + renderDepth={0} + PanelWidth={() => Math.min(350, NumCast(target._width, 350))} + PanelHeight={() => Math.min(250, NumCast(target._height, 250))} + focus={emptyFunction} + whenActiveChanged={returnFalse} + bringToFront={returnFalse} + ContentScaling={returnOne} + NativeWidth={returnZero} + NativeHeight={returnZero} + /> + </div> + </div>; + FormattedTextBoxComment.showCommentbox("", view, nbef); + + ReactDOM.render(docPreview, FormattedTextBoxComment.tooltipText); + FormattedTextBoxComment.tooltip.style.width = NumCast(target._width) ? `${NumCast(target._width)}` : "100%"; FormattedTextBoxComment.tooltip.style.height = NumCast(target._height) ? `${NumCast(target._height)}` : "100%"; } @@ -248,4 +335,4 @@ export class FormattedTextBoxComment { } destroy() { } -} +}
\ No newline at end of file diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts index 9c91d8007..3f73ec436 100644 --- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts +++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts @@ -1,7 +1,6 @@ -import { chainCommands, exitCode, joinDown, joinUp, lift, selectParentNode, setBlockType, splitBlockKeepMarks, toggleMark, wrapIn, newlineInCode } from "prosemirror-commands"; +import { chainCommands, exitCode, joinDown, joinUp, lift, deleteSelection, joinBackward, selectNodeBackward, setBlockType, splitBlockKeepMarks, toggleMark, wrapIn, newlineInCode } from "prosemirror-commands"; import { liftTarget } from "prosemirror-transform"; import { redo, undo } from "prosemirror-history"; -import { undoInputRule } from "prosemirror-inputrules"; import { Schema } from "prosemirror-model"; import { liftListItem, sinkListItem } from "./prosemirrorPatches.js"; import { splitListItem, wrapInList, } from "prosemirror-schema-list"; @@ -17,13 +16,17 @@ const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : export type KeyMap = { [key: string]: any }; -export let updateBullets = (tx2: Transaction, schema: Schema, mapStyle?: string, from?: number, to?: number) => { +export let updateBullets = (tx2: Transaction, schema: Schema, assignedMapStyle?: string, from?: number, to?: number) => { + let mapStyle = assignedMapStyle; tx2.doc.descendants((node: any, offset: any, index: any) => { if ((from === undefined || to === undefined || (from <= offset + node.nodeSize && to >= offset)) && (node.type === schema.nodes.ordered_list || node.type === schema.nodes.list_item)) { const path = (tx2.doc.resolve(offset) as any).path; let depth = Array.from(path).reduce((p: number, c: any) => p + (c.hasOwnProperty("type") && c.type === schema.nodes.ordered_list ? 1 : 0), 0); - if (node.type === schema.nodes.ordered_list) depth++; - tx2.setNodeMarkup(offset, node.type, { ...node.attrs, mapStyle: mapStyle || node.attrs.mapStyle, bulletStyle: depth, }, node.marks); + if (node.type === schema.nodes.ordered_list) { + if (depth === 0 && !assignedMapStyle) mapStyle = node.attrs.mapStyle; + depth++; + } + tx2.setNodeMarkup(offset, node.type, { ...node.attrs, mapStyle, bulletStyle: depth, }, node.marks); } }); return tx2; @@ -43,7 +46,6 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, props: any //History commands bind("Mod-z", undo); - bind("Backspace", undoInputRule); bind("Shift-Mod-z", redo); !mac && bind("Mod-y", redo); @@ -175,6 +177,25 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, props: any } }); + // backspace = chainCommands(deleteSelection, joinBackward, selectNodeBackward); + bind("Backspace", (state: EditorState<S>, dispatch: (tx: Transaction<Schema<any, any>>) => void) => { + if (!deleteSelection(state, (tx: Transaction<Schema<any, any>>) => { + dispatch(updateBullets(tx, schema)); + })) { + if (!joinBackward(state, (tx: Transaction<Schema<any, any>>) => { + dispatch(updateBullets(tx, schema)); + })) { + if (!selectNodeBackward(state, (tx: Transaction<Schema<any, any>>) => { + dispatch(updateBullets(tx, schema)); + })) { + return false; + } + } + } + return true; + }); + + //newlineInCode, createParagraphNear, liftEmptyBlock, splitBlock //command to break line bind("Enter", (state: EditorState<S>, dispatch: (tx: Transaction<Schema<any, any>>) => void) => { if (addTextOnRight(false)) return true; @@ -190,11 +211,19 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, props: any const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks()); const cr = state.selection.$from.node().textContent.endsWith("\n"); if (cr || !newlineInCode(state, dispatch)) { - if (!splitListItem(schema.nodes.list_item)(state, dispatch)) { + if (!splitListItem(schema.nodes.list_item)(state, (tx2: Transaction) => { + const tx3 = updateBullets(tx2, schema); + marks && tx3.ensureMarks([...marks]); + marks && tx3.setStoredMarks([...marks]); + dispatch(tx3); + })) { + const fromattrs = state.selection.$from.node().attrs; if (!splitBlockKeepMarks(state, (tx3: Transaction) => { - splitMetadata(marks, tx3); - if (!liftListItem(schema.nodes.list_item)(tx3, dispatch as ((tx: Transaction<Schema<any, any>>) => void))) { - dispatch(tx3); + const tonode = tx3.selection.$to.node(); + const tx4 = tx3.setNodeMarkup(tx3.selection.to - 1, tonode.type, fromattrs, tonode.marks); + splitMetadata(marks, tx4); + if (!liftListItem(schema.nodes.list_item)(tx4, dispatch as ((tx: Transaction<Schema<any, any>>) => void))) { + dispatch(tx4); } })) { return false; @@ -257,19 +286,6 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, props: any return false; }); - // bind("^", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { - // let newNode = schema.nodes.footnote.create({}); - // if (dispatch && state.selection.from === state.selection.to) { - // let tr = state.tr; - // tr.replaceSelectionWith(newNode); // replace insertion with a footnote. - // dispatch(tr.setSelection(new NodeSelection( // select the footnote node to open its display - // tr.doc.resolve( // get the location of the footnote node by subtracting the nodesize of the footnote from the current insertion point anchor (which will be immediately after the footnote node) - // tr.selection.anchor - tr.selection.$anchor.nodeBefore!.nodeSize)))); - // return true; - // } - // return false; - // }); - return keys; } diff --git a/src/client/views/nodes/formattedText/RichTextMenu.scss b/src/client/views/nodes/formattedText/RichTextMenu.scss index 7a0718c16..fbc468292 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.scss +++ b/src/client/views/nodes/formattedText/RichTextMenu.scss @@ -77,6 +77,12 @@ color: white; } } + .richTextMenu-divider { + margin: auto; + border-left: solid #ffffff70 0.5px; + height: 20px; + width: 1px; + } } .link-menu { diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 839943aac..9075a6486 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -1,8 +1,8 @@ import React = require("react"); import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; -import { faBold, faCaretDown, faChevronLeft, faEyeDropper, faHighlighter, faIndent, faItalic, faLink, faPaintRoller, faPalette, faStrikethrough, faSubscript, faSuperscript, faUnderline } from "@fortawesome/free-solid-svg-icons"; +import { faBold, faCaretDown, faChevronLeft, faEyeDropper, faHighlighter, faOutdent, faIndent, faHandPointLeft, faHandPointRight, faItalic, faLink, faPaintRoller, faPalette, faStrikethrough, faSubscript, faSuperscript, faUnderline } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, observable } from "mobx"; +import { action, observable, IReactionDisposer, reaction } from "mobx"; import { observer } from "mobx-react"; import { lift, wrapIn } from "prosemirror-commands"; import { Mark, MarkType, Node as ProsNode, NodeType, ResolvedPos } from "prosemirror-model"; @@ -23,9 +23,10 @@ import { updateBullets } from "./ProsemirrorExampleTransfer"; import "./RichTextMenu.scss"; import { schema } from "./schema_rts"; import { TraceMobx } from "../../../../fields/util"; +import { UndoManager } from "../../../util/UndoManager"; const { toggleMark } = require("prosemirror-commands"); -library.add(faBold, faItalic, faChevronLeft, faUnderline, faStrikethrough, faSuperscript, faSubscript, faIndent, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink, faPaintRoller); +library.add(faBold, faItalic, faChevronLeft, faUnderline, faStrikethrough, faSuperscript, faSubscript, faOutdent, faIndent, faHandPointLeft, faHandPointRight, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink, faPaintRoller); @observer @@ -54,6 +55,7 @@ export default class RichTextMenu extends AntimodeMenu { @observable private activeFontSize: string = ""; @observable private activeFontFamily: string = ""; @observable private activeListType: string = ""; + @observable private activeAlignment: string = "left"; @observable private brushIsEmpty: boolean = true; @observable private brushMarks: Set<Mark> = new Set(); @@ -68,6 +70,8 @@ export default class RichTextMenu extends AntimodeMenu { @observable private currentLink: string | undefined = ""; @observable private showLinkDropdown: boolean = false; + _reaction: IReactionDisposer | undefined; + _delayHide = false; constructor(props: Readonly<{}>) { super(props); RichTextMenu.Instance = this; @@ -88,7 +92,7 @@ export default class RichTextMenu extends AntimodeMenu { { mark: schema.marks.pFontSize.create({ fontSize: 32 }), title: "Set font size", label: "32pt", command: this.changeFontSize }, { mark: schema.marks.pFontSize.create({ fontSize: 48 }), title: "Set font size", label: "48pt", command: this.changeFontSize }, { mark: schema.marks.pFontSize.create({ fontSize: 72 }), title: "Set font size", label: "72pt", command: this.changeFontSize }, - { mark: null, title: "", label: "various", command: unimplementedFunction, hidden: true }, + { mark: null, title: "", label: "...", command: unimplementedFunction, hidden: true }, { mark: null, title: "", label: "13pt", command: unimplementedFunction, hidden: true }, // this is here because the default size is 13, but there is no actual 13pt option ]; @@ -107,7 +111,7 @@ export default class RichTextMenu extends AntimodeMenu { this.listTypeOptions = [ { node: schema.nodes.ordered_list.create({ mapStyle: "bullet" }), title: "Set list type", label: ":", command: this.changeListType }, { node: schema.nodes.ordered_list.create({ mapStyle: "decimal" }), title: "Set list type", label: "1.1", command: this.changeListType }, - { node: schema.nodes.ordered_list.create({ mapStyle: "multi" }), title: "Set list type", label: "1.A", command: this.changeListType }, + { node: schema.nodes.ordered_list.create({ mapStyle: "multi" }), title: "Set list type", label: "A.1", command: this.changeListType }, //{ node: undefined, title: "Set list type", label: "Remove", command: this.changeListType }, ]; @@ -138,6 +142,16 @@ export default class RichTextMenu extends AntimodeMenu { ]; } + componentDidMount() { + this._reaction = reaction(() => SelectionManager.SelectedDocuments(), + () => this._delayHide && !(this._delayHide = false) && this.fadeOut(true)); + } + componentWillUnmount() { + this._reaction?.(); + } + + public delayHide = () => this._delayHide = true; + @action changeView(view: EditorView) { this.view = view; @@ -147,16 +161,6 @@ export default class RichTextMenu extends AntimodeMenu { this.updateFromDash(view, lastState, this.editorProps); } - public MakeLinkToSelection = (linkDocId: string, title: string, location: string, targetDocId: string): string => { - if (this.view) { - const link = this.view.state.schema.marks.link.create({ href: Utils.prepend("/doc/" + linkDocId), title: title, location: location, linkId: linkDocId, targetId: targetDocId }); - this.view.dispatch(this.view.state.tr.removeMark(this.view.state.selection.from, this.view.state.selection.to, this.view.state.schema.marks.link). - addMark(this.view.state.selection.from, this.view.state.selection.to, link)); - return this.view.state.selection.$from.nodeAfter?.text || ""; - } - return ""; - } - @action public async updateFromDash(view: EditorView, lastState: EditorState | undefined, props: any) { if (!view) { @@ -175,11 +179,13 @@ export default class RichTextMenu extends AntimodeMenu { // update active font family and size const active = this.getActiveFontStylesOnSelection(); - const activeFamilies = active?.get("families"); - const activeSizes = active?.get("sizes"); + const activeFamilies = active.activeFamilies; + const activeSizes = active.activeSizes; - this.activeFontFamily = !activeFamilies?.length ? "Arial" : activeFamilies.length === 1 ? String(activeFamilies[0]) : "various"; - this.activeFontSize = !activeSizes?.length ? "13pt" : activeSizes.length === 1 ? String(activeSizes[0]) : "various"; + this.activeListType = this.getActiveListStyle(); + this.activeAlignment = this.getActiveAlignment(); + this.activeFontFamily = !activeFamilies.length ? "Arial" : activeFamilies.length === 1 ? String(activeFamilies[0]) : "various"; + this.activeFontSize = !activeSizes.length ? "13pt" : activeSizes.length === 1 ? String(activeSizes[0]) : "..."; // update link in current selection const targetTitle = await this.getTextLinkTargetTitle(); @@ -210,8 +216,34 @@ export default class RichTextMenu extends AntimodeMenu { } // finds font sizes and families in selection + getActiveAlignment() { + if (this.view) { + const path = (this.view.state.selection.$from as any).path; + for (let i = path.length - 3; i < path.length; i -= 3) { + if (path[i]?.type === this.view.state.schema.nodes.paragraph) { + return path[i].attrs.align || "left"; + } + } + } + return "left"; + } + + // finds font sizes and families in selection + getActiveListStyle() { + if (this.view) { + const path = (this.view.state.selection.$from as any).path; + for (let i = 0; i < path.length; i += 3) { + if (path[i].type === this.view.state.schema.nodes.ordered_list) { + return path[i].attrs.mapStyle; + } + } + } + return "decimal"; + } + + // finds font sizes and families in selection getActiveFontStylesOnSelection() { - if (!this.view) return; + if (!this.view) return { activeFamilies: [], activeSizes: [] }; const activeFamilies: string[] = []; const activeSizes: string[] = []; @@ -225,10 +257,7 @@ export default class RichTextMenu extends AntimodeMenu { }); } - const styles = new Map<String, String[]>(); - styles.set("families", activeFamilies); - styles.set("sizes", activeSizes); - return styles; + return { activeFamilies, activeSizes }; } getMarksInSelection(state: EditorState<any>) { @@ -310,8 +339,11 @@ export default class RichTextMenu extends AntimodeMenu { function onClick(e: React.PointerEvent) { e.preventDefault(); e.stopPropagation(); - self.view && command && command(self.view.state, self.view.dispatch, self.view); - self.view && onclick && onclick(self.view.state, self.view.dispatch, self.view); + self.TextView.endUndoTypingBatch(); + UndoManager.RunInBatch(() => { + self.view && command && command(self.view.state, self.view.dispatch, self.view); + self.view && onclick && onclick(self.view.state, self.view.dispatch, self.view); + }, "rich text menu command"); self.setActiveMarkButtons(self.getActiveMarksOnSelection()); } @@ -338,16 +370,18 @@ export default class RichTextMenu extends AntimodeMenu { function onChange(e: React.ChangeEvent<HTMLSelectElement>) { e.stopPropagation(); e.preventDefault(); + self.TextView.endUndoTypingBatch(); options.forEach(({ label, mark, command }) => { if (e.target.value === label) { - self.view && mark && command(mark, self.view); + UndoManager.RunInBatch(() => self.view && mark && command(mark, self.view), "text mark dropdown"); } }); } return <select onChange={onChange} key={key}>{items}</select>; } - createNodesDropdown(activeOption: string, options: { node: NodeType | any | null, title: string, label: string, command: (node: NodeType | any) => void, hidden?: boolean, style?: {} }[], key: string): JSX.Element { + createNodesDropdown(activeMap: string, options: { node: NodeType | any | null, title: string, label: string, command: (node: NodeType | any) => void, hidden?: boolean, style?: {} }[], key: string): JSX.Element { + const activeOption = activeMap === "bullet" ? ":" : activeMap === "decimal" ? "1.1" : "A.1"; const items = options.map(({ title, label, hidden, style }) => { if (hidden) { return label === activeOption ? @@ -361,9 +395,10 @@ export default class RichTextMenu extends AntimodeMenu { const self = this; function onChange(val: string) { + self.TextView.endUndoTypingBatch(); options.forEach(({ label, node, command }) => { if (val === label) { - self.view && node && command(node); + UndoManager.RunInBatch(() => self.view && node && command(node), "nodes dropdown"); } }); } @@ -412,6 +447,85 @@ export default class RichTextMenu extends AntimodeMenu { dispatch?.(tr.replaceSelectionWith(newNode).removeMark(tr.selection.from - 1, tr.selection.from, mark)); return true; } + alignCenter = (state: EditorState<any>, dispatch: any) => { + return this.alignParagraphs(state, "center", dispatch); + } + alignLeft = (state: EditorState<any>, dispatch: any) => { + return this.alignParagraphs(state, "left", dispatch); + } + alignRight = (state: EditorState<any>, dispatch: any) => { + return this.alignParagraphs(state, "right", dispatch); + } + + alignParagraphs(state: EditorState<any>, align: "left" | "right" | "center", dispatch: any) { + var tr = state.tr; + state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { + if (node.type === schema.nodes.paragraph) { + tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, align }, node.marks); + return false; + } + return true; + }); + dispatch?.(tr); + return true; + } + + insetParagraph(state: EditorState<any>, dispatch: any) { + var tr = state.tr; + state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { + if (node.type === schema.nodes.paragraph) { + const inset = (node.attrs.inset ? Number(node.attrs.inset) : 0) + 10; + tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, inset }, node.marks); + return false; + } + return true; + }); + dispatch?.(tr); + return true; + } + outsetParagraph(state: EditorState<any>, dispatch: any) { + var tr = state.tr; + state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { + if (node.type === schema.nodes.paragraph) { + const inset = Math.max(0, (node.attrs.inset ? Number(node.attrs.inset) : 0) - 10); + tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, inset }, node.marks); + return false; + } + return true; + }); + dispatch?.(tr); + return true; + } + + indentParagraph(state: EditorState<any>, dispatch: any) { + var tr = state.tr; + state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { + if (node.type === schema.nodes.paragraph) { + const nodeval = node.attrs.indent ? Number(node.attrs.indent) : undefined; + const indent = !nodeval ? 25 : nodeval < 0 ? 0 : nodeval + 25; + tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, indent }, node.marks); + return false; + } + return true; + }); + dispatch?.(tr); + return true; + } + + hangingIndentParagraph(state: EditorState<any>, dispatch: any) { + var tr = state.tr; + state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { + if (node.type === schema.nodes.paragraph) { + const nodeval = node.attrs.indent ? Number(node.attrs.indent) : undefined; + const indent = !nodeval ? -25 : nodeval > 0 ? 0 : nodeval - 10; + tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, indent }, node.marks); + return false; + } + return true; + }); + dispatch?.(tr); + return true; + } insertBlockquote(state: EditorState<any>, dispatch: any) { const path = (state.selection.$from as any).path; @@ -423,6 +537,11 @@ export default class RichTextMenu extends AntimodeMenu { return true; } + insertHorizontalRule(state: EditorState<any>, dispatch: any) { + dispatch(state.tr.replaceSelectionWith(state.schema.nodes.horizontal_rule.create()).scrollIntoView()); + return true; + } + @action toggleBrushDropdown() { this.showBrushDropdown = !this.showBrushDropdown; } // todo: add brushes to brushMap to save with a style name @@ -439,7 +558,8 @@ export default class RichTextMenu extends AntimodeMenu { function onBrushClick(e: React.PointerEvent) { e.preventDefault(); e.stopPropagation(); - self.view && self.fillBrush(self.view.state, self.view.dispatch); + self.TextView.endUndoTypingBatch(); + UndoManager.RunInBatch(() => self.view && self.fillBrush(self.view.state, self.view.dispatch), "rt brush"); } let label = "Stored marks: "; @@ -506,19 +626,24 @@ export default class RichTextMenu extends AntimodeMenu { @action toggleColorDropdown() { this.showColorDropdown = !this.showColorDropdown; } @action setActiveColor(color: string) { this.activeFontColor = color; } + get TextView() { return (this.view as any).TextView as FormattedTextBox; } createColorButton() { const self = this; function onColorClick(e: React.PointerEvent) { e.preventDefault(); e.stopPropagation(); - self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch); + self.TextView.endUndoTypingBatch(); + UndoManager.RunInBatch(() => self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch), "rt menu color"); + self.TextView.EditorView!.focus(); } function changeColor(e: React.PointerEvent, color: string) { e.preventDefault(); e.stopPropagation(); self.setActiveColor(color); - self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch); + self.TextView.endUndoTypingBatch(); + UndoManager.RunInBatch(() => self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch), "rt menu color"); + self.TextView.EditorView!.focus(); } const button = @@ -563,13 +688,15 @@ export default class RichTextMenu extends AntimodeMenu { function onHighlightClick(e: React.PointerEvent) { e.preventDefault(); e.stopPropagation(); - self.view && self.insertHighlight(self.activeHighlightColor, self.view.state, self.view.dispatch); + self.TextView.endUndoTypingBatch(); + UndoManager.RunInBatch(() => self.view && self.insertHighlight(self.activeHighlightColor, self.view.state, self.view.dispatch), "rt highligher"); } function changeHighlight(e: React.PointerEvent, color: string) { e.preventDefault(); e.stopPropagation(); self.setActiveHighlight(color); - self.view && self.insertHighlight(self.activeHighlightColor, self.view.state, self.view.dispatch); + self.TextView.endUndoTypingBatch(); + UndoManager.RunInBatch(() => self.view && self.insertHighlight(self.activeHighlightColor, self.view.state, self.view.dispatch), "rt highlighter"); } const button = @@ -609,7 +736,8 @@ export default class RichTextMenu extends AntimodeMenu { const self = this; function onLinkChange(e: React.ChangeEvent<HTMLInputElement>) { - self.setCurrentLink(e.target.value); + self.TextView.endUndoTypingBatch(); + UndoManager.RunInBatch(() => self.setCurrentLink(e.target.value), "link change"); } const link = this.currentLink ? this.currentLink : ""; @@ -636,7 +764,7 @@ export default class RichTextMenu extends AntimodeMenu { const node = this.view.state.selection.$from.nodeAfter; const link = node && node.marks.find(m => m.type.name === "link"); if (link) { - const href = link.attrs.allHrefs.length > 0 ? link.attrs.allHrefs[0].href : undefined; + const href = link.attrs.allLinks.length > 0 ? link.attrs.allLinks[0].href : undefined; if (href) { if (href.indexOf(Utils.prepend("/doc/")) === 0) { const linkclicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0]; @@ -671,40 +799,28 @@ export default class RichTextMenu extends AntimodeMenu { } deleteLink = () => { - if (!this.view) return; - - const node = this.view.state.selection.$from.nodeAfter; - const link = node && node.marks.find(m => m.type === this.view!.state.schema.marks.link); - const href = link!.attrs.allHrefs.length > 0 ? link!.attrs.allHrefs[0].href : undefined; - if (href) { - if (href.indexOf(Utils.prepend("/doc/")) === 0) { - const linkclicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0]; - if (linkclicked) { - DocServer.GetRefField(linkclicked).then(async linkDoc => { - if (linkDoc instanceof Doc) { - LinkManager.Instance.deleteLink(linkDoc); - this.view!.dispatch(this.view!.state.tr.removeMark(this.view!.state.selection.from, this.view!.state.selection.to, this.view!.state.schema.marks.link)); - } - }); - } - } else { - if (node) { - const { tr, schema, selection } = this.view.state; - const extension = this.linkExtend(selection.$anchor, href); - this.view.dispatch(tr.removeMark(extension.from, extension.to, schema.marks.link)); - } + if (this.view) { + const link = this.view.state.selection.$from.nodeAfter?.marks.find(m => m.type === this.view!.state.schema.marks.linkAnchor); + if (link) { + const allLinks = link.attrs.allLinks.slice(); + this.TextView.RemoveLinkFromSelection(link.attrs.allLinks); + // bcz: Argh ... this will remove the link from the document even it's anchored somewhere else in the text which happens if only part of the anchor text was selected. + allLinks.filter((aref: any) => aref?.href.indexOf(Utils.prepend("/doc/")) === 0).forEach((aref: any) => { + const linkId = aref.href.replace(Utils.prepend("/doc/"), "").split("?")[0]; + linkId && DocServer.GetRefField(linkId).then(linkDoc => LinkManager.Instance.deleteLink(linkDoc as Doc)); + }); } } } linkExtend($start: ResolvedPos, href: string) { - const mark = this.view!.state.schema.marks.link; + const mark = this.view!.state.schema.marks.linkAnchor; let startIndex = $start.index(); let endIndex = $start.indexAfter(); - while (startIndex > 0 && $start.parent.child(startIndex - 1).marks.filter(m => m.type === mark && m.attrs.allHrefs.find((item: { href: string }) => item.href === href)).length) startIndex--; - while (endIndex < $start.parent.childCount && $start.parent.child(endIndex).marks.filter(m => m.type === mark && m.attrs.allHrefs.find((item: { href: string }) => item.href === href)).length) endIndex++; + while (startIndex > 0 && $start.parent.child(startIndex - 1).marks.filter(m => m.type === mark && m.attrs.allLinks.find((item: { href: string }) => item.href === href)).length) startIndex--; + while (endIndex < $start.parent.childCount && $start.parent.child(endIndex).marks.filter(m => m.type === mark && m.attrs.allLinks.find((item: { href: string }) => item.href === href)).length) endIndex++; let startPos = $start.start(); let endPos = startPos; @@ -744,7 +860,7 @@ export default class RichTextMenu extends AntimodeMenu { return ref_node; } - @action onPointerEnter(e: React.PointerEvent) { RichTextMenu.Instance.overMenu = true; } + @action onPointerEnter(e: React.PointerEvent) { RichTextMenu.Instance.overMenu = false; } @action onPointerLeave(e: React.PointerEvent) { RichTextMenu.Instance.overMenu = false; } @action @@ -768,26 +884,41 @@ export default class RichTextMenu extends AntimodeMenu { TraceMobx(); const row1 = <div className="antimodeMenu-row" key="row1" style={{ display: this.collapsed ? "none" : undefined }}>{[ !this.collapsed ? this.getDragger() : (null), - this.createButton("bold", "Bold", this.boldActive, toggleMark(schema.marks.strong)), - this.createButton("italic", "Italic", this.italicsActive, toggleMark(schema.marks.em)), - this.createButton("underline", "Underline", this.underlineActive, toggleMark(schema.marks.underline)), - this.createButton("strikethrough", "Strikethrough", this.strikethroughActive, toggleMark(schema.marks.strikethrough)), - this.createButton("superscript", "Superscript", this.superscriptActive, toggleMark(schema.marks.superscript)), - this.createButton("subscript", "Subscript", this.subscriptActive, toggleMark(schema.marks.subscript)), + !this.Pinned ? (null) : <> {[ + this.createButton("bold", "Bold", this.boldActive, toggleMark(schema.marks.strong)), + this.createButton("italic", "Italic", this.italicsActive, toggleMark(schema.marks.em)), + this.createButton("underline", "Underline", this.underlineActive, toggleMark(schema.marks.underline)), + this.createButton("strikethrough", "Strikethrough", this.strikethroughActive, toggleMark(schema.marks.strikethrough)), + this.createButton("superscript", "Superscript", this.superscriptActive, toggleMark(schema.marks.superscript)), + this.createButton("subscript", "Subscript", this.subscriptActive, toggleMark(schema.marks.subscript)), + <div className="richTextMenu-divider" /> + ]}</>, this.createColorButton(), this.createHighlighterButton(), this.createLinkButton(), this.createBrushButton(), - this.createButton("sort-amount-down", "Summarize", undefined, this.insertSummarizer), - this.createButton("quote-left", "Blockquote", undefined, this.insertBlockquote), + <div className="richTextMenu-divider" />, + this.createButton("align-left", "Align Left", this.activeAlignment === "left", this.alignLeft), + this.createButton("align-center", "Align Center", this.activeAlignment === "center", this.alignCenter), + this.createButton("align-right", "Align Right", this.activeAlignment === "right", this.alignRight), + this.createButton("indent", "Inset More", undefined, this.insetParagraph), + this.createButton("outdent", "Inset Less", undefined, this.outsetParagraph), + this.createButton("hand-point-left", "Hanging Indent", undefined, this.hangingIndentParagraph), + this.createButton("hand-point-right", "Indent", undefined, this.indentParagraph), ]}</div>; const row2 = <div className="antimodeMenu-row row-2" key="antimodemenu row2"> {this.collapsed ? this.getDragger() : (null)} <div key="row" style={{ display: this.collapsed ? "none" : undefined }}> + <div className="richTextMenu-divider" />, {[this.createMarksDropdown(this.activeFontSize, this.fontSizeOptions, "font size"), this.createMarksDropdown(this.activeFontFamily, this.fontFamilyOptions, "font family"), - this.createNodesDropdown(this.activeListType, this.listTypeOptions, "nodes")]} + <div className="richTextMenu-divider" />, + this.createNodesDropdown(this.activeListType, this.listTypeOptions, "nodes"), + this.createButton("sort-amount-down", "Summarize", undefined, this.insertSummarizer), + this.createButton("quote-left", "Blockquote", undefined, this.insertBlockquote), + this.createButton("minus", "Horizontal Rule", undefined, this.insertHorizontalRule), + <div className="richTextMenu-divider" />,]} </div> <div key="button"> {/* <div key="collapser"> @@ -817,7 +948,7 @@ interface ButtonDropdownProps { } @observer -class ButtonDropdown extends React.Component<ButtonDropdownProps> { +export class ButtonDropdown extends React.Component<ButtonDropdownProps> { @observable private showDropdown: boolean = false; private ref: HTMLDivElement | null = null; diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index ba3230801..ca30dde9d 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -275,11 +275,11 @@ export class RichTextRules { if (!fieldKey) { if (docid) { DocServer.GetRefField(docid).then(docx => { - const target = ((docx instanceof Doc) && docx) || Docs.Create.FreeformDocument([], { title: docid, _width: 500, _height: 500, _LODdisable: true, }, docid); + const target = ((docx instanceof Doc) && docx) || Docs.Create.FreeformDocument([], { title: docid, _width: 500, _height: 500, }, docid); DocUtils.Publish(target, docid, returnFalse, returnFalse); DocUtils.MakeLink({ doc: this.Document }, { doc: target }, "portal to"); }); - const link = state.schema.marks.link.create({ href: Utils.prepend("/doc/" + docid), location: "onRight", title: docid, targetId: docid }); + const link = state.schema.marks.linkAnchor.create({ href: Utils.prepend("/doc/" + docid), location: "onRight", title: docid, targetId: docid }); return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 2).addMark(start, end - 3, link); } return state.tr; @@ -305,7 +305,7 @@ export class RichTextRules { if (!fieldKey && !docid) return state.tr; docid && DocServer.GetRefField(docid).then(docx => { if (!(docx instanceof Doc && docx)) { - const docx = Docs.Create.FreeformDocument([], { title: docid, _width: 500, _height: 500, _LODdisable: true }, docid); + const docx = Docs.Create.FreeformDocument([], { title: docid, _width: 500, _height: 500 }, docid); DocUtils.Publish(docx, docid, returnFalse, returnFalse); } }); @@ -315,8 +315,6 @@ export class RichTextRules { return node ? state.tr.replaceRangeWith(start, end, dashDoc).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr; }), - - // create an inline view of a tag stored under the '#' field new InputRule( new RegExp(/#([a-zA-Z_\-]+[a-zA-Z_;\-0-9]*)\s$/), @@ -374,7 +372,6 @@ export class RichTextRules { new InputRule( new RegExp(/%\)/), (state, match, start, end) => { - return state.tr.deleteRange(start, end).removeStoredMark(state.schema.marks.summarizeInclusive.create()); }), diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts index 1de211f28..3d7d71b14 100644 --- a/src/client/views/nodes/formattedText/marks_rts.ts +++ b/src/client/views/nodes/formattedText/marks_rts.ts @@ -17,12 +17,12 @@ export const marks: { [index: string]: MarkSpec } = { return ["div", { className: "dummy" }, 0]; } }, - // :: MarkSpec A link. Has `href` and `title` attributes. `title` + // :: MarkSpec A linkAnchor. The anchor can have multiple links, where each link has an href URL and a title for use in menus and hover (Dash links have linkIDs & targetIDs). `title` // defaults to the empty string. Rendered and parsed as an `<a>` // element. - link: { + linkAnchor: { attrs: { - allHrefs: { default: [] as { href: string, title: string, linkId: string, targetId: string }[] }, + allLinks: { default: [] as { href: string, title: string, linkId: string, targetId: string }[] }, showPreview: { default: true }, location: { default: null }, title: { default: null }, @@ -31,22 +31,22 @@ export const marks: { [index: string]: MarkSpec } = { inclusive: false, parseDOM: [{ tag: "a[href]", getAttrs(dom: any) { - return { allHrefs: [{ href: dom.getAttribute("href"), title: dom.getAttribute("title"), linkId: dom.getAttribute("linkids"), targetId: dom.getAttribute("targetids") }], location: dom.getAttribute("location"), }; + return { allLinks: [{ href: dom.getAttribute("href"), title: dom.getAttribute("title"), linkId: dom.getAttribute("linkids"), targetId: dom.getAttribute("targetids") }], location: dom.getAttribute("location"), }; } }], toDOM(node: any) { - const targetids = node.attrs.allHrefs.reduce((p: string, item: { href: string, title: string, targetId: string, linkId: string }) => p + " " + item.targetId, ""); - const linkids = node.attrs.allHrefs.reduce((p: string, item: { href: string, title: string, targetId: string, linkId: string }) => p + " " + item.linkId, ""); + const targetids = node.attrs.allLinks.reduce((p: string, item: { href: string, title: string, targetId: string, linkId: string }) => p + " " + item.targetId, ""); + const linkids = node.attrs.allLinks.reduce((p: string, item: { href: string, title: string, targetId: string, linkId: string }) => p + " " + item.linkId, ""); return node.attrs.docref && node.attrs.title ? - ["div", ["span", `"`], ["span", 0], ["span", `"`], ["br"], ["a", { ...node.attrs, class: "prosemirror-attribution", title: `${node.attrs.title}` }, node.attrs.title], ["br"]] : - node.attrs.allHrefs.length === 1 ? - ["a", { ...node.attrs, class: linkids, targetids, title: `${node.attrs.title}`, href: node.attrs.allHrefs[0].href }, 0] : + ["div", ["span", `"`], ["span", 0], ["span", `"`], ["br"], ["a", { ...node.attrs, href: node.attrs.allLinks[0].href, class: "prosemirror-attribution" }, node.attrs.title], ["br"]] : + node.attrs.allLinks.length === 1 ? + ["a", { ...node.attrs, class: linkids, targetids, title: `${node.attrs.title}`, href: node.attrs.allLinks[0].href }, 0] : ["div", { class: "prosemirror-anchor" }, ["span", { class: "prosemirror-linkBtn" }, ["a", { ...node.attrs, class: linkids, targetids, title: `${node.attrs.title}` }, 0], ["input", { class: "prosemirror-hrefoptions" }], ], - ["div", { class: "prosemirror-links" }, ...node.attrs.allHrefs.map((item: { href: string, title: string }) => + ["div", { class: "prosemirror-links" }, ...node.attrs.allLinks.map((item: { href: string, title: string }) => ["a", { class: "prosemirror-dropdownlink", href: item.href }, item.title] )] ]; @@ -270,6 +270,7 @@ export const marks: { [index: string]: MarkSpec } = { userid: { default: "" }, modified: { default: "when?" }, // 1 second intervals since 1970 }, + excludes: "user_mark", group: "inline", toDOM(node: any) { const uid = node.attrs.userid.replace(".", "").replace("@", ""); diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts index 0a867912f..1af821738 100644 --- a/src/client/views/nodes/formattedText/nodes_rts.ts +++ b/src/client/views/nodes/formattedText/nodes_rts.ts @@ -302,7 +302,7 @@ export const nodes: { [index: string]: NodeSpec } = { mapStyle: { default: "decimal" }, // "decimal", "multi", "bullet" visibility: { default: true } }, - content: 'paragraph block*', + content: 'paragraph+ | (paragraph ordered_list)', parseDOM: [{ tag: "li", getAttrs(dom: any) { return { mapStyle: dom.getAttribute("data-mapStyle"), bulletStyle: dom.getAttribute("data-bulletStyle") }; @@ -310,9 +310,9 @@ export const nodes: { [index: string]: NodeSpec } = { }], toDOM(node: any) { const map = node.attrs.bulletStyle ? node.attrs.mapStyle + node.attrs.bulletStyle : ""; - return node.attrs.visibility ? - ["li", { class: `${map}`, "data-mapStyle": node.attrs.mapStyle, "data-bulletStyle": node.attrs.bulletStyle }, 0] : - ["li", { class: `${map}`, "data-mapStyle": node.attrs.mapStyle, "data-bulletStyle": node.attrs.bulletStyle }, "..."]; + return ["li", { class: `${map}`, "data-mapStyle": node.attrs.mapStyle, "data-bulletStyle": node.attrs.bulletStyle }, node.attrs.visibility ? 0 : + ["span", { style: `position: relative; width: 100%; height: 1.5em; overflow: hidden; display: ${node.attrs.mapStyle !== "bullet" ? "inline-block" : "list-item"}; text-overflow: ellipsis; white-space: pre` }, + `${node.firstChild?.textContent}...`]]; } }, };
\ No newline at end of file diff --git a/src/client/views/nodes/formattedText/prosemirrorPatches.js b/src/client/views/nodes/formattedText/prosemirrorPatches.js index 763961958..0969ea4ef 100644 --- a/src/client/views/nodes/formattedText/prosemirrorPatches.js +++ b/src/client/views/nodes/formattedText/prosemirrorPatches.js @@ -9,6 +9,7 @@ var prosemirrorModel = require('prosemirror-model'); exports.liftListItem = liftListItem; exports.sinkListItem = sinkListItem; exports.wrappingInputRule = wrappingInputRule; +exports.removeMarkWithAttrs = removeMarkWithAttrs; // :: (NodeType) → (state: EditorState, dispatch: ?(tr: Transaction)) → bool // Create a command to lift the list item around the selection up into // a wrapping list. @@ -139,3 +140,57 @@ function wrappingInputRule(regexp, nodeType, getAttrs, joinPredicate, customWith } +// :: ([Mark]) → ?Mark +// Tests whether there is a mark of this type in the given set. +function isInSetWithAttrs(mark, set, attrs) { + for (var i = 0; i < set.length; i++) { + if (set[i].type == mark) { + if (Array.from(Object.keys(attrs)).reduce((p, akey) => { + return p && JSON.stringify(set[i].attrs[akey]) === JSON.stringify(attrs[akey]); + }, true)) { + return set[i]; + } + } + } +}; + +// :: (number, number, ?union<Mark, MarkType>) → this +// Remove marks from inline nodes between `from` and `to`. When `mark` +// is a single mark, remove precisely that mark. When it is a mark type, +// remove all marks of that type. When it is null, remove all marks of +// any type. +function removeMarkWithAttrs(tr, from, to, mark, attrs) { + if (mark === void 0) mark = null; + + var matched = [], step = 0; + tr.doc.nodesBetween(from, to, function (node, pos) { + if (!node.isInline) { return } + step++; + var toRemove = null; + if (mark) { + if (isInSetWithAttrs(mark, node.marks, attrs)) { toRemove = [mark]; } + } else { + toRemove = node.marks; + } + if (toRemove && toRemove.length) { + var end = Math.min(pos + node.nodeSize, to); + for (var i = 0; i < toRemove.length; i++) { + var style = toRemove[i], found$1 = (void 0); + for (var j = 0; j < matched.length; j++) { + var m = matched[j]; + if (m.step == step - 1 && style.eq(matched[j].style)) { found$1 = m; } + } + if (found$1) { + found$1.to = end; + found$1.step = step; + } else { + matched.push({ style: style, from: Math.max(pos, from), to: end, step: step }); + } + } + } + }); + matched.forEach(function (m) { return tr.step(new prosemirrorTransform.RemoveMarkStep(m.from, m.to, m.style)); }); + return tr +}; + + |
