diff options
Diffstat (limited to 'src/client/views/nodes')
33 files changed, 973 insertions, 734 deletions
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index a48906372..260c98816 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -25,7 +25,7 @@ export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { renderCutoffProvider: (doc: Doc) => boolean; zIndex?: number; highlight?: boolean; - jitterRotation: number; + rotation: number; dataTransition?: string; replica: string; CollectionFreeFormView: CollectionFreeFormView; @@ -38,7 +38,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF { key: '_width' }, { key: 'x' }, { key: 'y' }, - { key: '_jitterRotation', val: 0 }, + { key: '_rotation', val: 0 }, { key: '_scrollTop' }, { key: 'opacity', val: 1 }, { key: 'viewScale', val: 1 }, @@ -55,7 +55,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF } get transform() { - return `translate(${this.X}px, ${this.Y}px) rotate(${NumCast(this.Document.jitterRotation, this.props.jitterRotation)}deg)`; + return `translate(${this.X}px, ${this.Y}px) rotate(${NumCast(this.Document.rotation, this.props.rotation)}deg)`; } get X() { return this.dataProvider?.x ?? NumCast(this.Document.x); @@ -222,7 +222,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF panelWidth = () => this.sizeProvider?.width || this.props.PanelWidth?.(); panelHeight = () => this.sizeProvider?.height || this.props.PanelHeight?.(); screenToLocalTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.X, -this.Y); - focusDoc = (doc: Doc) => this.props.focus(doc); + focusDoc = (doc: Doc) => this.props.focus(doc, {}); returnThis = () => this; render() { TraceMobx(); diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index 5ea6d567a..d74da9748 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -1,6 +1,6 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, observable } from 'mobx'; -import { observer } from "mobx-react"; +import { observer } from 'mobx-react'; import { Doc, Opt } from '../../../fields/Doc'; import { Cast, NumCast, StrCast } from '../../../fields/Types'; import { emptyFunction, OmitKeys, returnFalse, returnNone, setupMoveUpEvents } from '../../../Utils'; @@ -9,19 +9,20 @@ import { SnappingManager } from '../../util/SnappingManager'; import { undoBatch } from '../../util/UndoManager'; import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent'; import { StyleProp } from '../StyleProvider'; -import "./ComparisonBox.scss"; +import './ComparisonBox.scss'; import { DocumentView, DocumentViewProps } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; -import React = require("react"); - +import React = require('react'); @observer export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() { - public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ComparisonBox, fieldKey); } - protected _multiTouchDisposer?: import("../../util/InteractionUtils").InteractionUtils.MultiTouchEventDisposer | undefined; + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(ComparisonBox, fieldKey); + } + protected _multiTouchDisposer?: import('../../util/InteractionUtils').InteractionUtils.MultiTouchEventDisposer | undefined; private _disposers: (DragManager.DragDropDisposer | undefined)[] = [undefined, undefined]; - @observable _animating = ""; + @observable _animating = ''; protected createDropTarget = (ele: HTMLDivElement | null, fieldKey: string, disposerId: number) => { this._disposers[disposerId]?.(); @@ -29,7 +30,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl // create disposers identified by disposerId to remove drag & drop listeners this._disposers[disposerId] = DragManager.MakeDropTarget(ele, (e, dropEvent) => this.dropHandler(e, dropEvent, fieldKey), this.layoutDoc); } - } + }; @undoBatch private dropHandler = (event: Event, dropEvent: DragManager.DropEvent, fieldKey: string) => { @@ -40,88 +41,113 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl this.dataDoc[fieldKey] = droppedDocs[0]; } } - } + }; private registerSliding = (e: React.PointerEvent<HTMLDivElement>, targetWidth: number) => { - e.button !== 2 && setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, action(() => { - // on click, animate slider movement to the targetWidth - this._animating = "all 200ms"; - this.layoutDoc._clipWidth = targetWidth * 100 / this.props.PanelWidth(); - setTimeout(action(() => this._animating = ""), 200); - }), false); - } + e.button !== 2 && + setupMoveUpEvents( + this, + e, + this.onPointerMove, + emptyFunction, + action(() => { + // on click, animate slider movement to the targetWidth + this._animating = 'all 200ms'; + this.layoutDoc._clipWidth = (targetWidth * 100) / this.props.PanelWidth(); + setTimeout( + action(() => (this._animating = '')), + 200 + ); + }), + false + ); + }; @action private onPointerMove = ({ movementX }: PointerEvent) => { - const width = movementX * this.props.ScreenToLocalTransform().Scale + NumCast(this.layoutDoc._clipWidth) / 100 * this.props.PanelWidth(); + const width = movementX * this.props.ScreenToLocalTransform().Scale + (NumCast(this.layoutDoc._clipWidth) / 100) * this.props.PanelWidth(); if (width && width > 5 && width < this.props.PanelWidth()) { - this.layoutDoc._clipWidth = width * 100 / this.props.PanelWidth(); + this.layoutDoc._clipWidth = (width * 100) / this.props.PanelWidth(); } return false; - } + }; @undoBatch clearDoc = (e: React.MouseEvent, fieldKey: string) => { e.stopPropagation; // prevent click event action (slider movement) in registerSliding delete this.dataDoc[fieldKey]; - } + }; docStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any => { - if (property === StyleProp.PointerEvents) return "none"; + if (property === StyleProp.PointerEvents) return 'none'; return this.props.styleProvider?.(doc, props, property); - } + }; render() { - const clipWidth = NumCast(this.layoutDoc._clipWidth) + "%"; + const clipWidth = NumCast(this.layoutDoc._clipWidth) + '%'; const clearButton = (which: string) => { - return <div className={`clear-button ${which}`} - onPointerDown={e => e.stopPropagation()} // prevent triggering slider movement in registerSliding - onClick={e => this.clearDoc(e, which)}> - <FontAwesomeIcon className={`clear-button ${which}`} icon={"times"} size="sm" /> - </div>; + return ( + <div + className={`clear-button ${which}`} + onPointerDown={e => e.stopPropagation()} // prevent triggering slider movement in registerSliding + onClick={e => this.clearDoc(e, which)}> + <FontAwesomeIcon className={`clear-button ${which}`} icon={'times'} size="sm" /> + </div> + ); }; const displayDoc = (which: string) => { const whichDoc = Cast(this.dataDoc[which], Doc, null); // if (whichDoc?.type === DocumentType.MARKER) whichDoc = Cast(whichDoc.annotationOn, Doc, null); const targetDoc = Cast(whichDoc?.annotationOn, Doc, null) ?? whichDoc; - return whichDoc ? <> - <DocumentView - ref={(r) => { - whichDoc !== targetDoc && r?.focus(whichDoc); - }} - {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} - isContentActive={returnFalse} - isDocumentActive={returnFalse} - styleProvider={this.docStyleProvider} - Document={targetDoc} - DataDoc={undefined} - hideLinkButton={true} - pointerEvents={returnNone} /> - {clearButton(which)} - </> : // placeholder image if doc is missing + return whichDoc ? ( + <> + <DocumentView + ref={r => { + whichDoc !== targetDoc && r?.focus(whichDoc, {}); + }} + {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight']).omit} + isContentActive={returnFalse} + isDocumentActive={returnFalse} + styleProvider={this.docStyleProvider} + Document={targetDoc} + DataDoc={undefined} + hideLinkButton={true} + pointerEvents={returnNone} + /> + {clearButton(which)} + </> // placeholder image if doc is missing + ) : ( <div className="placeholder"> - <FontAwesomeIcon className="upload-icon" icon={"cloud-upload-alt"} size="lg" /> - </div>; + <FontAwesomeIcon className="upload-icon" icon={'cloud-upload-alt'} size="lg" /> + </div> + ); }; const displayBox = (which: string, index: number, cover: number) => { - return <div className={`${which}Box-cont`} key={which} style={{ width: this.props.PanelWidth() }} - onPointerDown={e => this.registerSliding(e, cover)} - ref={ele => this.createDropTarget(ele, which, index)} > - {displayDoc(which)} - </div>; + return ( + <div className={`${which}Box-cont`} key={which} style={{ width: this.props.PanelWidth() }} onPointerDown={e => this.registerSliding(e, cover)} ref={ele => this.createDropTarget(ele, which, index)}> + {displayDoc(which)} + </div> + ); }; return ( - <div className={`comparisonBox${this.props.isContentActive() || SnappingManager.GetIsDragging() ? "-interactive" : ""}` /* change className to easily disable/enable pointer events in CSS */}> - {displayBox(this.fieldKey === "data" ? "compareBox-after" : `${this.fieldKey}2`, 1, this.props.PanelWidth() - 3)} - <div className="clip-div" style={{ width: clipWidth, transition: this._animating, background: StrCast(this.layoutDoc._backgroundColor, "gray") }}> - {displayBox(this.fieldKey === "data" ? "compareBox-before" : `${this.fieldKey}1`, 0, 0)} + <div className={`comparisonBox${this.props.isContentActive() || SnappingManager.GetIsDragging() ? '-interactive' : ''}` /* change className to easily disable/enable pointer events in CSS */}> + {displayBox(this.fieldKey === 'data' ? 'compareBox-after' : `${this.fieldKey}2`, 1, this.props.PanelWidth() - 3)} + <div className="clip-div" style={{ width: clipWidth, transition: this._animating, background: StrCast(this.layoutDoc._backgroundColor, 'gray') }}> + {displayBox(this.fieldKey === 'data' ? 'compareBox-before' : `${this.fieldKey}1`, 0, 0)} </div> - <div className="slide-bar" style={{ left: `calc(${clipWidth} - 0.5px)`, cursor: NumCast(this.layoutDoc._clipWidth) < 5 ? "e-resize" : NumCast(this.layoutDoc._clipWidth) / 100 > (this.props.PanelWidth() - 5) / this.props.PanelWidth() ? "w-resize" : undefined }} - onPointerDown={e => this.registerSliding(e, this.props.PanelWidth() / 2)} /* if clicked, return slide-bar to center */ > + <div + className="slide-bar" + style={{ + left: `calc(${clipWidth} - 0.5px)`, + cursor: NumCast(this.layoutDoc._clipWidth) < 5 ? 'e-resize' : NumCast(this.layoutDoc._clipWidth) / 100 > (this.props.PanelWidth() - 5) / this.props.PanelWidth() ? 'w-resize' : undefined, + }} + onPointerDown={e => this.registerSliding(e, this.props.PanelWidth() / 2)} /* if clicked, return slide-bar to center */ + > <div className="slide-handle" /> </div> - </div >); + </div> + ); } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/DocumentLinksButton.scss b/src/client/views/nodes/DocumentLinksButton.scss index 0f3eb14bc..6da0b73ba 100644 --- a/src/client/views/nodes/DocumentLinksButton.scss +++ b/src/client/views/nodes/DocumentLinksButton.scss @@ -1,8 +1,9 @@ -@import "../global/globalCssVariables.scss"; +@import '../global/globalCssVariables.scss'; .documentLinksButton-wrapper { transform-origin: top left; width: 100%; + height: 100%; } .documentLinksButton-menu { @@ -21,6 +22,16 @@ position: absolute; } +.documentLinksButton-showCount { + position: absolute; + border-radius: 50%; + opacity: 0.9; + pointer-events: auto; + display: flex; + align-items: center; + background-color: $light-blue; + color: black; +} .documentLinksButton, .documentLinksButton-endLink, .documentLinksButton-startLink { @@ -34,6 +45,7 @@ text-transform: uppercase; letter-spacing: 2px; font-size: 10px; + transform-origin: top left; transition: transform 0.2s; text-align: center; display: flex; @@ -46,13 +58,10 @@ cursor: pointer; } } - .documentLinksButton { background-color: $dark-gray; color: $white; font-weight: bold; - width: 80%; - height: 80%; font-size: 100%; font-family: 'Roboto'; transition: 0.2s ease all; @@ -61,31 +70,20 @@ background-color: $black; } } - .documentLinksButton.startLink { background-color: $medium-blue; + width: 75%; + height: 75%; color: $white; font-weight: bold; - width: 80%; - height: 80%; font-size: 100%; transition: 0.2s ease all; - - &:hover { - background-color: $black; - } } .documentLinksButton-endLink { border: $medium-blue 2px dashed; color: $medium-blue; background-color: none !important; - width: 80%; - height: 80%; font-size: 100%; transition: 0.2s ease all; - - &:hover { - background-color: $light-blue; - } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index 9ffbf8e37..627487a9e 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -4,19 +4,18 @@ import { action, computed, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { Doc, Opt } from '../../../fields/Doc'; import { StrCast } from '../../../fields/Types'; -import { TraceMobx } from '../../../fields/util'; import { emptyFunction, returnFalse, setupMoveUpEvents, StopEvent } from '../../../Utils'; import { DocUtils } from '../../documents/Documents'; import { DragManager } from '../../util/DragManager'; import { Hypothesis } from '../../util/HypothesisUtils'; import { LinkManager } from '../../util/LinkManager'; import { undoBatch, UndoManager } from '../../util/UndoManager'; -import { Colors } from '../global/globalEnums'; import './DocumentLinksButton.scss'; import { DocumentView } from './DocumentView'; import { LinkDescriptionPopup } from './LinkDescriptionPopup'; import { TaskCompletionBox } from './TaskCompletedBox'; import React = require('react'); +import _ = require('lodash'); const higflyout = require('@hig/flyout'); export const { anchorPoints } = higflyout; @@ -24,10 +23,12 @@ export const Flyout = higflyout.default; interface DocumentLinksButtonProps { View: DocumentView; - Offset?: (number | undefined)[]; + Bottom?: boolean; AlwaysOn?: boolean; InMenu?: boolean; + OnHover?: boolean; StartLink?: boolean; //whether the link HAS been started (i.e. now needs to be completed) + ShowCount?: boolean; scaling?: () => number; // how uch doc is scaled so that link buttons can invert it } @observer @@ -39,9 +40,6 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp @observable public static AnnotationUri: string | undefined; @observable public static LinkEditorDocView: DocumentView | undefined; - @observable public static invisibleWebDoc: Opt<Doc>; - public static invisibleWebRef = React.createRef<HTMLDivElement>(); - @action @undoBatch onLinkButtonMoved = (e: PointerEvent) => { @@ -119,7 +117,6 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp DocumentLinksButton.StartLink = this.props.View.props.Document; DocumentLinksButton.StartLinkView = this.props.View; } - //action(() => Doc.BrushDoc(this.props.View.Document)); } }; @@ -256,28 +253,32 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp * todo:glr / anh seperate functionality such as onClick onPointerDown of link menu button */ @computed get linkButtonInner() { - const btnDim = '30px'; - const link = <img style={{ width: '22px', height: '16px' }} src={`/assets/${'link.png'}`} />; + const btnDim = 30; const isActive = DocumentLinksButton.StartLink === this.props.View.props.Document && this.props.StartLink; - return !this.props.InMenu ? ( - <div className="documentLinksButton-cont" style={{ left: this.props.Offset?.[0], top: this.props.Offset?.[1], right: this.props.Offset?.[2], bottom: this.props.Offset?.[3] }}> - <div - className={'documentLinksButton'} - onPointerDown={this.onLinkMenuOpen} - onClick={this.onLinkClick} - style={{ - backgroundColor: Colors.LIGHT_BLUE, - color: Colors.BLACK, - fontSize: '20px', - width: btnDim, - height: btnDim, - }}> - {Array.from(this.filteredLinks).length} - </div> + const scaling = Math.min(1, this.props.scaling?.() || 1); + const showLinkCount = (onHover?: boolean, offset?: boolean) => ( + <div + className="documentLinksButton-showCount" + onPointerDown={this.onLinkMenuOpen} + style={{ + fontSize: (onHover ? btnDim / 2 : 20) * scaling, + width: (onHover ? btnDim / 2 : btnDim) * scaling, + height: (onHover ? btnDim / 2 : btnDim) * scaling, + bottom: offset ? 5 * scaling : onHover ? (-btnDim / 2) * scaling : undefined, + }}> + <span style={{ width: '100%', display: 'inline-block', textAlign: 'center' }}>{Array.from(this.filteredLinks).length}</span> </div> + ); + return this.props.ShowCount ? ( + showLinkCount(this.props.OnHover, this.props.Bottom) ) : ( <div className="documentLinksButton-menu"> - {this.props.InMenu && !this.props.StartLink && DocumentLinksButton.StartLink !== this.props.View.props.Document ? ( //if the origin node is not this node + {this.props.StartLink ? ( //if link has been started from current node, then set behavior of link button to deactivate linking when clicked again + <div className={`documentLinksButton ${isActive ? `startLink` : ``}`} ref={this._linkButton} onPointerDown={isActive ? StopEvent : this.onLinkButtonDown} onClick={isActive ? this.clearLinks : this.onLinkClick}> + <FontAwesomeIcon className="documentdecorations-icon" icon="link" /> + </div> + ) : null} + {!this.props.StartLink && DocumentLinksButton.StartLink !== this.props.View.props.Document ? ( //if the origin node is not this node <div className={'documentLinksButton-endLink'} ref={this._linkButton} @@ -286,34 +287,25 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp <FontAwesomeIcon className="documentdecorations-icon" icon="link" /> </div> ) : null} - {this.props.InMenu && this.props.StartLink ? ( //if link has been started from current node, then set behavior of link button to deactivate linking when clicked again - <div className={`documentLinksButton ${isActive ? `startLink` : ``}`} ref={this._linkButton} onPointerDown={isActive ? StopEvent : this.onLinkButtonDown} onClick={isActive ? this.clearLinks : this.onLinkClick}> - <FontAwesomeIcon className="documentdecorations-icon" icon="link" /> - </div> - ) : null} </div> ); } render() { - TraceMobx(); - const menuTitle = this.props.StartLink ? 'Drag or tap to start link' : 'Tap to complete link'; const buttonTitle = 'Tap to view links; double tap to open link collection'; - const title = this.props.InMenu ? menuTitle : buttonTitle; + const title = this.props.ShowCount ? buttonTitle : menuTitle; //render circular tooltip if it isn't set to invisible and show the number of doc links the node has, and render inner-menu link button for starting/stopping links if currently in menu return !Array.from(this.filteredLinks).length && !this.props.AlwaysOn ? null : ( <div className="documentLinksButton-wrapper" style={{ - transform: `scale(${this.props.scaling?.() || 1})`, + position: this.props.InMenu ? 'relative' : 'absolute', + top: 0, + pointerEvents: 'none', }}> - {(this.props.InMenu && (DocumentLinksButton.StartLink || this.props.StartLink)) || (!DocumentLinksButton.LinkEditorDocView && !this.props.InMenu) ? ( - <Tooltip title={<div className="dash-tooltip">{title}</div>}>{this.linkButtonInner}</Tooltip> - ) : ( - this.linkButtonInner - )} + <Tooltip title={!DocumentLinksButton.LinkEditorDocView ? <div className="dash-tooltip">{title}</div> : <></>}>{this.linkButtonInner}</Tooltip> </div> ); } diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 145d8bf3d..a35400e72 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -15,7 +15,7 @@ import { BoolCast, Cast, ImageCast, NumCast, ScriptCast, StrCast } from '../../. import { AudioField } from '../../../fields/URLField'; import { GetEffectiveAcl, SharingPermissions, TraceMobx } from '../../../fields/util'; import { MobileInterface } from '../../../mobile/MobileInterface'; -import { emptyFunction, hasDescendantTarget, lightOrDark, OmitKeys, returnEmptyString, returnFalse, returnTrue, returnVal, simulateMouseClick, Utils } from '../../../Utils'; +import { emptyFunction, isTargetChildOf as isParentOf, lightOrDark, OmitKeys, returnEmptyString, returnFalse, returnTrue, returnVal, simulateMouseClick, Utils } from '../../../Utils'; import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; import { DocServer } from '../../DocServer'; import { Docs, DocUtils } from '../../documents/Documents'; @@ -147,6 +147,8 @@ export interface DocumentViewSharedProps { pinToPres: (document: Doc) => void; ScreenToLocalTransform: () => Transform; bringToFront: (doc: Doc, sendToBack?: boolean) => void; + xPadding?: number; + yPadding?: number; dropAction?: dropActionType; dontRegisterView?: boolean; hideLinkButton?: boolean; @@ -196,6 +198,7 @@ export interface DocumentViewInternalProps extends DocumentViewProps { NativeWidth: () => number; NativeHeight: () => number; isSelected: (outsideReaction?: boolean) => boolean; + isHovering: () => boolean; select: (ctrlPressed: boolean) => void; DocumentView: () => DocumentView; viewPath: () => DocumentView[]; @@ -255,9 +258,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @computed get borderRounding() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding); } - @computed get hideLinkButton() { - return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HideLinkButton + (this.props.isSelected() ? ':selected' : '')); - } @computed get widgetDecorations() { return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Decorations + (this.props.isSelected() ? ':selected' : '')); } @@ -566,7 +566,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } if (this.onDoubleClickHandler?.script && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes(ScriptingBox.name)) { // bcz: hack? don't execute script if you're clicking on a scripting box itself - const { clientX, clientY, shiftKey } = e; + const { clientX, clientY, shiftKey, altKey, ctrlKey } = e; const func = () => this.onDoubleClickHandler.script.run( { @@ -577,7 +577,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps documentView: this.props.DocumentView(), clientX, clientY, + altKey, shiftKey, + ctrlKey, }, console.log ); @@ -589,7 +591,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } } else if (this.onClickHandler?.script && !isScriptBox()) { // bcz: hack? don't execute script if you're clicking on a scripting box itself - const { clientX, clientY, shiftKey } = e; + const { clientX, clientY, shiftKey, altKey } = e; const func = () => this.onClickHandler.script.run( { @@ -602,6 +604,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps clientX, clientY, shiftKey, + altKey, }, console.log ).result?.select === true @@ -616,6 +619,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps }, 350); } else clickFunc(); } else if (this.allLinks && this.Document.type !== DocumentType.LINK && !isScriptBox() && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) { + SelectionManager.DeselectAll(); this.allLinks.length && LinkFollower.FollowLink(undefined, this.props.Document, this.props, e.altKey); } else { if ((this.layoutDoc.onDragStart || this.props.Document.rootDocument) && !(e.ctrlKey || e.button > 0)) { @@ -652,7 +656,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } this._downX = e.clientX; this._downY = e.clientY; - if (Doc.ActiveTool === InkTool.None && !(this.props.Document.rootDocument && !(e.ctrlKey || e.button > 0))) { + if ((Doc.ActiveTool === InkTool.None || this.props.addDocTab === returnFalse) && !(this.props.Document.rootDocument && !(e.ctrlKey || e.button > 0))) { // if this is part of a template, let the event go up to the tempalte root unless right/ctrl clicking if ( (this.props.isDocumentActive?.() || this.layoutDoc.onDragStart) && @@ -953,15 +957,58 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps moreItems.push({ description: 'Close', event: this.deleteClicked, icon: 'times' }); } !more && moreItems.length && cm.addItem({ description: 'More...', subitems: moreItems, icon: 'compass' }); - - const help = cm.findByDescription('Help...'); - const helpItems: ContextMenuProps[] = help && 'subitems' in help ? help.subitems : []; - helpItems.push({ description: 'Show Fields ', event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), 'add:right'), icon: 'layer-group' }); - !Doc.noviceMode && helpItems.push({ description: 'Text Shortcuts Ctrl+/', event: () => this.props.addDocTab(Docs.Create.PdfDocument('/assets/cheat-sheet.pdf', { _width: 300, _height: 300 }), 'add:right'), icon: 'keyboard' }); - !Doc.noviceMode && helpItems.push({ description: 'Print Document in Console', event: () => console.log(this.props.Document), icon: 'hand-point-right' }); - !Doc.noviceMode && helpItems.push({ description: 'Print DataDoc in Console', event: () => console.log(this.props.Document[DataSym]), icon: 'hand-point-right' }); - cm.addItem({ description: 'Help...', noexpand: true, subitems: helpItems, icon: 'question' }); } + const help = cm.findByDescription('Help...'); + const helpItems: ContextMenuProps[] = help && 'subitems' in help ? help.subitems : []; + helpItems.push({ description: 'Show Metadata', event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), 'add:right'), icon: 'layer-group' }); + !Doc.noviceMode && helpItems.push({ description: 'Text Shortcuts Ctrl+/', event: () => this.props.addDocTab(Docs.Create.PdfDocument('/assets/cheat-sheet.pdf', { _width: 300, _height: 300 }), 'add:right'), icon: 'keyboard' }); + !Doc.noviceMode && helpItems.push({ description: 'Print Document in Console', event: () => console.log(this.props.Document), icon: 'hand-point-right' }); + !Doc.noviceMode && helpItems.push({ description: 'Print DataDoc in Console', event: () => console.log(this.props.Document[DataSym]), icon: 'hand-point-right' }); + + let documentationDescription: string | undefined = undefined; + let documentationLink: string | undefined = undefined; + switch (this.props.Document.type) { + case DocumentType.COL: + documentationDescription = 'See collection documentation'; + documentationLink = 'https://brown-dash.github.io/Dash-Documentation/views/'; + break; + case DocumentType.PDF: + documentationDescription = 'See PDF node documentation'; + documentationLink = 'https://brown-dash.github.io/Dash-Documentation/documents/pdf/'; + break; + case DocumentType.VID: + documentationDescription = 'See video node documentation'; + documentationLink = 'https://brown-dash.github.io/Dash-Documentation/documents/tempMedia/video'; + break; + case DocumentType.AUDIO: + documentationDescription = 'See audio node documentation'; + documentationLink = 'https://brown-dash.github.io/Dash-Documentation/documents/tempMedia/audio'; + break; + case DocumentType.WEB: + documentationDescription = 'See webpage node documentation'; + documentationLink = 'https://brown-dash.github.io/Dash-Documentation/documents/webpage/'; + break; + case DocumentType.IMG: + documentationDescription = 'See image node documentation'; + documentationLink = 'https://brown-dash.github.io/Dash-Documentation/documents/images/'; + break; + case DocumentType.RTF: + documentationDescription = 'See text node documentation'; + documentationLink = 'https://brown-dash.github.io/Dash-Documentation/documents/text/'; + break; + } + // Add link to help documentation + if (documentationDescription && documentationLink) { + console.log('add documentation item'); + helpItems.push({ + description: documentationDescription, + event: () => { + window.open(documentationLink, '_blank'); + }, + icon: 'book', + }); + } + cm.addItem({ description: 'Help...', noexpand: true, subitems: helpItems, icon: 'question' }); if (!this.topMost) e?.stopPropagation(); // DocumentViews should stop propagation of this event cm.displayMenu((e?.pageX || pageX || 0) - 15, (e?.pageY || pageY || 0) - 15); @@ -1006,25 +1053,27 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps ? true : false; }; - linkButtonInverseScaling = () => (this.props.NativeDimScaling?.() || 1) * this.props.DocumentView().screenToLocalTransform().Scale; - @computed get contents() { - TraceMobx(); + get audioAnnoState() { + return this.dataDoc.audioAnnoState ?? 'stopped'; + } + @computed get audioAnnoView() { const audioAnnosCount = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations'], listSpec(AudioField), null)?.length; const audioTextAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations-text'], listSpec('string'), null); - const audioView = - (!this.props.isSelected() && !this._isHovering && this.dataDoc.audioAnnoState !== 2) || this.props.renderDepth === -1 || SnappingManager.GetIsDragging() || (!audioAnnosCount && !this.dataDoc.audioAnnoState) ? null : ( - <Tooltip title={<div>{audioTextAnnos?.lastElement()}</div>}> - <div className="documentView-audioBackground" onPointerDown={this.playAnnotation}> - <FontAwesomeIcon - className="documentView-audioFont" - style={{ color: [audioAnnosCount ? 'blue' : 'gray', 'green', 'red'][NumCast(this.dataDoc.audioAnnoState)] }} - icon={!audioAnnosCount ? 'microphone' : 'file-audio'} - size="sm" - /> - </div> - </Tooltip> - ); - + const audioIconColors = new Map<string, string>([ + ['recording', 'red'], + ['playing', 'green'], + ['stopped', audioAnnosCount ? 'blue' : 'gray'], + ]); + return this.props.renderDepth === -1 || SnappingManager.GetIsDragging() || (!this.props.isSelected() && !this.props.isHovering() && this.audioAnnoState !== 'recording') || (!audioAnnosCount && this.audioAnnoState === 'stopped') ? null : ( + <Tooltip title={<div>{audioTextAnnos?.lastElement()}</div>}> + <div className="documentView-audioBackground" onPointerDown={this.playAnnotation}> + <FontAwesomeIcon className="documentView-audioFont" style={{ color: audioIconColors.get(StrCast(this.audioAnnoState)) }} icon={!audioAnnosCount ? 'microphone' : 'file-audio'} size="sm" /> + </div> + </Tooltip> + ); + } + @computed get contents() { + TraceMobx(); return ( <div className="documentView-contentsView" @@ -1046,10 +1095,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps width={this.props.PanelWidth()} height={this.props.PanelHeight()} onError={(e: any) => { - setTimeout( - action(() => (this._retryThumb = 0)), - 0 - ); + setTimeout(action(() => (this._retryThumb = 0))); setTimeout( action(() => (this._retryThumb = 1)), 150 @@ -1062,7 +1108,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps {...this.props} docViewPath={this.props.viewPath} thumbShown={this.thumbShown} - isHovering={this.isHovering} + isHovering={this.props.isHovering} setContentView={this.setContentView} NativeDimScaling={this.props.NativeDimScaling} PanelHeight={this.panelHeight} @@ -1075,14 +1121,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps layoutKey={this.finalLayoutKey} /> {this.layoutDoc.hideAllLinks ? null : this.allLinkEndpoints} - {(!this.props.isSelected() && !this._isHovering) || this.hideLinkButton || this.props.renderDepth === -1 || SnappingManager.GetIsDragging() ? null : ( - <DocumentLinksButton - View={this.props.DocumentView()} - scaling={this.linkButtonInverseScaling} - Offset={[this.topMost ? 0 : !this.props.isSelected() ? -15 : -36, undefined, undefined, this.topMost ? 10 : !this.props.isSelected() ? -15 : -28]} - /> - )} - {audioView} + {this.audioAnnoView} </div> ); } @@ -1098,16 +1137,13 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps anchorPanelWidth = () => this.props.PanelWidth() || 1; anchorPanelHeight = () => this.props.PanelHeight() || 1; anchorStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any => { + // prettier-ignore switch (property) { - case StyleProp.ShowTitle: - return ''; - case StyleProp.PointerEvents: - return 'none'; - case StyleProp.LinkSource: - return this.props.Document; // pass the LinkSource to the LinkAnchorBox - default: - return this.props.styleProvider?.(doc, props, property); + case StyleProp.ShowTitle: return ''; + case StyleProp.PointerEvents: return 'none'; + case StyleProp.LinkSource: return this.props.Document; // pass the LinkSource to the LinkAnchorBox } + return this.props.styleProvider?.(doc, props, property); }; // We need to use allrelatedLinks to get not just links to the document as a whole, but links to // anchors that are not rendered as DocumentViews (marked as 'unrendered' with their 'annotationOn' set to this document). e.g., @@ -1161,7 +1197,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const self = this; const audioAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations'], listSpec(AudioField), null); const anno = audioAnnos.lastElement(); - if (anno instanceof AudioField && this.dataDoc.audioAnnoState === 0) { + if (anno instanceof AudioField && this.audioAnnoState === 'stopped') { new Howl({ src: [anno.url.href], format: ['mp3'], @@ -1169,12 +1205,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps loop: false, volume: 0.5, onend: function () { - runInAction(() => { - self.dataDoc.audioAnnoState = 0; - }); + runInAction(() => (self.dataDoc.audioAnnoState = 'stopped')); }, }); - this.dataDoc.audioAnnoState = 1; + this.dataDoc.audioAnnoState = 'playing'; } }; @@ -1213,12 +1247,12 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } } }; - runInAction(() => (dataDoc.audioAnnoState = 2)); + runInAction(() => (dataDoc.audioAnnoState = 'recording')); recorder.start(); setTimeout(() => { recorder.stop(); DictationManager.Controls.stop(false); - runInAction(() => (dataDoc.audioAnnoState = 0)); + runInAction(() => (dataDoc.audioAnnoState = 'stopped')); gumStream.getAudioTracks()[0].stop(); }, 5000); }); @@ -1259,91 +1293,72 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps SharingManager.Instance.users.find(users => users.user.email === this.dataDoc.author)?.sharingDoc.userColor, Doc.UserDoc().showTitle && [DocumentType.RTF, DocumentType.COL].includes(this.rootDoc.type as any) ? StrCast(Doc.SharingDoc().userColor) : 'rgba(0,0,0,0.4)' ); - const titleView = - !showTitle || Doc.noviceMode ? null : ( - <div - className={`documentView-titleWrapper${showTitleHover ? '-hover' : ''}`} - key="title" - style={{ - position: this.headerMargin ? 'relative' : 'absolute', - height: this.titleHeight, - width: !this.headerMargin ? `calc(100% - 18px)` : '100%', // leave room for annotation button - color: lightOrDark(background), - background, - pointerEvents: this.onClickHandler || this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined, - }}> - <EditableView - ref={this._titleRef} - contents={showTitle - .split(';') - .map(field => field.trim()) - .map(field => targetDoc[field]?.toString()) - .join('\\')} - display={'block'} - fontSize={10} - GetValue={() => (showTitle.split(';').length === 1 ? showTitle + '=' + Field.toString(targetDoc[showTitle.split(';')[0]] as any as Field) : '#' + showTitle)} - SetValue={undoBatch((input: string) => { - if (input?.startsWith('#')) { - if (this.props.showTitle) { - this.rootDoc._showTitle = input?.substring(1) ? input.substring(1) : undefined; - } else { - Doc.UserDoc().showTitle = input?.substring(1) ? input.substring(1) : 'creationDate'; - } + const titleView = !showTitle ? null : ( + <div + className={`documentView-titleWrapper${showTitleHover ? '-hover' : ''}`} + key="title" + style={{ + position: this.headerMargin ? 'relative' : 'absolute', + height: this.titleHeight, + width: !this.headerMargin ? `calc(100% - 18px)` : '100%', // leave room for annotation button + color: lightOrDark(background), + background, + pointerEvents: this.onClickHandler || this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined, + }}> + <EditableView + ref={this._titleRef} + contents={showTitle + .split(';') + .map(field => field.trim()) + .map(field => targetDoc[field]?.toString()) + .join('\\')} + display={'block'} + fontSize={10} + GetValue={() => { + this.props.select(false); + return showTitle.split(';').length === 1 ? showTitle + '=' + Field.toString(targetDoc[showTitle.split(';')[0]] as any as Field) : '#' + showTitle; + }} + SetValue={undoBatch((input: string) => { + if (input?.startsWith('#')) { + if (this.props.showTitle) { + this.rootDoc._showTitle = input?.substring(1) ? input.substring(1) : undefined; } else { - var value = input.replace(new RegExp(showTitle + '='), '') as string | number; - if (showTitle !== 'title' && Number(value).toString() === value) value = Number(value); - if (showTitle.includes('Date') || showTitle === 'author') return true; - Doc.SetInPlace(targetDoc, showTitle, value, true); + Doc.UserDoc().showTitle = input?.substring(1) ? input.substring(1) : 'creationDate'; } - return true; - })} - /> - </div> - ); + } else { + var value = input.replace(new RegExp(showTitle + '='), '') as string | number; + if (showTitle !== 'title' && Number(value).toString() === value) value = Number(value); + if (showTitle.includes('Date') || showTitle === 'author') return true; + Doc.SetInPlace(targetDoc, showTitle, value, true); + } + return true; + })} + /> + </div> + ); return this.props.hideTitle || (!showTitle && !showCaption) ? ( this.contents ) : ( <div className="documentView-styleWrapper"> - {!this.headerMargin ? ( - <> - {' '} - {this.contents} {titleView}{' '} - </> - ) : ( - <> - {' '} - {titleView} {this.contents}{' '} - </> - )} + {' '} + {!this.headerMargin ? this.contents : titleView} + {!this.headerMargin ? titleView : this.contents} + {' ' /* */} {captionView} </div> ); } - isHovering = () => this._isHovering; - @observable _isHovering = false; @observable _: string = ''; - _hoverTimeout: any = undefined; renderDoc = (style: object) => { TraceMobx(); const thumb = ImageCast(this.layoutDoc['thumb-frozen'], ImageCast(this.layoutDoc.thumb))?.url?.href.replace('.png', '_m.png'); const isButton = this.props.Document.type === DocumentType.FONTICON; - if (!(this.props.Document instanceof Doc) || GetEffectiveAcl(this.props.Document[DataSym]) === AclPrivate || (this.hidden && !this.props.treeViewDoc)) return null; + if (!(this.props.Document instanceof Doc) || GetEffectiveAcl(this.props.Document[DataSym]) === AclPrivate || this.hidden) return null; return ( this.docContents ?? ( <div className={`documentView-node${this.topMost ? '-topmost' : ''}`} id={this.props.Document[Id]} - onPointerEnter={action(() => { - clearTimeout(this._hoverTimeout); - this._isHovering = true; - })} - onPointerLeave={action(() => { - clearTimeout(this._hoverTimeout); - this._hoverTimeout = setTimeout( - action(() => (this._isHovering = false)), - 500 - ); - })} style={{ ...style, background: isButton || thumb ? undefined : this.backgroundColor, @@ -1364,29 +1379,23 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps }; render() { TraceMobx(); - const highlightIndex = this.props.LayoutTemplateString ? (Doc.IsHighlighted(this.props.Document) ? 6 : Doc.DocBrushStatus.unbrushed) : Doc.isBrushedHighlightedDegree(this.props.Document); // bcz: Argh!! need to identify a tree view doc better than a LayoutTemlatString - const highlightColor = ['transparent', 'rgb(68, 118, 247)', 'rgb(68, 118, 247)', 'orange', 'lightBlue'][highlightIndex]; - const highlightStyle = ['solid', 'dashed', 'solid', 'solid', 'solid'][highlightIndex]; - const excludeTypes = !this.props.treeViewDoc ? [DocumentType.FONTICON, DocumentType.INK] : [DocumentType.FONTICON]; - let highlighting = !this.props.disableDocBrushing && highlightIndex && !excludeTypes.includes(this.layoutDoc.type as any) && this.layoutDoc._viewType !== CollectionViewType.Linear; - highlighting = highlighting && this.props.focus !== emptyFunction && this.layoutDoc.title !== '[pres element template]'; // bcz: hack to turn off highlighting onsidebar panel documents. need to flag a document as not highlightable in a more direct way - + const highlighting = this.props.styleProvider?.(this.props.Document, this.props, StyleProp.Highlighting); const borderPath = this.props.styleProvider?.(this.props.Document, this.props, StyleProp.BorderPath) || { path: undefined }; - const boxShadow = this.props.treeViewDoc - ? null - : highlighting && this.borderRounding && highlightStyle !== 'dashed' - ? `0 0 0 ${highlightIndex}px ${highlightColor}` - : this.boxShadow || (this.props.Document.isTemplateForField ? 'black 0.2vw 0.2vw 0.8vw' : undefined); + const boxShadow = + this.props.treeViewDoc || !highlighting + ? null + : highlighting && this.borderRounding && highlighting.highlightStyle !== 'dashed' + ? `0 0 0 ${highlighting.highlightIndex}px ${highlighting.highlightColor}` + : this.boxShadow || (this.props.Document.isTemplateForField ? 'black 0.2vw 0.2vw 0.8vw' : undefined); const renderDoc = this.renderDoc({ borderRadius: this.borderRounding, - outline: highlighting && !this.borderRounding ? `${highlightColor} ${highlightStyle} ${highlightIndex}px` : 'solid 0px', - border: highlighting && this.borderRounding && highlightStyle === 'dashed' ? `${highlightStyle} ${highlightColor} ${highlightIndex}px` : undefined, + outline: highlighting && !this.borderRounding && highlighting ? `${highlighting.highlightColor} ${highlighting.highlightStyle} ${highlighting.highlightIndex}px` : 'solid 0px', + border: highlighting && this.borderRounding && highlighting && highlighting.highlightStyle === 'dashed' ? `${highlighting.highlightStyle} ${highlighting.highlightColor} ${highlighting.highlightIndex}px` : undefined, boxShadow, clipPath: borderPath.path ? `path('${borderPath.path}')` : undefined, }); const animRenderDoc = PresBox.Instance?.isActiveItemTarget(this.layoutDoc) ? PresBox.AnimationEffect(renderDoc, PresBox.Instance.activeItem) : renderDoc; - // Return surrounding highlight return ( <div className={`${DocumentView.ROOT_DIV} docView-hack`} @@ -1395,8 +1404,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps onKeyDown={this.onKeyDown} onPointerDown={this.onPointerDown} onClick={this.onClick} - onPointerEnter={action(e => !SnappingManager.GetIsDragging() && Doc.BrushDoc(this.props.Document))} - onPointerLeave={action(e => !hasDescendantTarget(e.nativeEvent.x, e.nativeEvent.y, this.ContentDiv) && Doc.UnBrushDoc(this.props.Document))} + onPointerEnter={e => (!SnappingManager.GetIsDragging() || DragManager.CanEmbed) && Doc.BrushDoc(this.props.Document)} + onPointerOver={e => (!SnappingManager.GetIsDragging() || DragManager.CanEmbed) && Doc.BrushDoc(this.props.Document)} + onPointerLeave={e => !isParentOf(this.ContentDiv, document.elementFromPoint(e.nativeEvent.x, e.nativeEvent.y)) && Doc.UnBrushDoc(this.props.Document)} style={{ display: this.hidden ? 'inline' : undefined, borderRadius: this.borderRounding, @@ -1406,12 +1416,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps animRenderDoc ) : ( <> - {/* <div style={{ clipPath: `path('${borderPath.fill}')` }}> - {animRenderDoc} - </div> */} {animRenderDoc} <div key="border2" className="documentView-customBorder" style={{ pointerEvents: 'none' }}> - <svg style={{ overflow: 'visible' }} viewBox={`0 0 ${this.props.PanelWidth()} ${this.props.PanelHeight()}`}> + <svg style={{ overflow: 'visible', height: '100%' }} viewBox={`0 0 ${this.props.PanelWidth()} ${this.props.PanelHeight()}`}> <path d={borderPath.path} style={{ stroke: 'black', fill: 'transparent', strokeWidth: borderPath.width }} /> </svg> </div> @@ -1502,6 +1509,18 @@ export class DocumentView extends React.Component<DocumentViewProps> { return this.props.fitWidth?.(this.rootDoc) || this.layoutDoc.fitWidth; } + @computed get hideLinkButton() { + return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HideLinkButton + (this.isSelected() ? ':selected' : '')); + } + linkButtonInverseScaling = () => (this.props.NativeDimScaling?.() || 1) * this.screenToLocalTransform().Scale; + + @computed get linkCountView() { + return (this.props.renderDepth === -1 || SnappingManager.GetIsDragging() || (this.isSelected() && this.props.renderDepth) || !this._isHovering || this.hideLinkButton) && + DocumentLinksButton.LinkEditorDocView?.rootDoc !== this.rootDoc ? null : ( + <DocumentLinksButton View={this} scaling={this.linkButtonInverseScaling} OnHover={true} Bottom={this.topMost} ShowCount={true} /> + ); + } + @computed get docViewPath(): DocumentView[] { return this.props.docViewPath ? [...this.props.docViewPath(), this] : [this]; } @@ -1515,7 +1534,7 @@ export class DocumentView extends React.Component<DocumentViewProps> { return this.docView?._componentView?.reverseNativeScaling?.() ? 0 : returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this.props.DataDoc, !this.fitWidth)); } @computed get shouldNotScale() { - return (this.fitWidth && !this.nativeWidth) || this.props.dontScaleFilter?.(this.Document) || this.props.treeViewDoc || [CollectionViewType.Docking].includes(this.Document._viewType as any); + return (this.fitWidth && !this.nativeWidth) || this.props.dontScaleFilter?.(this.Document) || [CollectionViewType.Docking].includes(this.Document._viewType as any); } @computed get effectiveNativeWidth() { return this.shouldNotScale ? 0 : this.nativeWidth || NumCast(this.layoutDoc.width); @@ -1560,7 +1579,7 @@ export class DocumentView extends React.Component<DocumentViewProps> { toggleNativeDimensions = () => this.docView && Doc.toggleNativeDimensions(this.layoutDoc, this.docView.NativeDimScaling, this.props.PanelWidth(), this.props.PanelHeight()); focus = (doc: Doc, options: DocFocusOptions) => this.docView?.focus(doc, options); getBounds = () => { - if (!this.docView || !this.docView.ContentDiv || this.props.Document.presBox || this.docView.props.treeViewDoc || Doc.AreProtosEqual(this.props.Document, Doc.UserDoc())) { + if (!this.docView || !this.docView.ContentDiv || this.props.Document.presBox || this.props.treeViewDoc || Doc.AreProtosEqual(this.props.Document, Doc.UserDoc())) { return undefined; } const xf = this.docView?.props.ScreenToLocalTransform().scale(this.nativeScaling).inverse(); @@ -1647,15 +1666,29 @@ export class DocumentView extends React.Component<DocumentViewProps> { Object.values(this._disposers).forEach(disposer => disposer?.()); !BoolCast(this.props.Document.dontRegisterView, this.props.dontRegisterView) && DocumentManager.Instance.RemoveView(this); } + _hoverTimeout: any = undefined; + isHovering = () => this._isHovering; + @observable _isHovering = false; render() { TraceMobx(); - const xshift = () => (Math.abs(this.Xshift) <= 0.001 ? this.props.PanelWidth() : undefined); - const yshift = () => (Math.abs(this.Yshift) <= 0.001 ? this.props.PanelHeight() : undefined); - const isPresTreeElement: boolean = this.props.treeViewDoc?.type === DocumentType.PRES; - const isButton: boolean = this.props.Document.type === DocumentType.FONTICON || this.props.Document._viewType === CollectionViewType.Linear; + const xshift = Math.abs(this.Xshift) <= 0.001 ? this.props.PanelWidth() : undefined; + const yshift = Math.abs(this.Yshift) <= 0.001 ? this.props.PanelHeight() : undefined; + const isButton = this.props.Document.type === DocumentType.FONTICON || this.props.Document._viewType === CollectionViewType.Linear; return ( - <div className="contentFittingDocumentView"> + <div + className="contentFittingDocumentView" + onPointerEnter={action(() => { + clearTimeout(this._hoverTimeout); + this._isHovering = true; + })} + onPointerLeave={action(() => { + clearTimeout(this._hoverTimeout); + this._hoverTimeout = setTimeout( + action(() => (this._isHovering = false)), + 500 + ); + })}> {!this.props.Document || !this.props.PanelWidth() ? null : ( <div className="contentFittingDocumentView-previewDoc" @@ -1663,11 +1696,11 @@ export class DocumentView extends React.Component<DocumentViewProps> { style={{ transition: this.props.dataTransition, transform: isButton ? undefined : `translate(${this.centeringX}px, ${this.centeringY}px)`, - width: isButton || isPresTreeElement ? '100%' : xshift() ?? `${(100 * (this.props.PanelWidth() - this.Xshift * 2)) / this.props.PanelWidth()}%`, + width: isButton ? '100%' : xshift ?? `${(100 * (this.props.PanelWidth() - this.Xshift * 2)) / this.props.PanelWidth()}%`, height: isButton || this.props.forceAutoHeight ? undefined - : yshift() ?? (this.fitWidth ? `${this.panelHeight}px` : `${(((100 * this.effectiveNativeHeight) / this.effectiveNativeWidth) * this.props.PanelWidth()) / this.props.PanelHeight()}%`), + : yshift ?? (this.fitWidth ? `${this.panelHeight}px` : `${(((100 * this.effectiveNativeHeight) / this.effectiveNativeWidth) * this.props.PanelWidth()) / this.props.PanelHeight()}%`), }}> <DocumentViewInternal {...this.props} @@ -1680,12 +1713,15 @@ export class DocumentView extends React.Component<DocumentViewProps> { NativeDimScaling={this.NativeDimScaling} isSelected={this.isSelected} select={this.select} + isHovering={this.isHovering} ScreenToLocalTransform={this.screenToLocalTransform} focus={this.props.focus || emptyFunction} ref={action((r: DocumentViewInternal | null) => r && (this.docView = r))} /> </div> )} + + {this.linkCountView} </div> ); } diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx index 0bd30bce9..c279341cc 100644 --- a/src/client/views/nodes/EquationBox.tsx +++ b/src/client/views/nodes/EquationBox.tsx @@ -7,6 +7,7 @@ import { Id } from '../../../fields/FieldSymbols'; import { NumCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; import { Docs } from '../../documents/Documents'; +import { undoBatch } from '../../util/UndoManager'; import { ViewBoxBaseComponent } from '../DocComponent'; import { LightboxView } from '../LightboxView'; import './EquationBox.scss'; @@ -45,7 +46,7 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { { fireImmediately: true } ); } - plot: any; + @action keyPressed = (e: KeyboardEvent) => { const _height = Number(getComputedStyle(this._ref.current!.element.current).height.replace('px', '')); @@ -76,6 +77,7 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { } if (e.key === 'Backspace' && !this.dataDoc.text) this.props.removeDocument?.(this.rootDoc); }; + @undoBatch onChange = (str: string) => { this.dataDoc.text = str; const style = this._ref.current && getComputedStyle(this._ref.current.element.current); diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 77aaa4441..e53422ab7 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -36,8 +36,6 @@ export interface FieldViewProps extends DocumentViewSharedProps { fontSize?: number; height?: number; width?: number; - xPadding?: number; - yPadding?: number; noSidebar?: boolean; dontScale?: boolean; dontSelectOnLoad?: boolean; // suppress selecting (e.g.,. text box) when loaded (and mark as not being associated with scrollTop document field) diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx index 15d0f88f6..e09155ac2 100644 --- a/src/client/views/nodes/FunctionPlotBox.tsx +++ b/src/client/views/nodes/FunctionPlotBox.tsx @@ -4,11 +4,14 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast } from '../../../fields/Doc'; import { documentSchema } from '../../../fields/documentSchemas'; +import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { createSchema, listSpec, makeInterface } from '../../../fields/Schema'; import { Cast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; import { Docs } from '../../documents/Documents'; +import { DragManager } from '../../util/DragManager'; +import { undoBatch } from '../../util/UndoManager'; import { ViewBoxBaseComponent } from '../DocComponent'; import { FieldView, FieldViewProps } from './FieldView'; @@ -33,7 +36,7 @@ export class FunctionPlotBox extends ViewBoxBaseComponent<FieldViewProps>() { componentDidMount() { this.props.setContentView?.(this); reaction( - () => [DocListCast(this.dataDoc[this.fieldKey]).lastElement()?.text, this.layoutDoc.width, this.layoutDoc.height, this.dataDoc.xRange, this.dataDoc.yRange], + () => [DocListCast(this.dataDoc[this.fieldKey]).map(doc => doc?.text), this.layoutDoc.width, this.layoutDoc.height, this.dataDoc.xRange, this.dataDoc.yRange], () => this.createGraph() ); } @@ -53,8 +56,9 @@ export class FunctionPlotBox extends ViewBoxBaseComponent<FieldViewProps>() { this._plotEle = ele || this._plotEle; const width = this.props.PanelWidth(); const height = this.props.PanelHeight(); - const fn = StrCast(DocListCast(this.dataDoc.data).lastElement()?.text, 'x^2').replace(/\\frac\{(.*)\}\{(.*)\}/, '($1/$2)'); + const fns = DocListCast(this.dataDoc.data).map(doc => StrCast(doc.text, 'x^2').replace(/\\frac\{(.*)\}\{(.*)\}/, '($1/$2)')); try { + this._plotEle.children.length && this._plotEle.removeChild(this._plotEle.children[0]); this._plot = functionPlot({ target: '#' + this._plotEle.id, width, @@ -62,17 +66,34 @@ export class FunctionPlotBox extends ViewBoxBaseComponent<FieldViewProps>() { xAxis: { domain: Cast(this.dataDoc.xRange, listSpec('number'), [-10, 10]) }, yAxis: { domain: Cast(this.dataDoc.xRange, listSpec('number'), [-1, 9]) }, grid: true, - data: [ - { - fn, - // derivative: { fn: "2 * x", updateOnMouseMove: true } - }, - ], + data: fns.map(fn => ({ + fn, + // derivative: { fn: "2 * x", updateOnMouseMove: true } + })), }); } catch (e) { console.log(e); } }; + + @undoBatch + drop = (e: Event, de: DragManager.DropEvent) => { + if (de.complete.docDragData?.droppedDocuments.length) { + e.stopPropagation(); // prevent parent Doc from registering new position so that it snaps back into place + de.complete.docDragData.droppedDocuments.map(doc => Doc.AddDocToList(this.dataDoc, this.props.fieldKey, doc)); + return false; + } + return false; + }; + + _dropDisposer: any; + protected createDropTarget = (ele: HTMLDivElement) => { + this._dropDisposer?.(); + if (ele) { + this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.layoutDoc); + } + // if (this.autoHeight) this.tryUpdateScrollHeight(); + }; @computed get theGraph() { return <div id={`${this._plotId}`} ref={r => r && this.createGraph(r)} style={{ position: 'absolute', width: '100%', height: '100%' }} onPointerDown={e => e.stopPropagation()} />; } @@ -80,6 +101,7 @@ export class FunctionPlotBox extends ViewBoxBaseComponent<FieldViewProps>() { TraceMobx(); return ( <div + ref={this.createDropTarget} style={{ pointerEvents: !this.isContentActive() ? 'all' : undefined, width: this.props.PanelWidth(), diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 959c641a8..461d6984d 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -8,7 +8,7 @@ import { List } from '../../../fields/List'; import { ObjectField } from '../../../fields/ObjectField'; import { createSchema } from '../../../fields/Schema'; import { ComputedField } from '../../../fields/ScriptField'; -import { Cast, NumCast } from '../../../fields/Types'; +import { Cast, NumCast, StrCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; import { emptyFunction, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../Utils'; @@ -77,10 +77,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp ({ forceFull, scrSize, selected }) => (this._curSuffix = selected ? '_o' : this.fieldKey === 'icon' ? '_m' : forceFull ? '_o' : scrSize < 0.25 ? '_s' : scrSize < 0.5 ? '_m' : scrSize < 0.8 ? '_l' : '_o'), { fireImmediately: true, delay: 1000 } ); + const layoutDoc = this.layoutDoc; this._disposers.path = reaction( () => ({ nativeSize: this.nativeSize, width: this.layoutDoc[WidthSym]() }), ({ nativeSize, width }) => { - if (true || !this.layoutDoc._height) { + if (layoutDoc === this.layoutDoc || !this.layoutDoc._height) { this.layoutDoc._height = (width * nativeSize.nativeHeight) / nativeSize.nativeWidth; } }, diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index d9f46509e..7d04c4b64 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -209,13 +209,18 @@ export class KeyValueBox extends React.Component<FieldViewProps> { } return parent; } - return this.createFieldView(DocCast(this.props.Document.data), rows.lastElement()); + return rows.length ? this.createFieldView(DocCast(this.props.Document.data), rows.lastElement()) : undefined; }; createFieldView = (templateDoc: Doc, row: KeyValuePair) => { const metaKey = row.props.keyName; - const fieldTemplate = Doc.MakeAlias(templateDoc); + const fieldTemplate = Doc.IsDelegateField(templateDoc, metaKey) ? Doc.MakeDelegate(templateDoc) : Doc.MakeAlias(templateDoc); fieldTemplate.title = metaKey; + fieldTemplate.fitWidth = true; + fieldTemplate._xMargin = 10; + fieldTemplate._yMargin = 10; + fieldTemplate._width = 100; + fieldTemplate._height = 40; fieldTemplate.layout = this.inferType(templateDoc[metaKey], metaKey); return fieldTemplate; }; diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx index 5102eae51..d6cf79f87 100644 --- a/src/client/views/nodes/LinkAnchorBox.tsx +++ b/src/client/views/nodes/LinkAnchorBox.tsx @@ -144,6 +144,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() { linkDoc: this.rootDoc, showHeader: true, location: [e.clientX, e.clientY + 20], + noPreview: false, }) } onPointerDown={this.onPointerDown} diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx index 27e79a83b..0e66214d1 100644 --- a/src/client/views/nodes/LinkDocPreview.tsx +++ b/src/client/views/nodes/LinkDocPreview.tsx @@ -26,6 +26,7 @@ interface LinkDocPreviewProps { location: number[]; hrefs?: string[]; showHeader?: boolean; + noPreview?: boolean; } @observer export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { @@ -111,6 +112,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { this._targetDoc = /*linkTarget?.type === DocumentType.MARKER &&*/ linkTarget?.annotationOn ? Cast(linkTarget.annotationOn, Doc, null) ?? linkTarget : linkTarget; } this._toolTipText = ''; + if (LinkDocPreview.LinkInfo?.noPreview) this.followLink(); } }) ); diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index c7001f846..fcb3ccb07 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -74,7 +74,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps if (oldDiv.className === 'pdfBox-ui' || oldDiv.className === 'pdfViewerDash-overlay-inking') { newDiv.style.display = 'none'; } - if (newDiv && newDiv.style) newDiv.style.overflow = 'hidden'; + if (newDiv?.style) newDiv.style.overflow = 'hidden'; if (oldDiv instanceof HTMLCanvasElement) { const canvas = oldDiv; const img = document.createElement('img'); // create a Image Element @@ -398,9 +398,12 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps </div> ); } - sidebarWidth = () => - !this.SidebarShown ? 0 : PDFBox.sidebarResizerWidth + (this._previewWidth ? PDFBox.openSidebarWidth : ((NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc)) * this.props.PanelWidth()) / NumCast(this.layoutDoc.nativeWidth)); - + sidebarWidth = () => { + if (!this.SidebarShown) return 0; + if (this._previewWidth) return PDFBox.sidebarResizerWidth + PDFBox.openSidebarWidth; // return default sidebar if previewing (as in viewing a link target) + const nativeDiff = NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc); + return PDFBox.sidebarResizerWidth + nativeDiff * (this.props.NativeDimScaling?.() || 1); + }; specificContextMenu = (e: React.MouseEvent): void => { const funcs: ContextMenuProps[] = []; funcs.push({ description: 'Copy path', event: () => this.pdfUrl && Utils.CopyText(Utils.prepend('') + this.pdfUrl.url.pathname), icon: 'expand-arrows-alt' }); @@ -483,7 +486,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps crop={this.crop} /> </div> - <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this.layoutDoc[WidthSym]()}%` }}> + <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this.props.PanelWidth()}%` }}> <SidebarAnnos ref={this._sidebarRef} {...this.props} diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx index 4883ad538..4c8a836f1 100644 --- a/src/client/views/nodes/ScriptingBox.tsx +++ b/src/client/views/nodes/ScriptingBox.tsx @@ -115,6 +115,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable @action componentDidMount() { + this.props.setContentView?.(this); this.rawText = this.rawScript; const observer = new _global.ResizeObserver( action((entries: any) => { @@ -480,6 +481,10 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable return value; } + scrollFocus = () => { + return undefined; + }; + getSuggestedParams(pos: number) { const firstScript = this.rawText.slice(0, pos); const indexP = firstScript.lastIndexOf('.'); diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index f7f558bb4..70ac84fa4 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -9,7 +9,7 @@ import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; import { Cast, NumCast, StrCast } from '../../../fields/Types'; import { AudioField, ImageField, VideoField } from '../../../fields/URLField'; -import { emptyFunction, formatTime, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../Utils'; +import { emptyFunction, formatTime, OmitKeys, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; import { Networking } from '../../Network'; @@ -935,7 +935,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp className="videoBox-ui" style={{ transformOrigin: 'top left', - transform: `rotate(${NumCast(this.rootDoc.jitterRotation)}deg) translate(${-(xRight - xPos) + 10}px, ${yBot - yMid - uiHeight - uiMargin}px)`, + transform: `rotate(${NumCast(this.rootDoc.rotation)}deg) translate(${-(xRight - xPos) + 10}px, ${yBot - yMid - uiHeight - uiMargin}px)`, left: xPos, top: yMid, height: uiHeight, @@ -1107,6 +1107,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp addDocument={this.addDocWithTimecode} finishMarquee={this.finishMarquee} savedAnnotations={this.savedAnnotations} + selectionText={returnEmptyString} annotationLayer={this._annotationLayer.current} mainCont={this._mainCont.current} anchorMenuCrop={this.crop} diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 460edb7c2..5ce6a0eb1 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -11,7 +11,7 @@ import { listSpec } from '../../../fields/Schema'; import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types'; import { ImageField, WebField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, getWordAtPoint, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, smoothScroll, StopEvent, Utils } from '../../../Utils'; +import { emptyFunction, getWordAtPoint, OmitKeys, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, smoothScroll, StopEvent, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { ScriptingGlobals } from '../../util/ScriptingGlobals'; import { SnappingManager } from '../../util/SnappingManager'; @@ -156,6 +156,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps this.rootDoc.thumbLockout = true; // lock to prevent multiple thumb updates. CreateImage(this._webUrl.endsWith('/') ? this._webUrl.substring(0, this._webUrl.length - 1) : this._webUrl, this._iframe.contentDocument?.styleSheets ?? [], htmlString, nativeWidth, nativeHeight, scrollTop) .then((data_url: any) => { + if (data_url.includes('<!DOCTYPE')) { + console.log('BAD DATA IN THUMB CREATION'); + return; + } VideoBox.convertDataUri(data_url, this.layoutDoc[Id] + '-icon' + new Date().getTime(), true, this.layoutDoc[Id] + '-icon').then(returnedfilename => setTimeout( action(() => { @@ -369,10 +373,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps iframeScaling = () => 1 / this.props.ScreenToLocalTransform().Scale; addStyleSheet(document: any, styleType: string = 'text/css') { - const style = document.createElement('style'); - style.type = styleType; - const sheets = document.head.appendChild(style); - return (sheets as any).sheet; + if (document) { + const style = document.createElement('style'); + style.type = styleType; + const sheets = document.head.appendChild(style); + return (sheets as any).sheet; + } } addStyleSheetRule(sheet: any, selector: any, css: any, selectorPrefix = '.') { const propText = @@ -381,7 +387,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps : Object.keys(css) .map(p => p + ':' + (p === 'content' ? "'" + css[p] + "'" : css[p])) .join(';'); - return sheet.insertRule(selectorPrefix + selector + '{' + propText + '}', sheet.cssRules.length); + return sheet?.insertRule(selectorPrefix + selector + '{' + propText + '}', sheet.cssRules.length); } _iframetimeout: any = undefined; @@ -394,7 +400,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps this.addStyleSheetRule(this.addStyleSheet(this._iframe?.contentDocument), '::selection', { color: 'white', background: 'orange' }, ''); - let requrlraw = decodeURIComponent(iframe?.contentWindow?.location.href.replace(Utils.prepend('') + '/corsProxy/', '') ?? this._url.toString()); + let href: Opt<string>; + try { + href = iframe?.contentWindow?.location.href; + } catch (e) { + href = undefined; + } + let requrlraw = decodeURIComponent(href?.replace(Utils.prepend('') + '/corsProxy/', '') ?? this._url.toString()); if (requrlraw !== this._url.toString()) { if (requrlraw.match(/q=.*&/)?.length && this._url.toString().match(/q=.*&/)?.length) { const matches = requrlraw.match(/[^a-zA-z]q=[^&]*/g); @@ -824,9 +836,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } } }); - sidebarWidth = () => - !this.SidebarShown ? 0 : WebBox.sidebarResizerWidth + (this._previewWidth ? WebBox.openSidebarWidth : ((NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc)) * this.props.PanelWidth()) / NumCast(this.layoutDoc.nativeWidth)); - + sidebarWidth = () => { + if (!this.SidebarShown) return 0; + if (this._previewWidth) return WebBox.sidebarResizerWidth + WebBox.openSidebarWidth; // return default sidebar if previewing (as in viewing a link target) + const nativeDiff = NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc); + return WebBox.sidebarResizerWidth + nativeDiff * (this.props.NativeDimScaling?.() || 1); + }; @computed get content() { const interactive = !this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick && this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && Doc.ActiveTool === InkTool.None && !DocumentDecorations.Instance?.Interacting; @@ -988,6 +1003,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps docView={this.props.docViewPath().lastElement()} finishMarquee={this.finishMarquee} savedAnnotations={this.savedAnnotationsCreator} + selectionText={returnEmptyString} annotationLayer={this._annotationLayer.current} mainCont={this._mainCont.current} />{' '} @@ -1003,7 +1019,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }} onPointerDown={e => this.sidebarBtnDown(e, false)} /> - <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this.layoutDoc[WidthSym]()}%` }}> + <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this.props.PanelWidth()}%` }}> <SidebarAnnos ref={this._sidebarRef} {...this.props} diff --git a/src/client/views/nodes/WebBoxRenderer.js b/src/client/views/nodes/WebBoxRenderer.js index f3f1bcf5c..cebb94d86 100644 --- a/src/client/views/nodes/WebBoxRenderer.js +++ b/src/client/views/nodes/WebBoxRenderer.js @@ -1,14 +1,13 @@ /** - * - * @param {StyleSheetList} styleSheets + * + * @param {StyleSheetList} styleSheets */ var ForeignHtmlRenderer = function (styleSheets) { - const self = this; /** - * - * @param {String} binStr + * + * @param {String} binStr */ const binaryStringToBase64 = function (binStr) { return new Promise(function (resolve) { @@ -16,7 +15,7 @@ var ForeignHtmlRenderer = function (styleSheets) { reader.readAsDataURL(binStr); reader.onloadend = function () { resolve(reader.result); - } + }; }); }; @@ -24,11 +23,11 @@ var ForeignHtmlRenderer = function (styleSheets) { return window.location.origin + extension; } function CorsProxy(url) { - return prepend("/corsProxy/") + encodeURIComponent(url); + return prepend('/corsProxy/') + encodeURIComponent(url); } /** - * - * @param {String} url + * + * @param {String} url * @returns {Promise} */ const getResourceAsBase64 = function (webUrl, inurl) { @@ -37,35 +36,30 @@ var ForeignHtmlRenderer = function (styleSheets) { //const url = inurl.startsWith("/") && !inurl.startsWith("//") ? webUrl + inurl : inurl; //const url = CorsProxy(inurl.startsWith("/") && !inurl.startsWith("//") ? webUrl + inurl : inurl);// inurl.startsWith("http") ? CorsProxy(inurl) : inurl; var url = inurl; - if (inurl.startsWith("/static")) { - url = (new URL(webUrl).origin + inurl); - } else - if ((inurl.startsWith("/") && !inurl.startsWith("//"))) { - url = CorsProxy(new URL(webUrl).origin + inurl); - } else if (!inurl.startsWith("http") && !inurl.startsWith("//")) { - url = CorsProxy(webUrl + "/" + inurl); - } - xhr.open("GET", url); + if (inurl.startsWith('/static')) { + url = new URL(webUrl).origin + inurl; + } else if (inurl.startsWith('/') && !inurl.startsWith('//')) { + url = CorsProxy(new URL(webUrl).origin + inurl); + } else if (!inurl.startsWith('http') && !inurl.startsWith('//')) { + url = CorsProxy(webUrl + '/' + inurl); + } + xhr.open('GET', url); xhr.responseType = 'blob'; xhr.onreadystatechange = async function () { if (xhr.readyState === 4 && xhr.status === 200) { const resBase64 = await binaryStringToBase64(xhr.response); - resolve( - { - "resourceUrl": inurl, - "resourceBase64": resBase64 - } - ); + resolve({ + resourceUrl: inurl, + resourceBase64: resBase64, + }); } else if (xhr.readyState === 4) { - console.log("COULDN'T FIND: " + (inurl.startsWith("/") ? webUrl + inurl : inurl)); - resolve( - { - "resourceUrl": "", - "resourceBase64": inurl - } - ); + console.log("COULDN'T FIND: " + (inurl.startsWith('/') ? webUrl + inurl : inurl)); + resolve({ + resourceUrl: '', + resourceBase64: inurl, + }); } }; @@ -74,8 +68,8 @@ var ForeignHtmlRenderer = function (styleSheets) { }; /** - * - * @param {String[]} urls + * + * @param {String[]} urls * @returns {Promise} */ const getMultipleResourcesAsBase64 = function (webUrl, urls) { @@ -87,13 +81,13 @@ var ForeignHtmlRenderer = function (styleSheets) { }; /** - * - * @param {String} str - * @param {Number} startIndex - * @param {String} prefixToken + * + * @param {String} str + * @param {Number} startIndex + * @param {String} prefixToken * @param {String[]} suffixTokens - * - * @returns {String|null} + * + * @returns {String|null} */ const parseValue = function (str, startIndex, prefixToken, suffixTokens) { const idx = str.indexOf(prefixToken, startIndex); @@ -111,17 +105,17 @@ var ForeignHtmlRenderer = function (styleSheets) { } return { - "foundAtIndex": idx, - "value": val - } + foundAtIndex: idx, + value: val, + }; }; /** - * - * @param {String} cssRuleStr + * + * @param {String} cssRuleStr * @returns {String[]} */ - const getUrlsFromCssString = function (cssRuleStr, selector = "url(", delimiters = [')'], mustEndWithQuote = false) { + const getUrlsFromCssString = function (cssRuleStr, selector = 'url(', delimiters = [')'], mustEndWithQuote = false) { const urlsFound = []; let searchStartIndex = 0; @@ -133,7 +127,7 @@ var ForeignHtmlRenderer = function (styleSheets) { searchStartIndex = url.foundAtIndex + url.value.length; if (mustEndWithQuote && url.value[url.value.length - 1] !== '"') continue; const unquoted = removeQuotes(url.value); - if (!unquoted /* || (!unquoted.startsWith('http')&& !unquoted.startsWith("/") )*/ || unquoted === 'http://' || unquoted === 'https://') { + if (!unquoted /* || (!unquoted.startsWith('http')&& !unquoted.startsWith("/") )*/ || unquoted === 'http://' || unquoted === 'https://') { continue; } @@ -144,24 +138,24 @@ var ForeignHtmlRenderer = function (styleSheets) { }; /** - * - * @param {String} html + * + * @param {String} html * @returns {String[]} */ const getImageUrlsFromFromHtml = function (html) { - return getUrlsFromCssString(html, "src=", [' ', '>', '\t'], true); + return getUrlsFromCssString(html, 'src=', [' ', '>', '\t'], true); }; const getSourceUrlsFromFromHtml = function (html) { - return getUrlsFromCssString(html, "source=", [' ', '>', '\t'], true); + return getUrlsFromCssString(html, 'source=', [' ', '>', '\t'], true); }; /** - * + * * @param {String} str * @returns {String} */ const removeQuotes = function (str) { - return str.replace(/["']/g, ""); + return str.replace(/["']/g, ''); }; const escapeRegExp = function (string) { @@ -169,37 +163,33 @@ var ForeignHtmlRenderer = function (styleSheets) { }; /** - * - * @param {String} contentHtml + * + * @param {String} contentHtml * @param {Number} width * @param {Number} height - * + * * @returns {Promise<String>} */ const buildSvgDataUri = async function (webUrl, contentHtml, width, height, scroll, xoff) { - return new Promise(async function (resolve, reject) { - /* !! The problems !! - * 1. CORS (not really an issue, expect perhaps for images, as this is a general security consideration to begin with) - * 2. Platform won't wait for external assets to load (fonts, images, etc.) - */ + * 1. CORS (not really an issue, expect perhaps for images, as this is a general security consideration to begin with) + * 2. Platform won't wait for external assets to load (fonts, images, etc.) + */ // copy styles - let cssStyles = ""; + let cssStyles = ''; let urlsFoundInCss = []; for (let i = 0; i < styleSheets.length; i++) { try { - const rules = styleSheets[i].cssRules + const rules = styleSheets[i].cssRules; for (let j = 0; j < rules.length; j++) { const cssRuleStr = rules[j].cssText; urlsFoundInCss.push(...getUrlsFromCssString(cssRuleStr)); cssStyles += cssRuleStr; } - } catch (e) { - - } + } catch (e) {} } // const fetchedResourcesFromStylesheets = await getMultipleResourcesAsBase64(webUrl, urlsFoundInCss); @@ -210,30 +200,32 @@ var ForeignHtmlRenderer = function (styleSheets) { // } // } - contentHtml = contentHtml.replace(/<source[^>]*>/g, "") // <picture> tags have a <source> which has a srcset field of image refs. instead of converting each, just use the default <img> of the picture - .replace(/noscript/g, "div").replace(/<div class="mediaset"><\/div>/g, "") // when scripting isn't available (ie, rendering web pages here), <noscript> tags should become <div>'s. But for Brown CS, there's a layout problem if you leave the empty <mediaset> tag - .replace(/<link[^>]*>/g, "") // don't need to keep any linked style sheets because we've already processed all style sheets above - .replace(/srcset="([^ "]*)[^"]*"/g, "src=\"$1\""); // instead of converting each item in the srcset to a data url, just convert the first one and use that - let urlsFoundInHtml = getImageUrlsFromFromHtml(contentHtml).filter(url => !url.startsWith("data:")); + contentHtml = contentHtml + .replace(/<source[^>]*>/g, '') // <picture> tags have a <source> which has a srcset field of image refs. instead of converting each, just use the default <img> of the picture + .replace(/noscript/g, 'div') + .replace(/<div class="mediaset"><\/div>/g, '') // when scripting isn't available (ie, rendering web pages here), <noscript> tags should become <div>'s. But for Brown CS, there's a layout problem if you leave the empty <mediaset> tag + .replace(/<link[^>]*>/g, '') // don't need to keep any linked style sheets because we've already processed all style sheets above + .replace(/srcset="([^ "]*)[^"]*"/g, 'src="$1"'); // instead of converting each item in the srcset to a data url, just convert the first one and use that + let urlsFoundInHtml = getImageUrlsFromFromHtml(contentHtml).filter(url => !url.startsWith('data:')); const fetchedResources = webUrl ? await getMultipleResourcesAsBase64(webUrl, urlsFoundInHtml) : []; for (let i = 0; i < fetchedResources.length; i++) { const r = fetchedResources[i]; if (r.resourceUrl) { - contentHtml = contentHtml.replace(new RegExp(escapeRegExp(r.resourceUrl), "g"), r.resourceBase64); + contentHtml = contentHtml.replace(new RegExp(escapeRegExp(r.resourceUrl), 'g'), r.resourceBase64); } } - const styleElem = document.createElement("style"); - styleElem.innerHTML = cssStyles.replace(">", ">").replace("<", "<"); + const styleElem = document.createElement('style'); + styleElem.innerHTML = cssStyles.replace('>', '>').replace('<', '<'); - const styleElemString = new XMLSerializer().serializeToString(styleElem).replace(/>/g, ">").replace(/</g, "<"); + const styleElemString = new XMLSerializer().serializeToString(styleElem).replace(/>/g, '>').replace(/</g, '<'); // create DOM element string that encapsulates styles + content - const contentRootElem = document.createElement("body"); - contentRootElem.style.zIndex = "1111"; + const contentRootElem = document.createElement('body'); + contentRootElem.style.zIndex = '1111'; // contentRootElem.style.transform = "scale(0.08)" contentRootElem.innerHTML = styleElemString + contentHtml; - contentRootElem.setAttribute("xmlns", "http://www.w3.org/1999/xhtml"); + contentRootElem.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml'); //document.body.appendChild(contentRootElem); const contentRootElemString = new XMLSerializer().serializeToString(contentRootElem); @@ -256,17 +248,17 @@ var ForeignHtmlRenderer = function (styleSheets) { * @param {String} html * @param {Number} width * @param {Number} height - * + * * @return {Promise<Image>} */ this.renderToImage = async function (webUrl, html, width, height, scroll, xoff) { return new Promise(async function (resolve, reject) { const img = new Image(); - console.log("BUILDING SVG for:" + webUrl); + console.log('BUILDING SVG for:' + webUrl); img.src = await buildSvgDataUri(webUrl, html, width, height, scroll, xoff); img.onload = function () { - console.log("IMAGE SVG created:" + webUrl); + console.log('IMAGE SVG created:' + webUrl); resolve(img); }; }); @@ -276,7 +268,7 @@ var ForeignHtmlRenderer = function (styleSheets) { * @param {String} html * @param {Number} width * @param {Number} height - * + * * @return {Promise<Image>} */ this.renderToCanvas = async function (webUrl, html, width, height, scroll, xoff, oversample) { @@ -298,7 +290,7 @@ var ForeignHtmlRenderer = function (styleSheets) { * @param {String} html * @param {Number} width * @param {Number} height - * + * * @return {Promise<String>} */ this.renderToBase64Png = async function (webUrl, html, width, height, scroll, xoff, oversample) { @@ -307,24 +299,30 @@ var ForeignHtmlRenderer = function (styleSheets) { resolve(canvas.toDataURL('image/png')); }); }; - }; - export function CreateImage(webUrl, styleSheets, html, width, height, scroll, xoff = 0, oversample = 1) { - const val = (new ForeignHtmlRenderer(styleSheets)).renderToBase64Png(webUrl, html.replace(/docView-hack/g, 'documentView-hack').replace(/\n/g, "").replace(/<script((?!\/script).)*<\/script>/g, ""), width, height, scroll, xoff, oversample); - return val; + return new ForeignHtmlRenderer(styleSheets).renderToBase64Png( + webUrl, + html + .replace(/docView-hack/g, 'documentView-hack') + .replace(/\n/g, '') + .replace(/<script((?!\/script).)*<\/script>/g, ''), + width, + height, + scroll, + xoff, + oversample + ); } - - -var ClipboardUtils = new function () { +var ClipboardUtils = new (function () { var permissions = { 'image/bmp': true, 'image/gif': true, 'image/png': true, 'image/jpeg': true, - 'image/tiff': true + 'image/tiff': true, }; function getType(types) { @@ -387,9 +385,8 @@ var ClipboardUtils = new function () { callback(null, 'Clipboard is not supported.'); } }; -}; - +})(); export function pasteImageBitmap(callback) { return ClipboardUtils.readImage(callback); -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx index 6d1751b25..42d20ba99 100644 --- a/src/client/views/nodes/button/FontIconBox.tsx +++ b/src/client/views/nodes/button/FontIconBox.tsx @@ -1,7 +1,6 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; -import Color from 'color'; import { action, computed, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -11,7 +10,8 @@ import { InkTool } from '../../../../fields/InkField'; import { ScriptField } from '../../../../fields/ScriptField'; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; import { WebField } from '../../../../fields/URLField'; -import { aggregateBounds, Utils } from '../../../../Utils'; +import { GestureUtils } from '../../../../pen-gestures/GestureUtils'; +import { aggregateBounds, StopEvent, Utils } from '../../../../Utils'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; import { ScriptingGlobals } from '../../../util/ScriptingGlobals'; import { SelectionManager } from '../../../util/SelectionManager'; @@ -85,6 +85,12 @@ export class FontIconBox extends DocComponent<ButtonProps>() { static SetShowLabels(show: boolean) { Doc.UserDoc()._showLabel = show; } + static GetRecognizeGestures() { + return BoolCast(Doc.UserDoc()._recognizeGestures); + } + static SetRecognizeGesturs(show: boolean) { + Doc.UserDoc()._recognizeGestures = show; + } // Determining UI Specs @computed get label() { @@ -191,7 +197,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { e.preventDefault(); }} onClick={action(() => (this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen))}> - <input style={{ width: 30 }} className="button-input" type="number" value={checkResult} onChange={action(e => setValue(Number(e.target.value)))} /> + <input style={{ width: 30 }} className="button-input" type="number" value={checkResult} onChange={undoBatch(action(e => setValue(Number(e.target.value))))} /> </div> <div className={`button`} onClick={action(e => setValue(Number(checkResult) + 1))}> <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={'plus'} /> @@ -214,7 +220,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { </div> ); } else { - return <div></div>; + return <div />; } } @@ -296,7 +302,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { fontFamily: script.script.originalScript.startsWith('setFont') ? value : undefined, backgroundColor: value === text ? Colors.LIGHT_BLUE : undefined, }} - onClick={() => script.script.run({ value }).result}> + onClick={undoBatch(() => script.script.run({ value }))}> {value[0].toUpperCase() + value.slice(1)} </div> )); @@ -345,12 +351,14 @@ export class FontIconBox extends DocComponent<ButtonProps>() { } colorPicker = (curColor: string) => { - const change = (value: ColorState) => { + const change = (value: ColorState, ev: MouseEvent) => { + ev.preventDefault(); + ev.stopPropagation(); const s = this.colorScript; s && undoBatch(() => s.script.run({ value: Utils.colorString(value), _readOnly_: false }).result)(); }; const presets = ['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']; - return <SketchPicker onChange={change} color={curColor} presetColors={presets} />; + return <SketchPicker onChange={change as any /* SketchPicker passes the mouse event to the callback, but the type system doesn't know that */} color={curColor} presetColors={presets} />; }; /** * Color button @@ -373,12 +381,15 @@ export class FontIconBox extends DocComponent<ButtonProps>() { // style={{ borderBottomRightRadius: this.dropdown ? 0 : undefined }}> // <FontAwesomeIcon icon={'caret-down'} color={color} size="sm" /> // </div>; - setTimeout(() => this.colorPicker(curColor)); // cause an update to the color picker rendered in MainView + //setTimeout(() => this.colorPicker(curColor)); // cause an update to the color picker rendered in MainView return ( <div className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? 'Label' : '')} ${this.colorPickerClosed}`} style={{ color: color, borderBottomLeftRadius: this.dropdown ? 0 : undefined }} - onClick={action(() => (this.colorPickerClosed = !this.colorPickerClosed))} + onClick={action(e => { + this.colorPickerClosed = !this.colorPickerClosed; + e.stopPropagation(); + })} onPointerDown={e => e.stopPropagation()}> {this.Icon(color)} <div className="colorButton-color" style={{ backgroundColor: curColor }} /> @@ -386,7 +397,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { {/* {dropdownCaret} */} {this.colorPickerClosed ? null : ( <div> - <div className="menuButton-dropdownBox" onPointerDown={e => e.stopPropagation()} onClick={e => e.stopPropagation()}> + <div className="menuButton-dropdownBox" onPointerDown={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onClick={e => e.stopPropagation()}> {this.colorPicker(curColor)} </div> <div @@ -764,41 +775,42 @@ export function createInkGroup(inksToGroup?: Doc[], isSubGroup?: boolean) { CollectionFreeFormView.collectionsWithUnprocessedInk.clear(); } -/** INK - * setActiveTool - * setStrokeWidth - * setStrokeColor - **/ - -ScriptingGlobals.add(function setActiveTool(tool: string, checkResult?: boolean) { +function setActiveTool(tool: InkTool | GestureUtils.Gestures, keepPrim: boolean, checkResult?: boolean) { InkTranscription.Instance?.createInkGroup(); if (checkResult) { - return (Doc.ActiveTool === tool && !GestureOverlay.Instance?.InkShape) || GestureOverlay.Instance?.InkShape === tool ? Colors.MEDIUM_BLUE : 'transparent'; - } - if (['circle', 'square', 'line'].includes(tool)) { - if (GestureOverlay.Instance.InkShape === tool) { - Doc.ActiveTool = InkTool.None; - GestureOverlay.Instance.InkShape = InkTool.None; - } else { - Doc.ActiveTool = InkTool.Pen; - GestureOverlay.Instance.InkShape = tool; + return (Doc.ActiveTool === tool && !GestureOverlay.Instance?.InkShape) || GestureOverlay.Instance?.InkShape === tool + ? GestureOverlay.Instance?.KeepPrimitiveMode || ![GestureUtils.Gestures.Circle, GestureUtils.Gestures.Line, GestureUtils.Gestures.Rectangle].includes(tool as GestureUtils.Gestures) + ? Colors.MEDIUM_BLUE + : Colors.MEDIUM_BLUE_ALT + : 'transparent'; + } + runInAction(() => { + if (GestureOverlay.Instance) { + GestureOverlay.Instance.KeepPrimitiveMode = keepPrim; } - } else if (tool) { - // pen or eraser - if (Doc.ActiveTool === tool && !GestureOverlay.Instance.InkShape) { - Doc.ActiveTool = InkTool.None; - } else if (tool == InkTool.Write) { - // console.log("write mode selected - create groupDoc here!", tool) - Doc.ActiveTool = tool; - GestureOverlay.Instance.InkShape = ''; + if (Object.values(GestureUtils.Gestures).includes(tool as any)) { + if (GestureOverlay.Instance.InkShape === tool) { + Doc.ActiveTool = InkTool.None; + GestureOverlay.Instance.InkShape = undefined; + } else { + Doc.ActiveTool = InkTool.Pen; + GestureOverlay.Instance.InkShape = tool as GestureUtils.Gestures; + } + } else if (tool) { + // pen or eraser + if (Doc.ActiveTool === tool && !GestureOverlay.Instance.InkShape) { + Doc.ActiveTool = InkTool.None; + } else { + Doc.ActiveTool = tool as any; + GestureOverlay.Instance.InkShape = undefined; + } } else { - Doc.ActiveTool = tool as any; - GestureOverlay.Instance.InkShape = ''; + Doc.ActiveTool = InkTool.None; } - } else { - Doc.ActiveTool = InkTool.None; - } -}); + }); +} + +ScriptingGlobals.add(setActiveTool, 'sets the active ink tool mode'); // toggle: Set overlay status of selected document ScriptingGlobals.add(function setIsInkMask(checkResult?: boolean) { diff --git a/src/client/views/nodes/formattedText/DashDocCommentView.tsx b/src/client/views/nodes/formattedText/DashDocCommentView.tsx index 40dd6fbc7..4bff57842 100644 --- a/src/client/views/nodes/formattedText/DashDocCommentView.tsx +++ b/src/client/views/nodes/formattedText/DashDocCommentView.tsx @@ -1,5 +1,5 @@ import { TextSelection } from 'prosemirror-state'; -import * as ReactDOM from 'react-dom'; +import * as ReactDOM from 'react-dom/client'; import { Doc } from '../../../../fields/Doc'; import { DocServer } from '../../../DocServer'; import React = require('react'); @@ -9,6 +9,7 @@ import React = require('react'); // the comment can be toggled on/off with the '<-' text anchor. export class DashDocCommentView { dom: HTMLDivElement; // container for label and value + root: any; constructor(node: any, view: any, getPos: any) { this.dom = document.createElement('div'); @@ -30,15 +31,17 @@ export class DashDocCommentView { e.stopPropagation(); }; - ReactDOM.render(<DashDocCommentViewInternal view={view} getPos={getPos} docid={node.attrs.docid} />, this.dom); + this.root = ReactDOM.createRoot(this.dom); + this.root.render(<DashDocCommentViewInternal view={view} getPos={getPos} docid={node.attrs.docid} />); (this as any).dom = this.dom; } - destroy() { - ReactDOM.unmountComponentAtNode(this.dom); + deselectNode() { + this.dom.classList.remove('ProseMirror-selectednode'); + } + selectNode() { + this.dom.classList.add('ProseMirror-selectednode'); } - - selectNode() {} } interface IDashDocCommentViewInternal { diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx index 73a711b9d..63ee7d1f3 100644 --- a/src/client/views/nodes/formattedText/DashDocView.tsx +++ b/src/client/views/nodes/formattedText/DashDocView.tsx @@ -1,7 +1,7 @@ import { action, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import { NodeSelection } from 'prosemirror-state'; -import * as ReactDOM from 'react-dom'; +import * as ReactDOM from 'react-dom/client'; import { Doc, HeightSym, WidthSym } from '../../../../fields/Doc'; import { Cast, NumCast, StrCast } from '../../../../fields/Types'; import { emptyFunction, returnFalse, Utils } from '../../../../Utils'; @@ -15,6 +15,7 @@ import React = require('react'); export class DashDocView { dom: HTMLSpanElement; // container for label and value + root: any; constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) { this.dom = document.createElement('span'); @@ -38,14 +39,13 @@ export class DashDocView { e.stopPropagation(); }; - ReactDOM.render( - <DashDocViewInternal docid={node.attrs.docid} alias={node.attrs.alias} width={node.attrs.width} height={node.attrs.height} hidden={node.attrs.hidden} fieldKey={node.attrs.fieldKey} tbox={tbox} view={view} node={node} getPos={getPos} />, - this.dom + this.root = ReactDOM.createRoot(this.dom); + this.root.render( + <DashDocViewInternal docid={node.attrs.docid} alias={node.attrs.alias} width={node.attrs.width} height={node.attrs.height} hidden={node.attrs.hidden} fieldKey={node.attrs.fieldKey} tbox={tbox} view={view} node={node} getPos={getPos} /> ); - (this as any).dom = this.dom; } destroy() { - ReactDOM.unmountComponentAtNode(this.dom); + // ReactDOM.unmountComponentAtNode(this.dom); } selectNode() {} } @@ -149,7 +149,7 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> { const { scale, translateX, translateY } = Utils.GetScreenTransform(this._spanRef.current); return new Transform(-translateX, -translateY, 1).scale(1 / scale); }; - outerFocus = (target: Doc) => this._textBox.props.focus(this._textBox.props.Document); // ideally, this would scroll to show the focus target + outerFocus = (target: Doc) => this._textBox.props.focus(this._textBox.props.Document, {}); // ideally, this would scroll to show the focus target onKeyDown = (e: any) => { e.stopPropagation(); diff --git a/src/client/views/nodes/formattedText/DashFieldView.scss b/src/client/views/nodes/formattedText/DashFieldView.scss index c36e6804b..f17579853 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.scss +++ b/src/client/views/nodes/formattedText/DashFieldView.scss @@ -1,3 +1,5 @@ +@import '../../global/globalCssVariables'; + .dashFieldView { position: relative; display: inline-flex; @@ -22,7 +24,7 @@ position: relative; display: inline-block; font-weight: normal; - background: rgba(0,0,0,0.1); + background: rgba(0, 0, 0, 0.1); } .dashFieldView-fieldSpan { min-width: 8px; @@ -31,11 +33,12 @@ padding-left: 2px; display: inline-block; background-color: rgba(155, 155, 155, 0.24); - font-weight: bold; span { min-width: 100%; display: inline-block; } } } -
\ No newline at end of file +.ProseMirror-selectedNode { + outline: solid 1px $light-blue !important; +} diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx index 35d919f38..afae59733 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.tsx +++ b/src/client/views/nodes/formattedText/DashFieldView.tsx @@ -1,6 +1,6 @@ import { action, computed, IReactionDisposer, observable } from 'mobx'; import { observer } from 'mobx-react'; -import * as ReactDOM from 'react-dom'; +import * as ReactDOM from 'react-dom/client'; import { DataSym, Doc, DocListCast, Field } from '../../../../fields/Doc'; import { List } from '../../../../fields/List'; import { listSpec } from '../../../../fields/Schema'; @@ -16,20 +16,18 @@ import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu'; import { Tooltip } from '@material-ui/core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { CollectionViewType } from '../../../documents/DocumentTypes'; +import { NodeSelection } from 'prosemirror-state'; export class DashFieldView { dom: HTMLDivElement; // container for label and value + root: any; constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) { - const { boolVal, strVal } = DashFieldViewInternal.fieldContent(tbox.props.Document, tbox.rootDoc, node.attrs.fieldKey); - this.dom = document.createElement('div'); this.dom.style.width = node.attrs.width; this.dom.style.height = node.attrs.height; - this.dom.style.fontWeight = 'bold'; this.dom.style.position = 'relative'; this.dom.style.display = 'inline-block'; - this.dom.textContent = node.attrs.fieldKey.startsWith('#') ? node.attrs.fieldKey : node.attrs.fieldKey + ' ' + strVal; this.dom.onkeypress = function (e: any) { e.stopPropagation(); }; @@ -43,13 +41,20 @@ export class DashFieldView { e.stopPropagation(); }; - setTimeout(() => ReactDOM.render(<DashFieldViewInternal fieldKey={node.attrs.fieldKey} docid={node.attrs.docid} width={node.attrs.width} height={node.attrs.height} hideKey={node.attrs.hideKey} tbox={tbox} />, this.dom)); - (this as any).dom = this.dom; + this.root = ReactDOM.createRoot(this.dom); + this.root.render( + <DashFieldViewInternal node={node} getPos={getPos} fieldKey={node.attrs.fieldKey} docid={node.attrs.docid} width={node.attrs.width} height={node.attrs.height} hideKey={node.attrs.hideKey} editable={node.attrs.editable} tbox={tbox} /> + ); } destroy() { - ReactDOM.unmountComponentAtNode(this.dom); + //this.root.unmount(); + } + deselectNode() { + this.dom.classList.remove('ProseMirror-selectednode'); + } + selectNode() { + this.dom.classList.add('ProseMirror-selectednode'); } - selectNode() {} } interface IDashFieldViewInternal { @@ -59,6 +64,9 @@ interface IDashFieldViewInternal { tbox: FormattedTextBox; width: number; height: number; + editable: boolean; + node: any; + getPos: any; } @observer @@ -116,7 +124,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna return ( <span className="dashFieldView-fieldSpan" - contentEditable={true} + contentEditable={this.props.editable} style={{ display: strVal.length < 2 ? 'inline-block' : undefined }} suppressContentEditableWarning={true} defaultValue={strVal} @@ -125,7 +133,13 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna r?.addEventListener('blur', e => r && this.updateText(r.textContent!, false)); r?.addEventListener( 'pointerdown', - action(e => e.stopPropagation()) + action(e => { + // let target = e.target as any; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span> + // while (target && !target.dataset?.targethrefs) target = target.parentElement; + this.props.tbox.EditorView!.dispatch(this.props.tbox.EditorView!.state.tr.setSelection(new NodeSelection(this.props.tbox.EditorView!.state.doc.resolve(this.props.getPos())))); + // FormattedTextBoxComment.update(this.props.tbox, this.props.tbox.EditorView!, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc); + // e.stopPropagation(); + }) ); }}> {strVal} @@ -160,7 +174,6 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna updateText = (nodeText: string, forceMatch: boolean) => { if (nodeText) { const newText = nodeText.startsWith(':=') || nodeText.startsWith('=:=') ? ':=-computed-' : nodeText; - // look for a document whose id === the fieldKey being displayed. If there's a match, then that document // holds the different enumerated values for the field in the titles of its collected documents. // if there's a partial match from the start of the input text, complete the text --- TODO: make this an auto suggest box and select from a drop down. @@ -266,14 +279,12 @@ export class DashFieldViewMenu extends AntimodeMenu<AntimodeMenuProps> { document.addEventListener('pointerdown', hideMenu, true); }; render() { - const buttons = [ + return this.getElement([ <Tooltip key="trash" title={<div className="dash-tooltip">{`Show Pivot Viewer for '${this._fieldKey}'`}</div>}> <button className="antimodeMenu-button" onPointerDown={this.showFields}> <FontAwesomeIcon icon="eye" size="lg" /> </button> </Tooltip>, - ]; - - return this.getElement(buttons); + ]); } } diff --git a/src/client/views/nodes/formattedText/EquationView.tsx b/src/client/views/nodes/formattedText/EquationView.tsx index 98d611ca6..0fd2a7808 100644 --- a/src/client/views/nodes/formattedText/EquationView.tsx +++ b/src/client/views/nodes/formattedText/EquationView.tsx @@ -1,7 +1,8 @@ import EquationEditor from 'equation-editor-react'; import { IReactionDisposer } from 'mobx'; import { observer } from 'mobx-react'; -import * as ReactDOM from 'react-dom'; +import { TextSelection } from 'prosemirror-state'; +import * as ReactDOM from 'react-dom/client'; import { Doc } from '../../../../fields/Doc'; import { StrCast } from '../../../../fields/Types'; import './DashFieldView.scss'; @@ -10,7 +11,7 @@ import React = require('react'); export class EquationView { dom: HTMLDivElement; // container for label and value - + root: any; constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) { this.dom = document.createElement('div'); this.dom.style.width = node.attrs.width; @@ -21,13 +22,16 @@ export class EquationView { e.stopPropagation(); }; - ReactDOM.render(<EquationViewInternal fieldKey={node.attrs.fieldKey} width={node.attrs.width} height={node.attrs.height} setEditor={this.setEditor} tbox={tbox} />, this.dom); - (this as any).dom = this.dom; + this.root = ReactDOM.createRoot(this.dom); + this.root.render(<EquationViewInternal fieldKey={node.attrs.fieldKey} width={node.attrs.width} height={node.attrs.height} getPos={getPos} setEditor={this.setEditor} tbox={tbox} />); } _editor: EquationEditor | undefined; setEditor = (editor?: EquationEditor) => (this._editor = editor); destroy() { - ReactDOM.unmountComponentAtNode(this.dom); + // ReactDOM.unmountComponentAtNode(this.dom); + } + setSelection() { + this._editor?.mathField.focus(); } selectNode() { this._editor?.mathField.focus(); @@ -40,6 +44,7 @@ interface IEquationViewInternal { tbox: FormattedTextBox; width: number; height: number; + getPos: () => number; setEditor: (editor: EquationEditor | undefined) => void; } @@ -67,11 +72,22 @@ export class EquationViewInternal extends React.Component<IEquationViewInternal> return ( <div className="equationView" + onKeyDown={e => { + if (e.key === 'Enter') { + this.props.tbox.EditorView!.dispatch(this.props.tbox.EditorView!.state.tr.setSelection(new TextSelection(this.props.tbox.EditorView!.state.doc.resolve(this.props.getPos() + 1)))); + this.props.tbox.EditorView!.focus(); + e.preventDefault(); + } + e.stopPropagation(); + }} + onKeyPress={e => e.stopPropagation()} style={{ position: 'relative', display: 'inline-block', width: this.props.width, height: this.props.height, + background: 'white', + borderRadius: '10%', bottom: 3, }}> <EquationEditor diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 6db199149..63435eea8 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -18,7 +18,7 @@ import { PrefetchProxy } from '../../../../fields/Proxy'; import { RichTextField } from '../../../../fields/RichTextField'; import { RichTextUtils } from '../../../../fields/RichTextUtils'; import { ComputedField } from '../../../../fields/ScriptField'; -import { BoolCast, Cast, FieldValue, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; +import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; import { GetEffectiveAcl, TraceMobx } from '../../../../fields/util'; import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, OmitKeys, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils'; import { GoogleApiClientUtils, Pulls, Pushes } from '../../../apis/google_docs/GoogleApiClientUtils'; @@ -124,7 +124,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps return this._showSidebar ? '20%' : StrCast(this.layoutDoc._sidebarWidthPercent, '0%'); } @computed get sidebarColor() { - return StrCast(this.layoutDoc.sidebarColor, StrCast(this.layoutDoc[this.props.fieldKey + '-backgroundColor'], '#e4e4e4')); + return StrCast(this.layoutDoc.sidebarColor, StrCast(this.layoutDoc[this.fieldKey + '-backgroundColor'], '#e4e4e4')); } @computed get autoHeight() { return (this.props.forceAutoHeight || this.layoutDoc._autoHeight) && !this.props.ignoreAutoHeight; @@ -281,12 +281,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const state = this._editorView.state.apply(tx); this._editorView.updateState(state); + const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc.proto), this.fieldKey) ? DocCast(this.layoutDoc.proto) : this.dataDoc; const curText = state.doc.textBetween(0, state.doc.content.size, ' \n'); - const curTemp = this.layoutDoc.resolvedDataDoc ? Cast(this.layoutDoc[this.props.fieldKey], RichTextField) : undefined; // the actual text in the text box - const curProto = Cast(Cast(this.dataDoc.proto, Doc, null)?.[this.fieldKey], RichTextField, null); // the default text inherited from a prototype + const curTemp = this.layoutDoc.resolvedDataDoc ? Cast(this.layoutDoc[this.fieldKey], RichTextField) : undefined; // the actual text in the text box + const curProto = Cast(Cast(dataDoc.proto, Doc, null)?.[this.fieldKey], RichTextField, null); // the default text inherited from a prototype const curLayout = this.rootDoc !== this.layoutDoc ? Cast(this.layoutDoc[this.fieldKey], RichTextField, null) : undefined; // the default text stored in a layout template const json = JSON.stringify(state.toJSON()); - const effectiveAcl = GetEffectiveAcl(this.dataDoc); + const effectiveAcl = GetEffectiveAcl(dataDoc); const removeSelection = (json: string | undefined) => (json?.indexOf('"storedMarks"') === -1 ? json?.replace(/"selection":.*/, '') : json?.replace(/"selection":"\"storedMarks\""/, '"storedMarks"')); @@ -297,29 +298,34 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps accumTags.push(node.attrs.fieldKey); } }); - const curTags = Object.keys(this.dataDoc).filter(key => key.startsWith('#')); + const curTags = Object.keys(dataDoc).filter(key => key.startsWith('#')); const added = accumTags.filter(tag => !curTags.includes(tag)); const removed = curTags.filter(tag => !accumTags.includes(tag)); - removed.forEach(r => (this.dataDoc[r] = undefined)); - added.forEach(a => (this.dataDoc[a] = a)); + removed.forEach(r => (dataDoc[r] = undefined)); + added.forEach(a => (dataDoc[a] = a)); let unchanged = true; if (this._applyingChange !== this.fieldKey && removeSelection(json) !== removeSelection(curProto?.Data)) { this._applyingChange = this.fieldKey; - curText !== Cast(this.dataDoc[this.fieldKey], RichTextField)?.Text && (this.dataDoc[this.props.fieldKey + '-lastModified'] = new DateField(new Date(Date.now()))); + curText !== Cast(dataDoc[this.fieldKey], RichTextField)?.Text && (dataDoc[this.fieldKey + '-lastModified'] = new DateField(new Date(Date.now()))); if ((!curTemp && !curProto) || curText || json.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 (removeSelection(json) !== removeSelection(curLayout?.Data)) { - this.dataDoc[this.props.fieldKey] = new RichTextField(json, curText); - this.dataDoc[this.props.fieldKey + '-noTemplate'] = true; //(curTemp?.Text || "") !== curText; // mark the data field as being split from the template if it has been edited + const numstring = NumCast(dataDoc[this.fieldKey], null); + if (numstring !== undefined) { + dataDoc[this.fieldKey] = Number(curText); + } else { + dataDoc[this.fieldKey] = new RichTextField(json, curText); + } + dataDoc[this.fieldKey + '-noTemplate'] = true; //(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 }); unchanged = false; } } 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; + dataDoc[this.fieldKey] = undefined; this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse((curProto || curTemp).Data))); - this.dataDoc[this.props.fieldKey + '-noTemplate'] = undefined; // mark the data field as not being split from any template it might have + dataDoc[this.fieldKey + '-noTemplate'] = undefined; // mark the data field as not being split from any template it might have ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: curText }); unchanged = false; } @@ -330,7 +336,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } } } else { - const jsonstring = Cast(this.dataDoc[this.fieldKey], RichTextField)?.Data!; + const jsonstring = Cast(dataDoc[this.fieldKey], RichTextField)?.Data!; if (jsonstring) { const json = JSON.parse(jsonstring); json.selection = state.toJSON().selection; @@ -387,13 +393,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const newAutoLinks = new Set<Doc>(); const oldAutoLinks = DocListCast(this.props.Document.links).filter(link => link.linkRelationship === LinkManager.AutoKeywords); if (this._editorView?.state.doc.textContent) { + const isNodeSel = this._editorView.state.selection instanceof NodeSelection; const f = this._editorView.state.selection.from; const t = this._editorView.state.selection.to; var tr = this._editorView.state.tr as any; const autoAnch = this._editorView.state.schema.marks.autoLinkAnchor; tr = tr.removeMark(0, tr.doc.content.size, autoAnch); DocListCast(Doc.MyPublishedDocs.data).forEach(term => (tr = this.hyperlinkTerm(tr, term, newAutoLinks))); - tr = tr.setSelection(new TextSelection(tr.doc.resolve(f), tr.doc.resolve(t))); + tr = tr.setSelection(isNodeSel && false ? new NodeSelection(tr.doc.resolve(f)) : new TextSelection(tr.doc.resolve(f), tr.doc.resolve(t))); this._editorView?.dispatch(tr); } oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.anchor2 !== this.rootDoc).forEach(LinkManager.Instance.deleteLink); @@ -508,7 +515,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps this._dropDisposer?.(); this.ProseRef = ele; if (ele) { - this.setupEditor(this.config, this.props.fieldKey); + this.setupEditor(this.config, this.fieldKey); this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.layoutDoc); } // if (this.autoHeight) this.tryUpdateScrollHeight(); @@ -524,7 +531,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps // replace text contents whend dragging with Alt if (draggedDoc && draggedDoc.type === DocumentType.RTF && !Doc.AreProtosEqual(draggedDoc, this.props.Document) && de.altKey) { if (draggedDoc.data instanceof RichTextField) { - Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new RichTextField(draggedDoc.data.Data, draggedDoc.data.Text); + Doc.GetProto(this.dataDoc)[this.fieldKey] = new RichTextField(draggedDoc.data.Data, draggedDoc.data.Text); e.stopPropagation(); } // embed document when dragg marked as embed @@ -538,6 +545,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps docid: target[Id], float: 'unset', }); + if (!['alias', 'copy'].includes((dragData.dropAction ?? '') as any)) { + dragData.removeDocument?.(dragData.draggedDocuments[0]); + } const view = this._editorView!; view.dispatch(view.state.tr.insert(view.posAtCoords({ left: de.x, top: de.y })!.pos, node)); e.stopPropagation(); @@ -675,8 +685,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps @undoBatch deleteAnnotation = (anchor: Doc) => { LinkManager.Instance.deleteLink(DocListCast(anchor.links)[0]); - // const docAnnotations = DocListCast(this.props.dataDoc[this.props.fieldKey]); - // this.props.dataDoc[this.props.fieldKey] = new List<Doc>(docAnnotations.filter(a => a !== this.annoTextRegion)); + // const docAnnotations = DocListCast(this.props.dataDoc[this.fieldKey]); + // this.props.dataDoc[this.fieldKey] = new List<Doc>(docAnnotations.filter(a => a !== this.annoTextRegion)); // AnchorMenu.Instance.fadeOut(true); this.props.select(false); }; @@ -787,6 +797,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const appearanceItems = appearance && 'subitems' in appearance ? appearance.subitems : []; appearanceItems.push({ description: 'Change Perspective...', noexpand: true, subitems: changeItems, icon: 'external-link-alt' }); // this.rootDoc.isTemplateDoc && appearanceItems.push({ description: "Make Default Layout", event: async () => Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.rootDoc), icon: "eye" }); + !Doc.noviceMode && appearanceItems.push({ description: 'Make Default Layout', @@ -994,11 +1005,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps () => this.autoHeight, autoHeight => autoHeight && this.tryUpdateScrollHeight() ); + this._disposers.width = reaction( + () => this.props.PanelWidth(), + width => this.tryUpdateScrollHeight() + ); this._disposers.scrollHeight = reaction( () => ({ scrollHeight: this.scrollHeight, autoHeight: this.autoHeight, width: NumCast(this.layoutDoc._width) }), - ({ width, scrollHeight, autoHeight }) => { - width && autoHeight && this.resetNativeHeight(scrollHeight); - }, + ({ width, scrollHeight, autoHeight }) => width && autoHeight && this.resetNativeHeight(scrollHeight), { fireImmediately: true } ); this._disposers.componentHeights = reaction( @@ -1030,8 +1043,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps ); this._disposers.editorState = reaction( () => { - const whichDoc = !this.dataDoc || !this.layoutDoc ? undefined : this.dataDoc?.[this.props.fieldKey + '-noTemplate'] || !this.layoutDoc[this.props.fieldKey] ? this.dataDoc : this.layoutDoc; - return !whichDoc ? undefined : { data: Cast(whichDoc[this.props.fieldKey], RichTextField, null), str: StrCast(whichDoc[this.props.fieldKey]) }; + const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc?.proto), this.fieldKey) ? DocCast(this.layoutDoc?.proto) : this?.dataDoc; + const whichDoc = !this.dataDoc || !this.layoutDoc ? undefined : dataDoc?.[this.fieldKey + '-noTemplate'] || !this.layoutDoc[this.fieldKey] ? dataDoc : this.layoutDoc; + return !whichDoc ? undefined : { data: Cast(whichDoc[this.fieldKey], RichTextField, null), str: Field.toString(whichDoc[this.fieldKey]) }; }, incomingValue => { if (this._editorView && this._applyingChange !== this.fieldKey) { @@ -1119,6 +1133,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps ); quickScroll = undefined; this.tryUpdateScrollHeight(); + setTimeout(this.tryUpdateScrollHeight, 250); } pushToGoogleDoc = async () => { @@ -1168,7 +1183,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps let pullSuccess = false; if (exportState !== undefined) { pullSuccess = true; - dataDoc[this.props.fieldKey] = new RichTextField(JSON.stringify(exportState.state.toJSON())); + dataDoc[this.fieldKey] = new RichTextField(JSON.stringify(exportState.state.toJSON())); setTimeout(() => { if (this._editorView) { const state = this._editorView.state; @@ -1296,8 +1311,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } _didScroll = false; setupEditor(config: any, fieldKey: string) { - const curText = Cast(this.dataDoc[this.props.fieldKey], RichTextField, null) || StrCast(this.dataDoc[this.props.fieldKey]); - const rtfField = Cast((!curText && this.layoutDoc[this.props.fieldKey]) || this.dataDoc[fieldKey], RichTextField); + const curText = Cast(this.dataDoc[this.fieldKey], RichTextField, null) || StrCast(this.dataDoc[this.fieldKey]); + const rtfField = Cast((!curText && this.layoutDoc[this.fieldKey]) || this.dataDoc[fieldKey], RichTextField); if (this.ProseRef) { const self = this; this._editorView?.destroy(); @@ -1348,7 +1363,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps }); const { state, dispatch } = this._editorView; if (!rtfField) { - const startupText = Field.toString(this.dataDoc[fieldKey] as Field); + const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc.proto), this.fieldKey) ? DocCast(this.layoutDoc.proto) : this.dataDoc; + const startupText = Field.toString(dataDoc[fieldKey] as Field); if (startupText) { dispatch(state.tr.insertText(startupText)); } @@ -1383,11 +1399,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } else if (this._editorView) { this._editorView.dispatch(this._editorView.state.tr.addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) }))); } - FormattedTextBox.DontSelectInitialText = false; } selectOnLoad && 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. if (this._editorView) { + const tr = this._editorView.state.tr; + const { from, to } = tr.selection; + // for some reason, the selection is sometimes lost in the sidebar view when prosemirror syncs the seledtion with the dom, so reset the selectoin after the document has ben fully instantiated. + if (FormattedTextBox.DontSelectInitialText) setTimeout(() => this._editorView?.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(from), tr.doc.resolve(to)))), 250); this._editorView.state.storedMarks = [ ...(this._editorView.state.storedMarks ?? []), ...(!this._editorView.state.storedMarks?.some(mark => mark.type === schema.marks.user_mark) ? [schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })] : []), @@ -1399,6 +1418,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps ...(Doc.UserDoc().fontWeight === 'bold' ? [schema.mark(schema.marks.strong)] : []), ]; } + FormattedTextBox.DontSelectInitialText = false; } componentWillUnmount() { @@ -1466,7 +1486,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps document.removeEventListener('pointermove', this.onSelectMove); }; onPointerUp = (e: React.PointerEvent): void => { - if (!this._editorView?.state.selection.empty && FormattedTextBox._canAnnotate && !(e.nativeEvent as any).dash) this.setupAnchorMenu(); + if (!this._editorView?.state.selection.empty && !(this._editorView?.state.selection instanceof NodeSelection) && FormattedTextBox._canAnnotate && !(e.nativeEvent as any).dash) this.setupAnchorMenu(); if (!this._downEvent) return; this._downEvent = false; if (this.props.isContentActive(true) && !(e.nativeEvent as any).dash) { @@ -1475,7 +1495,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps !this.props.isSelected(true) && editor.dispatch(editor.state.tr.setSelection(new TextSelection(editor.state.doc.resolve(pcords?.pos || 0)))); let target = e.target as any; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span> while (target && !target.dataset?.targethrefs) target = target.parentElement; - FormattedTextBoxComment.update(this, editor, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc); + FormattedTextBoxComment.update(this, editor, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc, target?.dataset.nopreview); } if (e.button === 0 && this.props.isSelected(true) && !e.altKey) { diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx index bdf59863b..e7ca26d5c 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx @@ -1,5 +1,5 @@ import { Mark, ResolvedPos } from 'prosemirror-model'; -import { EditorState } from 'prosemirror-state'; +import { EditorState, NodeSelection } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; import { Doc } from '../../../../fields/Doc'; import { DocServer } from '../../../DocServer'; @@ -92,7 +92,7 @@ export class FormattedTextBoxComment { FormattedTextBoxComment.tooltip.style.display = ''; } - static update(textBox: FormattedTextBox, view: EditorView, lastState?: EditorState, hrefs: string = '', linkDoc: string = '') { + static update(textBox: FormattedTextBox, view: EditorView, lastState?: EditorState, hrefs: string = '', linkDoc: string = '', noPreview: boolean = false) { FormattedTextBoxComment.textBox = textBox; if (hrefs || !lastState?.doc.eq(view.state.doc) || !lastState?.selection.eq(view.state.selection)) { FormattedTextBoxComment.setupPreview( @@ -102,12 +102,13 @@ export class FormattedTextBoxComment { ?.trim() .split(' ') .filter(h => h), - linkDoc + linkDoc, + noPreview ); } } - static setupPreview(view: EditorView, textBox: FormattedTextBox, hrefs?: string[], linkDoc?: string) { + static setupPreview(view: EditorView, textBox: FormattedTextBox, hrefs?: string[], linkDoc?: string, noPreview?: boolean) { const state = view.state; // this section checks to see if the insertion point is over text entered by a different user. If so, it sets ths comment text to indicate the user and the modification date if (state.selection.$from) { @@ -130,8 +131,8 @@ export class FormattedTextBoxComment { if (state.selection.$from && hrefs?.length) { const nbef = findStartOfMark(state.selection.$from, view, findLinkMark); const naft = findEndOfMark(state.selection.$from, view, findLinkMark) || nbef; - nbef && - naft && + //nbef && + naft && LinkDocPreview.SetLinkInfo({ docProps: textBox.props, linkSrc: textBox.rootDoc, @@ -139,6 +140,7 @@ export class FormattedTextBoxComment { location: (pos => [pos.left, pos.top + 25])(view.coordsAtPos(state.selection.from - Math.max(0, nbef - 1))), hrefs, showHeader: true, + noPreview, }); } } diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts index be501329f..3d9bd6add 100644 --- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts +++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts @@ -2,7 +2,7 @@ import { chainCommands, deleteSelection, exitCode, joinBackward, joinDown, joinU import { redo, undo } from 'prosemirror-history'; import { Schema } from 'prosemirror-model'; import { splitListItem, wrapInList } from 'prosemirror-schema-list'; -import { EditorState, TextSelection, Transaction } from 'prosemirror-state'; +import { EditorState, NodeSelection, TextSelection, Transaction } from 'prosemirror-state'; import { liftTarget } from 'prosemirror-transform'; import { AclAugment, AclSelfEdit, Doc } from '../../../../fields/Doc'; import { GetEffectiveAcl } from '../../../../fields/util'; @@ -143,7 +143,12 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey bind('Alt-\\', (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && setBlockType(schema.nodes.paragraph)(state as any, dispatch as any)); bind('Shift-Ctrl-\\', (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && setBlockType(schema.nodes.code_block)(state as any, dispatch as any)); - bind('Ctrl-m', (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && dispatch(state.tr.replaceSelectionWith(schema.nodes.equation.create({ fieldKey: 'math' + Utils.GenerateGuid() })))); + bind('Ctrl-m', (state: EditorState, dispatch: (tx: Transaction) => void) => { + if (canEdit(state)) { + const tr = state.tr.replaceSelectionWith(schema.nodes.equation.create({ fieldKey: 'math' + Utils.GenerateGuid() })); + dispatch(tr.setSelection(new NodeSelection(tr.doc.resolve(tr.selection.$from.pos - 1)))); + } + }); for (let i = 1; i <= 6; i++) { bind('Shift-Ctrl-' + i, (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && setBlockType(schema.nodes.heading, { level: i })(state as any, dispatch as any)); diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index e5ea7b3b0..e3c67ad2e 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -248,8 +248,7 @@ export class RichTextRules { // [[fieldKey:Doc]] => show field of doc new InputRule(new RegExp(/\[\[([a-zA-Z_\? \-0-9]*)(=[a-zA-Z_@\? /\-0-9]*)?(:[a-zA-Z_@:\.\? \-0-9]+)?\]\]$/), (state, match, start, end) => { const fieldKey = match[1]; - const rawdocid = match[3]; - const docid = rawdocid ? normalizeEmail(!rawdocid.includes('@') ? Doc.CurrentUserEmail + rawdocid : rawdocid.substring(1)) : undefined; + const docid = match[3]?.replace(':', ''); const value = match[2]?.substring(1); if (!fieldKey) { if (docid) { @@ -259,7 +258,7 @@ export class RichTextRules { if (rstate) { this.TextBox.EditorView?.dispatch(rstate.tr.setSelection(new TextSelection(rstate.doc.resolve(start), rstate.doc.resolve(end - 3)))); } - const target = (docx instanceof Doc && docx) || Docs.Create.FreeformDocument([], { title: rawdocid.replace(/^:/, ''), _width: 500, _height: 500 }, docid); + const target = (docx instanceof Doc && docx) || Docs.Create.FreeformDocument([], { title: docid, _width: 500, _height: 500 }, docid); DocUtils.MakeLink({ doc: this.TextBox.getAnchor() }, { doc: target }, 'portal to:portal from', undefined); const fstate = this.TextBox.EditorView?.state; @@ -275,7 +274,7 @@ export class RichTextRules { const num = value.match(/^[0-9.]$/); this.Document[DataSym][fieldKey] = value === 'true' ? true : value === 'false' ? false : num ? Number(value) : value; } - const fieldView = state.schema.nodes.dashField.create({ fieldKey, docid }); + const fieldView = state.schema.nodes.dashField.create({ fieldKey, docid, hideKey: false }); return state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).replaceSelectionWith(fieldView, true); }), @@ -295,6 +294,15 @@ export class RichTextRules { return state.tr; }), + // create an inline equation node + // eq:<equation>> + new InputRule(new RegExp(/:eq([a-zA-Z-0-9\(\)]*)$/), (state, match, start, end) => { + const fieldKey = 'math' + Utils.GenerateGuid(); + this.TextBox.dataDoc[fieldKey] = match[1]; + const tr = state.tr.setSelection(new TextSelection(state.tr.doc.resolve(end - 3), state.tr.doc.resolve(end))).replaceSelectionWith(schema.nodes.equation.create({ fieldKey })); + return tr.setSelection(new NodeSelection(tr.doc.resolve(tr.selection.$from.pos - 1))); + }), + // create an inline view of a document {{ <layoutKey> : <Doc> }} // {{:Doc}} => show default view of document // {{<layout>}} => show layout for this doc diff --git a/src/client/views/nodes/formattedText/SummaryView.tsx b/src/client/views/nodes/formattedText/SummaryView.tsx index 01acc3de9..1fe6d822b 100644 --- a/src/client/views/nodes/formattedText/SummaryView.tsx +++ b/src/client/views/nodes/formattedText/SummaryView.tsx @@ -1,6 +1,6 @@ import { TextSelection } from 'prosemirror-state'; import { Fragment, Node, Slice } from 'prosemirror-model'; -import * as ReactDOM from 'react-dom'; +import * as ReactDOM from 'react-dom/client'; import React = require('react'); // an elidable textblock that collapses when its '<-' is clicked and expands when its '...' anchor is clicked. @@ -9,6 +9,7 @@ import React = require('react'); // method instead of changing prosemirror's text when the expand/elide buttons are clicked. export class SummaryView { dom: HTMLSpanElement; // container for label and value + root: any; constructor(node: any, view: any, getPos: any) { const self = this; @@ -35,13 +36,13 @@ export class SummaryView { return js.apply(this, arguments); }; - ReactDOM.render(<SummaryViewInternal />, this.dom); - (this as any).dom = this.dom; + this.root = ReactDOM.createRoot(this.dom); + this.root.render(<SummaryViewInternal />); } className = (visible: boolean) => 'formattedTextBox-summarizer' + (visible ? '' : '-collapsed'); destroy() { - ReactDOM.unmountComponentAtNode(this.dom); + // ReactDOM.unmountComponentAtNode(this.dom); } selectNode() {} diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts index 00c41e187..97549830c 100644 --- a/src/client/views/nodes/formattedText/marks_rts.ts +++ b/src/client/views/nodes/formattedText/marks_rts.ts @@ -75,6 +75,7 @@ export const marks: { [index: string]: MarkSpec } = { allAnchors: { default: [] as { href: string; title: string; anchorId: string }[] }, location: { default: null }, title: { default: null }, + noPreview: { default: false }, docref: { default: false }, // flags whether the linked text comes from a document within Dash. If so, an attribution label is appended after the text }, inclusive: false, @@ -85,6 +86,7 @@ export const marks: { [index: string]: MarkSpec } = { return { location: dom.getAttribute('location'), title: dom.getAttribute('title'), + noPreview: dom.getAttribute('noPreview'), }; }, }, @@ -111,7 +113,7 @@ export const marks: { [index: string]: MarkSpec } = { ['br'], ] : //node.attrs.allLinks.length === 1 ? - ['a', { class: anchorids, 'data-targethrefs': targethrefs, title: node.attrs.title, location: node.attrs.location, style: `text-decoration: underline` }, 0]; + ['a', { class: anchorids, 'data-targethrefs': targethrefs, title: node.attrs.title, 'data-noPreview': node.attrs.noPreview, location: node.attrs.location, style: `text-decoration: underline` }, 0]; // ["div", { class: "prosemirror-anchor" }, // ["span", { class: "prosemirror-linkBtn" }, // ["a", { ...node.attrs, class: linkids, "data-targetids": targetids, title: `${node.attrs.title}` }, 0], diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts index 5142b7da6..aa2475dca 100644 --- a/src/client/views/nodes/formattedText/nodes_rts.ts +++ b/src/client/views/nodes/formattedText/nodes_rts.ts @@ -157,6 +157,18 @@ export const nodes: { [index: string]: NodeSpec } = { }, }, + equation: { + inline: true, + attrs: { + fieldKey: { default: '' }, + }, + group: 'inline', + toDOM(node) { + const attrs = { style: `width: ${node.attrs.width}, height: ${node.attrs.height}` }; + return ['div', { ...node.attrs, ...attrs }]; + }, + }, + // :: NodeSpec The text node. text: { group: 'inline', @@ -251,6 +263,7 @@ export const nodes: { [index: string]: NodeSpec } = { fieldKey: { default: '' }, docid: { default: '' }, hideKey: { default: false }, + editable: { default: true }, }, group: 'inline', draggable: false, @@ -260,20 +273,6 @@ export const nodes: { [index: string]: NodeSpec } = { }, }, - equation: { - inline: true, - attrs: { - fieldKey: { default: '' }, - }, - atom: true, - group: 'inline', - draggable: false, - toDOM(node) { - const attrs = { style: `width: ${node.attrs.width}, height: ${node.attrs.height}` }; - return ['div', { ...node.attrs, ...attrs }]; - }, - }, - video: { inline: true, attrs: { diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index 0c4d514cd..4e8aed8a6 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -5,14 +5,15 @@ import { action, computed, IReactionDisposer, observable, ObservableSet, reactio import { observer } from 'mobx-react'; import { ColorState, SketchPicker } from 'react-color'; import { Bounce, Fade, Flip, LightSpeed, Roll, Rotate, Zoom } from 'react-reveal'; -import { Doc, DocListCast, DocListCastAsync, FieldResult, Opt } from '../../../../fields/Doc'; -import { Copy } from '../../../../fields/FieldSymbols'; +import { Doc, DocListCast, FieldResult, StrListCast } from '../../../../fields/Doc'; +import { Copy, Id } from '../../../../fields/FieldSymbols'; import { InkTool } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; import { ObjectField } from '../../../../fields/ObjectField'; import { listSpec } from '../../../../fields/Schema'; import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types'; import { emptyFunction, returnFalse, returnOne, returnTrue, setupMoveUpEvents, StopEvent } from '../../../../Utils'; +import { DocServer } from '../../../DocServer'; import { Docs } from '../../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; import { DocumentManager } from '../../../util/DocumentManager'; @@ -29,6 +30,7 @@ import { Colors } from '../../global/globalEnums'; import { LightboxView } from '../../LightboxView'; import { CollectionFreeFormDocumentView } from '../CollectionFreeFormDocumentView'; import { FieldView, FieldViewProps } from '../FieldView'; +import { ScriptingBox } from '../ScriptingBox'; import './PresBox.scss'; import { PresEffect, PresMovement, PresStatus } from './PresEnums'; @@ -36,14 +38,9 @@ export interface PinProps { audioRange?: boolean; activeFrame?: number; hidePresBox?: boolean; - pinWithView?: PinViewProps; - pinDocView?: boolean; // whether the current view specs of the document should be saved the pinned document - panelWidth?: number; // panel width and height of the document (used to compute the bounds of the pinned view area) - panelHeight?: number; -} - -export interface PinViewProps { - bounds: MarqueeViewBounds; + pinViewport?: MarqueeViewBounds; // pin a specific viewport on a freeform view (use MarqueeView.CurViewBounds to compute if no region has been selected) + pinDocLayout?: boolean; // pin layout info (width/height/x/y) + pinDocContent?: boolean; // pin data info (scroll/pan/zoom/text) } @observer @@ -268,6 +265,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { // Case 3: Last slide and presLoop is toggled ON or it is in Edit mode this.nextSlide(0); } + return this.itemIndex; }; // Called when the user activates 'back' - to move to the previous part of the pres. trail @@ -298,6 +296,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { // Case 3: Pres loop is on so it should go to the last slide this.gotoDocument(this.childDocs.length - 1, activeItem); } + return this.itemIndex; }; //The function that is called when a document is clicked or reached through next or back. @@ -351,16 +350,17 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const pannable = [DocumentType.IMG].includes(target.type as any) || (target.type === DocumentType.COL && target._viewType === CollectionViewType.Freeform); const temporal = [DocumentType.AUDIO, DocumentType.VID].includes(target.type as any); const clippable = [DocumentType.COMPARISON].includes(target.type as any); - const dataview = [DocumentType.INK].includes(target.type as any) && target.activeFrame === undefined; + const dataview = [DocumentType.INK, DocumentType.COL].includes(target.type as any) && target.activeFrame === undefined; + const poslayoutview = [DocumentType.COL].includes(target.type as any) && target.activeFrame === undefined; const textview = [DocumentType.RTF].includes(target.type as any) && target.activeFrame === undefined; - return { scrollable, pannable, temporal, clippable, dataview, textview }; + return { scrollable, pannable, temporal, clippable, dataview, textview, poslayoutview }; } @action static restoreTargetDocView(bestTarget: Doc, activeItem: Doc) { const transTime = NumCast(activeItem.presTransition, 500); const presTransitionTime = `all ${transTime}ms`; - const { scrollable, pannable, temporal, clippable, dataview, textview } = this.pinDataTypes(bestTarget); + const { scrollable, pannable, temporal, clippable, dataview, textview, poslayoutview } = this.pinDataTypes(bestTarget); bestTarget._viewTransition = presTransitionTime; if (clippable) bestTarget._clipWidth = activeItem.presPinClipWidth; if (temporal) bestTarget._currentTimecode = activeItem.presStartTime; @@ -372,8 +372,32 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { dv?.brushView?.({ panX: (contentBounds[0] + contentBounds[2]) / 2, panY: (contentBounds[1] + contentBounds[3]) / 2, width: contentBounds[2] - contentBounds[0], height: contentBounds[3] - contentBounds[1] }); } } - if (dataview) Doc.GetProto(bestTarget).data = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData; - if (textview) Doc.GetProto(bestTarget).text = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData; + if (dataview) Doc.GetProto(bestTarget)[Doc.LayoutFieldKey(bestTarget)] = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData; + if (textview) Doc.GetProto(bestTarget)[Doc.LayoutFieldKey(bestTarget)] = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData; + if (poslayoutview) { + StrListCast(activeItem.presPinLayoutData) + .map(str => JSON.parse(str) as { id: string; x: number; y: number; w: number; h: number }) + .forEach(data => { + const doc = DocServer.GetCachedRefField(data.id) as Doc; + doc._dataTransition = presTransitionTime; + doc.x = data.x; + doc.y = data.y; + doc._width = data.w; + doc._height = data.h; + }); + setTimeout( + () => + StrListCast(activeItem.presPinLayoutData) + .map(str => JSON.parse(str) as { id: string; x: number; y: number; w: number; h: number }) + .forEach( + action(data => { + const doc = DocServer.GetCachedRefField(data.id) as Doc; + doc._dataTransition = undefined; + }) + ), + transTime + 10 + ); + } if (pannable) { const contentBounds = Cast(activeItem.presPinViewBounds, listSpec('number')); if (contentBounds) { @@ -383,7 +407,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const dv = DocumentManager.Instance.getDocumentView(bestTarget); if (dv) { const computedScale = NumCast(activeItem.presZoom, 1) * Math.min(dv.props.PanelWidth() / viewport.width, dv.props.PanelHeight() / viewport.height); - activeItem.presMovement === 'zoom' && (bestTarget._viewScale = activeItem.presZoom !== undefined ? computedScale : Math.min(computedScale, NumCast(bestTarget._viewScale))); + activeItem.presMovement === 'zoom' && (bestTarget._viewScale = computedScale); dv.ComponentView?.brushView?.(viewport); } } else { @@ -400,47 +424,41 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { /// target doc when navigating to it. @action static pinDocView(pinDoc: Doc, pinProps: PinProps | undefined, targetDoc: Doc) { - if (pinProps?.pinWithView) { - // If pinWithView option set then update scale and x / y props of slide - const bounds = pinProps.pinWithView.bounds; - pinDoc.presPinView = true; - pinDoc.presPinViewX = bounds.left + bounds.width / 2; - pinDoc.presPinViewY = bounds.top + bounds.height / 2; - pinDoc.presPinViewBounds = new List<number>([bounds.left, bounds.top, bounds.left + bounds.width, bounds.top + bounds.height]); - } - if (pinProps?.pinDocView) { - const { scrollable, pannable, temporal, clippable, dataview, textview } = this.pinDataTypes(pinDoc); - pinDoc.presPinView = (pinProps?.pinWithView ? true : false) || scrollable || temporal || pannable || clippable || dataview || textview || pinProps.activeFrame !== undefined; + const { scrollable, pannable, temporal, clippable, dataview, textview, poslayoutview } = this.pinDataTypes(pinDoc); + if (pinProps?.pinDocLayout) { + pinDoc.presPinLayout = true; pinDoc.presX = NumCast(targetDoc.x); pinDoc.presY = NumCast(targetDoc.y); - pinDoc.presRot = NumCast(targetDoc.jitterRotation); + pinDoc.presRot = NumCast(targetDoc.rotation); pinDoc.presWidth = NumCast(targetDoc.width); pinDoc.presHeight = NumCast(targetDoc.height); - - if (scrollable) { - pinDoc.presPinViewScroll = pinDoc._scrollTop; - } + } + if (pinProps?.pinDocContent) { + pinDoc.presPinData = scrollable || temporal || pannable || clippable || dataview || textview || poslayoutview || pinProps.activeFrame !== undefined; + if (dataview) pinDoc.presData = targetDoc[Doc.LayoutFieldKey(targetDoc)] instanceof ObjectField ? (targetDoc[Doc.LayoutFieldKey(targetDoc)] as ObjectField)[Copy]() : targetDoc.data; + if (textview) pinDoc.presData = targetDoc[Doc.LayoutFieldKey(targetDoc)] instanceof ObjectField ? (targetDoc[Doc.LayoutFieldKey(targetDoc)] as ObjectField)[Copy]() : targetDoc.text; + if (scrollable) pinDoc.presPinViewScroll = pinDoc._scrollTop; if (clippable) pinDoc.presPinClipWidth = pinDoc._clipWidth; + if (poslayoutview) pinDoc.presPinLayoutData = new List<string>(DocListCast(pinDoc.presData).map(d => JSON.stringify({ id: d[Id], x: NumCast(d.x), y: NumCast(d.y), w: NumCast(d._width), h: NumCast(d._height) }))); + if (pannable) { + pinDoc.presPinViewX = NumCast(pinDoc._panX); + pinDoc.presPinViewY = NumCast(pinDoc._panY); + pinDoc.presPinViewScale = NumCast(pinDoc._viewScale, 1); + } if (temporal) { pinDoc.presStartTime = pinDoc._currentTimecode; const duration = NumCast(pinDoc[`${Doc.LayoutFieldKey(pinDoc)}-duration`], NumCast(pinDoc.presStartTime) + 0.1); pinDoc.presEndTime = NumCast(pinDoc.clipEnd, duration); } - if (textview) pinDoc.presData = targetDoc.text instanceof ObjectField ? targetDoc.text[Copy]() : targetDoc.text; - if (dataview) pinDoc.presData = targetDoc.data instanceof ObjectField ? targetDoc.data[Copy]() : targetDoc.data; - if (pannable || scrollable) { - const panX = NumCast(pinDoc._panX); - const panY = NumCast(pinDoc._panY); - const pw = NumCast(pinProps.panelWidth); - const ph = NumCast(pinProps.panelHeight); - const ps = NumCast(pinDoc._viewScale, 1); - if (pw && ph && ps) { - pinDoc.presPinViewBounds = new List<number>([panX - pw / 2 / ps, panY - ph / 2 / ps, panX + pw / 2 / ps, panY + ph / 2 / ps]); - } - pinDoc.presPinViewX = panX; - pinDoc.presPinViewY = panY; - pinDoc.presPinViewScale = ps; - } + } + if (pinProps?.pinViewport) { + // If pinWithView option set then update scale and x / y props of slide + const bounds = pinProps.pinViewport; + pinDoc.presPinView = true; + pinDoc.presPinViewScale = NumCast(pinDoc._viewScale, 1); + pinDoc.presPinViewX = bounds.left + bounds.width / 2; + pinDoc.presPinViewY = bounds.top + bounds.height / 2; + pinDoc.presPinViewBounds = new List<number>([bounds.left, bounds.top, bounds.left + bounds.width, bounds.top + bounds.height]); } } @@ -497,13 +515,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }; static NavigateToTarget(targetDoc: Doc, activeItem: Doc, openInTab: any, srcContext: Doc, finished?: () => void) { - if (activeItem.presPinView && DocCast(targetDoc.context)?._currentFrame === undefined) { + if ((activeItem.presPinLayout || activeItem.presPinView) && DocCast(targetDoc.context)?._currentFrame === undefined) { const transTime = NumCast(activeItem.presTransition, 500); const presTransitionTime = `all ${transTime}ms`; targetDoc._dataTransition = presTransitionTime; targetDoc.x = NumCast(activeItem.presX, NumCast(targetDoc.x)); targetDoc.y = NumCast(activeItem.presY, NumCast(targetDoc.y)); - targetDoc.jitterRotation = NumCast(activeItem.presRot, NumCast(targetDoc.jitterRotation)); + targetDoc.rotation = NumCast(activeItem.presRot, NumCast(targetDoc.rotation)); targetDoc.width = NumCast(activeItem.presWidth, NumCast(targetDoc.width)); targetDoc.height = NumCast(activeItem.presHeight, NumCast(targetDoc.height)); setTimeout(() => (targetDoc._dataTransition = undefined), transTime + 10); @@ -514,11 +532,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } else if (targetDoc && activeItem.presMovement !== PresMovement.None) { LightboxView.SetLightboxDoc(undefined); const zooming = activeItem.presMovement !== PresMovement.Pan; - DocumentManager.Instance.jumpToDocument(targetDoc, zooming, openInTab, srcContext ? [srcContext] : [], undefined, undefined, undefined, finished, undefined, true, NumCast(activeItem.presZoom)); + DocumentManager.Instance.jumpToDocument(targetDoc, zooming, openInTab, srcContext ? [srcContext] : [], undefined, undefined, undefined, finished, undefined, true, NumCast(activeItem.presZoom, 1)); + } else if (activeItem.presMovement === PresMovement.None && targetDoc.type === DocumentType.SCRIPTING) { + (DocumentManager.Instance.getFirstDocumentView(targetDoc)?.ComponentView as ScriptingBox)?.onRun?.(); } // After navigating to the document, if it is added as a presPinView then it will // adjust the pan and scale to that of the pinView when it was added. - if (activeItem.presPinView) { + if (activeItem.presPinData || activeItem.presPinView) { clearTimeout(PresBox._navTimer); // targetDoc may or may not be displayed. this gets the first available document (or alias) view that matches targetDoc const bestTarget = DocumentManager.Instance.getFirstDocumentView(targetDoc)?.props.Document; @@ -615,42 +635,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { //The function that starts or resets presentaton functionally, depending on presStatus of the layoutDoc @action startAutoPres = (startSlide: number) => { - this.updateCurrentPresentation(); - let activeItem: Doc = this.activeItem; - let targetDoc: Doc = this.targetDoc; - let duration = NumCast(activeItem.presDuration) + NumCast(activeItem.presTransition); - const timer = (ms: number) => new Promise(res => (this._presTimer = setTimeout(res, ms))); - const load = async () => { - // Wrap the loop into an async function for this to work - for (var i = startSlide; i < this.childDocs.length; i++) { - activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); - duration = NumCast(activeItem.presDuration) + NumCast(activeItem.presTransition); - if (duration < 100) { - duration = 2500; - } - if (NumCast(targetDoc.lastFrame) > 0) { - for (var f = 0; f < NumCast(targetDoc.lastFrame); f++) { - await timer(duration / NumCast(targetDoc.lastFrame)); - this.next(); - } - } - - await timer(duration); - this.next(); // then the created Promise can be awaited - if (i === this.childDocs.length - 1) { - setTimeout(() => { - clearTimeout(this._presTimer); - if (this.layoutDoc.presStatus === 'auto' && !this.layoutDoc.presLoop) this.layoutDoc.presStatus = PresStatus.Manual; - else if (this.layoutDoc.presLoop) this.startAutoPres(0); - }, duration); - } - } - }; this.layoutDoc.presStatus = PresStatus.Autoplay; this.startPresentation(startSlide); - this.gotoDocument(startSlide, activeItem); - load(); + clearTimeout(this._presTimer); + const func = (itemIndex: number) => { + if (itemIndex === this.next()) this.layoutDoc.presStatus = PresStatus.Manual; + else + this._presTimer = setTimeout( + () => this.layoutDoc.presStatus !== PresStatus.Manual && func(this.itemIndex), + NumCast(this.activeItem.presDuration, this.activeItem.type === DocumentType.SCRIPTING ? 0 : 2500) + NumCast(this.activeItem.presTransition) + ); + }; + + func(this.itemIndex); }; // The function pauses the auto presentation @@ -659,7 +656,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (this.layoutDoc.presStatus === PresStatus.Autoplay) { if (this._presTimer) clearTimeout(this._presTimer); this.layoutDoc.presStatus = PresStatus.Manual; - this.layoutDoc.presLoop = false; this.childDocs.forEach(this.stopTempMedia); } }; @@ -667,7 +663,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { //The function that resets the presentation by removing every action done by it. It also //stops the presentaton. resetPresentation = () => { - this.rootDoc._itemIndex = 0; this.childDocs .map(doc => Cast(doc.presentationTargetDoc, Doc, null)) .filter(doc => doc instanceof Doc) @@ -706,8 +701,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { * @param startIndex: index that the presentation will start at */ startPresentation = (startIndex: number) => { - this.updateCurrentPresentation(); - this.childDocs.map(doc => { + this.childDocs.forEach(doc => { const tagDoc = doc.presentationTargetDoc as Doc; if (doc.presHideBefore && this.childDocs.indexOf(doc) > startIndex) { tagDoc.opacity = 0; @@ -716,6 +710,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { tagDoc.opacity = 0; } }); + this.gotoDocument(startIndex, this.activeItem); }; /** @@ -996,7 +991,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { break; case 'Spacebar': case ' ': - if (this.layoutDoc.presStatus === PresStatus.Manual) this.startAutoPres(this.itemIndex); + if (this.layoutDoc.presStatus === PresStatus.Manual) this.startOrPause(true); else if (this.layoutDoc.presStatus === PresStatus.Autoplay) if (this._presTimer) clearTimeout(this._presTimer); handled = true; break; @@ -1254,7 +1249,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (activeItem && targetDoc) { const type = targetDoc.type; const transitionSpeed = activeItem.presTransition ? NumCast(activeItem.presTransition) / 1000 : 0.5; - const zoom = activeItem.presZoom ? NumCast(activeItem.presZoom) * 100 : 75; + const zoom = NumCast(activeItem.presZoom, 1) * 100; let duration = activeItem.presDuration ? NumCast(activeItem.presDuration) / 1000 : 2; if (activeItem.type === DocumentType.AUDIO) duration = NumCast(activeItem.duration); const effect = this.activeItem.presEffect ? this.activeItem.presEffect : 'None'; @@ -1808,6 +1803,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { ); } + scrollFocus = () => { + this.startOrPause(false); + return undefined; + }; + // Case in which the document has keyframes to navigate to next key frame @action nextKeyframe = (tagDoc: Doc, curDoc: Doc): void => { @@ -2480,7 +2480,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <FontAwesomeIcon icon={'arrow-left'} /> </div> <Tooltip title={<div className="dash-tooltip">{this.layoutDoc.presStatus === PresStatus.Autoplay ? 'Pause' : 'Autoplay'}</div>}> - <div className="presPanel-button" onClick={this.startOrPause}> + <div className="presPanel-button" onClick={e => this.startOrPause(true)}> <FontAwesomeIcon icon={this.layoutDoc.presStatus === PresStatus.Autoplay ? 'pause' : 'play'} /> </div> </Tooltip> @@ -2529,7 +2529,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } @action - startOrPause = () => { + startOrPause = (makeActive = true) => { + makeActive && this.updateCurrentPresentation(); if (this.layoutDoc.presStatus === PresStatus.Manual || this.layoutDoc.presStatus === PresStatus.Edit) this.startAutoPres(this.itemIndex); else this.pauseAutoPres(); }; @@ -2613,7 +2614,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <FontAwesomeIcon icon={'arrow-left'} /> </div> <Tooltip title={<div className="dash-tooltip">{this.layoutDoc.presStatus === PresStatus.Autoplay ? 'Pause' : 'Autoplay'}</div>}> - <div className="presPanel-button" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, this.startOrPause, false, false)}> + <div className="presPanel-button" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, () => this.startOrPause(true), false, false)}> <FontAwesomeIcon icon={this.layoutDoc.presStatus === 'auto' ? 'pause' : 'play'} /> </div> </Tooltip> @@ -2651,9 +2652,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { PanelHeight={this.panelHeight} childIgnoreNativeSize={true} moveDocument={returnFalse} - childFitWidth={returnTrue} + //childFitWidth={returnTrue} childOpacity={returnOne} childLayoutTemplate={this.childLayoutTemplate} + childXPadding={Doc.IsComicStyle(this.rootDoc) ? 20 : undefined} filterAddDocument={this.addDocumentFilter} removeDocument={returnFalse} dontRegisterView={true} diff --git a/src/client/views/nodes/trails/PresElementBox.scss b/src/client/views/nodes/trails/PresElementBox.scss index 8a6c2a6dc..415253af1 100644 --- a/src/client/views/nodes/trails/PresElementBox.scss +++ b/src/client/views/nodes/trails/PresElementBox.scss @@ -42,10 +42,7 @@ $slide-active: #5b9fdd; width: 100%; border-bottom: 0.5px solid grey; display: flex; - justify-content: space-between; align-items: center; - grid-template-rows: 16px 10px auto; - grid-template-columns: max-content max-content max-content max-content auto; .presItem-name { display: flex; @@ -102,13 +99,7 @@ $slide-active: #5b9fdd; grid-row: 3; grid-column: 1/8; position: relative; - display: flex; - width: auto; - justify-content: center; - margin: auto; - margin-bottom: 2px; - border-bottom-left-radius: 5px; - border-bottom-right-radius: 5px; + display: inline-block; } .presItem-embeddedMask { @@ -124,8 +115,7 @@ $slide-active: #5b9fdd; .presItem-slideButtons { display: flex; - grid-column: 7; - grid-row: 1/3; + position: absolute; width: max-content; justify-self: right; justify-content: flex-end; diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx index e6d08cd53..5d14a0e9a 100644 --- a/src/client/views/nodes/trails/PresElementBox.tsx +++ b/src/client/views/nodes/trails/PresElementBox.tsx @@ -4,7 +4,9 @@ import { action, computed, IReactionDisposer, observable, reaction } from 'mobx' import { observer } from 'mobx-react'; import { Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../../fields/Doc'; import { Copy, Id } from '../../../../fields/FieldSymbols'; +import { InkField } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; +import { RichTextField } from '../../../../fields/RichTextField'; import { Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types'; import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils'; import { Docs, DocUtils } from '../../../documents/Documents'; @@ -14,6 +16,7 @@ import { DragManager } from '../../../util/DragManager'; import { SettingsManager } from '../../../util/SettingsManager'; import { Transform } from '../../../util/Transform'; import { undoBatch } from '../../../util/UndoManager'; +import { MarqueeView } from '../../collections/collectionFreeForm'; import { ViewBoxBaseComponent } from '../../DocComponent'; import { EditableView } from '../../EditableView'; import { Colors } from '../../global/globalEnums'; @@ -24,8 +27,6 @@ import { PresBox } from './PresBox'; import './PresElementBox.scss'; import { PresMovement } from './PresEnums'; import React = require('react'); -import { InkField } from '../../../../fields/InkField'; -import { RichTextField } from '../../../../fields/RichTextField'; /** * This class models the view a document added to presentation will have in the presentation. * It involves some functionality for its buttons and options. @@ -43,8 +44,11 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { @computed get indexInPres() { return DocListCast(this.presBox[StrCast(this.presBox.presFieldKey, 'data')]).indexOf(this.rootDoc); } // the index field is where this document is in the presBox display list (since this value is different for each presentation element, the value can't be stored on the layout template which is used by all display elements) + @computed get expandViewHeight() { + return 100; + } @computed get collapsedHeight() { - return [CollectionViewType.Tree, CollectionViewType.Stacking].includes(this.presBox._viewType as any) ? 35 : 31; + return 35; } // the collapsed height changes depending on the state of the presBox. We could store this on the presentation element template if it's used by only one presentation - but if it's shared by multiple, then this value must be looked up @computed get presStatus() { return this.presBox.presStatus; @@ -57,7 +61,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { return vpath.length > 1 ? (vpath[vpath.length - 2].ComponentView as PresBox) : undefined; } @computed get presBox() { - return (this.props.DocumentView?.().props.treeViewDoc ?? this.props.ContainingCollectionDoc)!; + return this.props.ContainingCollectionDoc!; } @computed get targetDoc() { return Cast(this.rootDoc.presentationTargetDoc, Doc, null) || this.rootDoc; @@ -66,8 +70,8 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { componentDidMount() { this.layoutDoc.hideLinkButton = true; this._heightDisposer = reaction( - () => [this.rootDoc.presExpandInlineButton, this.collapsedHeight], - params => (this.layoutDoc._height = NumCast(params[1]) + (Number(params[0]) ? 100 : 0)), + () => ({ expand: this.rootDoc.presExpandInlineButton, height: this.collapsedHeight }), + ({ expand, height }) => (this.layoutDoc._height = height + (expand ? this.expandViewHeight : 0)), { fireImmediately: true } ); } @@ -83,10 +87,10 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { @action presExpandDocumentClick = () => (this.rootDoc.presExpandInlineButton = !this.rootDoc.presExpandInlineButton); - embedHeight = (): number => 97; + embedHeight = (): number => this.collapsedHeight + this.expandViewHeight; // embedWidth = () => this.props.PanelWidth(); // embedHeight = () => Math.min(this.props.PanelWidth() - 20, this.props.PanelHeight() - this.collapsedHeight); - embedWidth = (): number => this.props.PanelWidth() - 35; + embedWidth = (): number => this.props.PanelWidth() / 2; styleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string): any => { if (property === StyleProp.Opacity) return 1; return this.props.styleProvider?.(doc, props, property); @@ -97,35 +101,35 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { */ @computed get renderEmbeddedInline() { return !this.rootDoc.presExpandInlineButton || !this.targetDoc ? null : ( - <div className="presItem-embedded" style={{ height: this.embedHeight(), width: this.embedWidth() }}> + <div className="presItem-embedded" style={{ height: this.embedHeight(), width: '50%' }}> <DocumentView Document={this.rootDoc} DataDoc={undefined} //this.targetDoc[DataSym] !== this.targetDoc && this.targetDoc[DataSym]} + PanelWidth={this.embedWidth} + PanelHeight={this.embedHeight} + isContentActive={this.props.isContentActive} styleProvider={this.styleProvider} + hideLinkButton={true} + ScreenToLocalTransform={Transform.Identity} + renderDepth={this.props.renderDepth + 1} docViewPath={returnEmptyDoclist} + docFilters={this.props.docFilters} + docRangeFilters={this.props.docRangeFilters} + searchFilterDocs={this.props.searchFilterDocs} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} rootSelected={returnTrue} addDocument={returnFalse} removeDocument={returnFalse} - isContentActive={this.props.isContentActive} - addDocTab={returnFalse} - pinToPres={returnFalse} fitContentsToBox={returnTrue} - PanelWidth={this.embedWidth} - PanelHeight={this.embedHeight} - ScreenToLocalTransform={Transform.Identity} moveDocument={this.props.moveDocument!} - renderDepth={this.props.renderDepth + 1} focus={DocUtils.DefaultFocus} whenChildContentsActiveChanged={returnFalse} + addDocTab={returnFalse} + pinToPres={returnFalse} bringToFront={returnFalse} - docFilters={this.props.docFilters} - docRangeFilters={this.props.docRangeFilters} - searchFilterDocs={this.props.searchFilterDocs} - ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} - hideLinkButton={true} /> - <div className="presItem-embeddedMask" /> + {/* <div className="presItem-embeddedMask" /> */} </div> ); } @@ -142,7 +146,6 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { this.presExpandDocumentClick(); }}> <div className="presItem-groupNum">{`${ind + 1}.`}</div> - {/* style={{ maxWidth: showMore ? (toolbarWidth - 195) : toolbarWidth - 105, cursor: isSelected ? 'text' : 'grab' }} */} <div className="presItem-name"> <EditableView ref={this._titleRef} @@ -206,8 +209,8 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { const dragData = new DragManager.DocumentDragData(this.presBoxView?.sortArray() ?? []); if (!dragData.draggedDocuments.length) dragData.draggedDocuments.push(this.rootDoc); dragData.dropAction = 'move'; - dragData.treeViewDoc = this.props.docViewPath().lastElement()?.props.treeViewDoc; - dragData.moveDocument = this.props.docViewPath().lastElement()?.props.moveDocument; + dragData.treeViewDoc = this.presBox._viewType === CollectionViewType.Tree ? this.props.ContainingCollectionDoc : undefined; // this.props.DocumentView?.()?.props.treeViewDoc; + dragData.moveDocument = this.props.moveDocument; const dragItem: HTMLElement[] = []; if (dragArray.length === 1) { const doc = this._itemRef.current || dragArray[0]; @@ -304,7 +307,22 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { */ @undoBatch @action - updateView = (targetDoc: Doc, activeItem: Doc) => { + updateCapturedContainerLayout = (targetDoc: Doc, activeItem: Doc) => { + activeItem.presX = NumCast(targetDoc.x); + activeItem.presY = NumCast(targetDoc.y); + activeItem.presRot = NumCast(targetDoc.rotation); + activeItem.presWidth = NumCast(targetDoc.width); + activeItem.presHeight = NumCast(targetDoc.height); + }; + /** + * Method called for updating the view of the currently selected document + * + * @param targetDoc + * @param activeItem + */ + @undoBatch + @action + updateCapturedViewContents = (targetDoc: Doc, activeItem: Doc) => { switch (targetDoc.type) { case DocumentType.PDF: case DocumentType.WEB: @@ -312,11 +330,11 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { const scroll = targetDoc._scrollTop; activeItem.presPinViewScroll = scroll; if (targetDoc.type === DocumentType.RTF) { - activeItem.presData = targetDoc.text instanceof RichTextField ? targetDoc.text[Copy]() : targetDoc.text; + activeItem.presData = targetDoc[Doc.LayoutFieldKey(targetDoc)] instanceof RichTextField ? (targetDoc[Doc.LayoutFieldKey(targetDoc)] as RichTextField)[Copy]() : targetDoc.text; } break; case DocumentType.INK: - activeItem.presData = targetDoc.data instanceof InkField ? targetDoc.data[Copy]() : targetDoc.data; + activeItem.presData = targetDoc[Doc.LayoutFieldKey(targetDoc)] instanceof InkField ? (targetDoc[Doc.LayoutFieldKey(targetDoc)] as InkField)[Copy]() : targetDoc.data; break; case DocumentType.VID: case DocumentType.AUDIO: @@ -326,20 +344,23 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { const clipWidth = targetDoc._clipWidth; activeItem.presPinClipWidth = clipWidth; break; + case DocumentType.COL: + activeItem.presPinLayoutData = new List<string>(DocListCast(targetDoc[Doc.LayoutFieldKey(targetDoc)]).map(d => JSON.stringify({ id: d[Id], x: NumCast(d.x), y: NumCast(d.y), w: NumCast(d._width), h: NumCast(d._height) }))); default: - const x = targetDoc._panX; - const y = targetDoc._panY; - const scale = targetDoc._viewScale; - activeItem.presPinViewX = x; - activeItem.presPinViewY = y; - activeItem.presPinViewScale = scale; + const bestView = DocumentManager.Instance.getFirstDocumentView(targetDoc); + if (activeItem.presPinViewBounds && bestView) { + const bounds = MarqueeView.CurViewBounds(targetDoc, bestView.props.PanelWidth(), bestView.props.PanelHeight()); + activeItem.presPinView = true; + activeItem.presPinViewScale = NumCast(targetDoc._viewScale, 1); + activeItem.presPinViewX = bounds.left + bounds.width / 2; + activeItem.presPinViewY = bounds.top + bounds.height / 2; + activeItem.presPinViewBounds = new List<number>([bounds.left, bounds.top, bounds.left + bounds.width, bounds.top + bounds.height]); + } else { + activeItem.presPinViewX = targetDoc._panX; + activeItem.presPinViewY = targetDoc._panY; + activeItem.presPinViewScale = targetDoc._viewScale; + } } - - activeItem.presX = NumCast(targetDoc.x); - activeItem.presY = NumCast(targetDoc.y); - activeItem.presRot = NumCast(targetDoc.jitterRotation); - activeItem.presWidth = NumCast(targetDoc.width); - activeItem.presHeight = NumCast(targetDoc.height); }; @computed get recordingIsInOverlay() { @@ -446,16 +467,90 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { return width; } + @computed get presButtons() { + const presBox: Doc = this.presBox; //presBox + const presBoxColor: string = StrCast(presBox._backgroundColor); + const presColorBool: boolean = presBoxColor ? presBoxColor !== Colors.WHITE && presBoxColor !== 'transparent' : false; + const targetDoc: Doc = this.targetDoc; + const activeItem: Doc = this.rootDoc; + + const items: JSX.Element[] = []; + if (activeItem.presPinLayout) { + items.push( + <Tooltip key="slide" title={<div className="dash-tooltip">Update captured doc layout</div>}> + <div className="slideButton" onClick={() => this.updateCapturedContainerLayout(targetDoc, activeItem)} style={{ fontWeight: 700, display: 'flex' }}> + L + </div> + </Tooltip> + ); + } + if (activeItem.presPinData || activeItem.presPinView) { + items.push( + <Tooltip key="flex" title={<div className="dash-tooltip">Update captured doc content</div>}> + <div className="slideButton" onClick={() => this.updateCapturedViewContents(targetDoc, activeItem)} style={{ fontWeight: 700, display: 'flex' }}> + C + </div> + </Tooltip> + ); + } + if (!Doc.noviceMode) { + items.push( + <Tooltip key="slash" title={<div className="dash-tooltip">{this.recordingIsInOverlay ? 'Hide Recording' : `${PresElementBox.videoIsRecorded(activeItem) ? 'Show' : 'Start'} recording`}</div>}> + <div className="slideButton" onClick={e => (this.recordingIsInOverlay ? this.hideRecording(e, true) : this.startRecording(e, activeItem))} style={{ fontWeight: 700 }}> + <FontAwesomeIcon icon={`video${this.recordingIsInOverlay ? '-slash' : ''}`} onPointerDown={e => e.stopPropagation()} /> + </div> + </Tooltip> + ); + } + if (this.indexInPres === 0) { + items.push( + <Tooltip key="arrow" title={<div className="dash-tooltip">{activeItem.groupWithUp ? 'Ungroup' : 'Group with up'}</div>}> + <div + className="slideButton" + onClick={() => (activeItem.groupWithUp = !activeItem.groupWithUp)} + style={{ + zIndex: 1000 - this.indexInPres, + fontWeight: 700, + backgroundColor: activeItem.groupWithUp ? (presColorBool ? presBoxColor : Colors.MEDIUM_BLUE) : undefined, + height: activeItem.groupWithUp ? 53 : 18, + transform: activeItem.groupWithUp ? 'translate(0, -17px)' : undefined, + }}> + <div style={{ transform: activeItem.groupWithUp ? 'rotate(180deg) translate(0, -17.5px)' : 'rotate(0deg)' }}> + <FontAwesomeIcon icon={'arrow-up'} onPointerDown={e => e.stopPropagation()} /> + </div> + </div> + </Tooltip> + ); + } + items.push( + <Tooltip key="eye" title={<div className="dash-tooltip">{this.rootDoc.presExpandInlineButton ? 'Minimize' : 'Expand'}</div>}> + <div + className="slideButton" + onClick={e => { + e.stopPropagation(); + this.presExpandDocumentClick(); + }}> + <FontAwesomeIcon icon={this.rootDoc.presExpandInlineButton ? 'eye-slash' : 'eye'} onPointerDown={e => e.stopPropagation()} /> + </div> + </Tooltip> + ); + items.push( + <Tooltip key="trash" title={<div className="dash-tooltip">Remove from presentation</div>}> + <div className={'slideButton'} onClick={this.removeItem}> + <FontAwesomeIcon icon={'trash'} onPointerDown={e => e.stopPropagation()} /> + </div> + </Tooltip> + ); + return items; + } + @computed get mainItem() { const isSelected: boolean = this.selectedArray?.has(this.rootDoc) ? true : false; const isCurrent: boolean = this.presBox._itemIndex === this.indexInPres; - const toolbarWidth: number = this.toolbarWidth; - const showMore: boolean = this.toolbarWidth >= 300; const miniView: boolean = this.toolbarWidth <= 110; const presBox: Doc = this.presBox; //presBox const presBoxColor: string = StrCast(presBox._backgroundColor); const presColorBool: boolean = presBoxColor ? presBoxColor !== Colors.WHITE && presBoxColor !== 'transparent' : false; - const targetDoc: Doc = this.targetDoc; const activeItem: Doc = this.rootDoc; return ( @@ -466,6 +561,10 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { style={{ backgroundColor: presColorBool ? (isSelected ? 'rgba(250,250,250,0.3)' : 'transparent') : isSelected ? Colors.LIGHT_BLUE : 'transparent', opacity: this._dragging ? 0.3 : 1, + paddingLeft: NumCast(this.layoutDoc._xPadding, this.props.xPadding), + paddingRight: NumCast(this.layoutDoc._xPadding, this.props.xPadding), + paddingTop: NumCast(this.layoutDoc._yPadding, this.props.yPadding), + paddingBottom: NumCast(this.layoutDoc._yPadding, this.props.yPadding), }} onClick={e => { e.stopPropagation(); @@ -489,6 +588,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { ref={this._dragRef} className={`presItem-slide ${isCurrent ? 'active' : ''}`} style={{ + display: 'infline-block', backgroundColor: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor), //boxShadow: presBoxColor && presBoxColor !== 'white' && presBoxColor !== 'transparent' ? (isCurrent ? '0 0 0px 1.5px' + presBoxColor : undefined) : undefined, border: presBoxColor && presBoxColor !== 'white' && presBoxColor !== 'transparent' ? (isCurrent ? presBoxColor + ' solid 2.5px' : undefined) : undefined, @@ -496,8 +596,9 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="presItem-name" style={{ + display: 'inline-flex', pointerEvents: isSelected ? undefined : 'none', - maxWidth: showMore ? toolbarWidth - 195 : toolbarWidth - 105, + width: `calc(100% ${this.rootDoc.presExpandInlineButton ? '- 50%' : ''} - ${this.presButtons.length * 22}px`, cursor: isSelected ? 'text' : 'grab', }}> <div>{`${this.indexInPres + 1}. `}</div> @@ -505,49 +606,8 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> {/* <Tooltip title={<><div className="dash-tooltip">{"Movement speed"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.transition}</div></Tooltip> */} {/* <Tooltip title={<><div className="dash-tooltip">{"Duration"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.duration}</div></Tooltip> */} - <div className="presItem-slideButtons"> - <Tooltip title={<div className="dash-tooltip">Update view</div>}> - <div className="slideButton" onClick={() => this.updateView(targetDoc, activeItem)} style={{ fontWeight: 700, display: activeItem.presPinView ? 'flex' : 'none' }}> - V - </div> - </Tooltip> - {!Doc.noviceMode && <Tooltip title={<div className="dash-tooltip">{this.recordingIsInOverlay ? 'Hide Recording' : `${PresElementBox.videoIsRecorded(activeItem) ? 'Show' : 'Start'} recording`}</div>}> - <div className="slideButton" onClick={e => (this.recordingIsInOverlay ? this.hideRecording(e, true) : this.startRecording(e, activeItem))} style={{ fontWeight: 700 }}> - <FontAwesomeIcon icon={`video${this.recordingIsInOverlay ? '-slash' : ''}`} onPointerDown={e => e.stopPropagation()} /> - </div> - </Tooltip>} - <Tooltip title={<div className="dash-tooltip">{activeItem.groupWithUp ? 'Ungroup' : 'Group with up'}</div>}> - <div - className="slideButton" - onClick={() => (activeItem.groupWithUp = !activeItem.groupWithUp)} - style={{ - display: this.indexInPres === 0 ? 'none' : '', - zIndex: 1000 - this.indexInPres, - fontWeight: 700, - backgroundColor: activeItem.groupWithUp ? (presColorBool ? presBoxColor : Colors.MEDIUM_BLUE) : undefined, - height: activeItem.groupWithUp ? 53 : 18, - transform: activeItem.groupWithUp ? 'translate(0, -17px)' : undefined, - }}> - <div style={{ transform: activeItem.groupWithUp ? 'rotate(180deg) translate(0, -17.5px)' : 'rotate(0deg)' }}> - <FontAwesomeIcon icon={'arrow-up'} onPointerDown={e => e.stopPropagation()} /> - </div> - </div> - </Tooltip> - <Tooltip title={<div className="dash-tooltip">{this.rootDoc.presExpandInlineButton ? 'Minimize' : 'Expand'}</div>}> - <div - className="slideButton" - onClick={e => { - e.stopPropagation(); - this.presExpandDocumentClick(); - }}> - <FontAwesomeIcon icon={this.rootDoc.presExpandInlineButton ? 'eye-slash' : 'eye'} onPointerDown={e => e.stopPropagation()} /> - </div> - </Tooltip> - <Tooltip title={<div className="dash-tooltip">Remove from presentation</div>}> - <div className={'slideButton'} onClick={this.removeItem}> - <FontAwesomeIcon icon={'trash'} onPointerDown={e => e.stopPropagation()} /> - </div> - </Tooltip> + <div className="presItem-slideButtons" style={{ position: 'absolute', right: 0 }}> + {...this.presButtons} </div> {this.renderEmbeddedInline} </div> |
