diff options
Diffstat (limited to 'src/client/views/nodes')
38 files changed, 1137 insertions, 484 deletions
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index e28e5b453..787ad939d 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -84,12 +84,12 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @computed get miniPlayer() { return this.props.PanelHeight() < 50; } // used to collapse timeline when node is shrunk @computed get links() { return DocListCast(this.dataDoc.links); } @computed get pauseTime() { return this._pauseEnd - this._pauseStart; } // total time paused to update the correct recording time - @computed get mediaState() { return this.layoutDoc.mediaState as media_state; } + @computed get mediaState() { return this.dataDoc.mediaState as media_state; } @computed get path() { // returns the path of the audio file const path = Cast(this.props.Document[this.fieldKey], AudioField, null)?.url.href || ""; return path === nullAudio ? "" : path; } - set mediaState(value) { this.layoutDoc.mediaState = value; } + set mediaState(value) { this.dataDoc.mediaState = value; } @computed get timeline() { return this._stackedTimeline; } // returns CollectionStackedTimeline ref @@ -237,9 +237,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this.dataDoc[this.fieldKey + "-recordingStart"] = new DateField(); DocUtils.ActiveRecordings.push(this); this._recorder.ondataavailable = async (e: any) => { - console.log("Data available", e); const [{ result }] = await Networking.UploadFilesToServer(e.data); - console.log("Data result", result); if (!(result instanceof Error)) { this.props.Document[this.fieldKey] = new AudioField(result.accessPaths.agnostic.client); } @@ -350,13 +348,16 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp 2 * NumCast(this.layoutDoc._height) ); Doc.GetProto(newDoc).recordingSource = this.dataDoc; - Doc.GetProto(newDoc).recordingStart = ComputedField.MakeFunction( - `self.recordingSource["${this.fieldKey}-recordingStart"]` - ); - Doc.GetProto(newDoc).mediaState = ComputedField.MakeFunction( - "self.recordingSource.mediaState" - ); - this.props.addDocument?.(newDoc); + Doc.GetProto(newDoc).recordingStart = ComputedField.MakeFunction(`self.recordingSource["${this.fieldKey}-recordingStart"]`); + Doc.GetProto(newDoc).mediaState = ComputedField.MakeFunction("self.recordingSource.mediaState"); + const overlayDoc = Doc.UserDoc().myOverlayDocs as Doc; + if (DocListCast(overlayDoc[Doc.LayoutFieldKey(overlayDoc)]).includes(this.rootDoc)) { + newDoc.x = this.rootDoc.x; + newDoc.y = NumCast(this.rootDoc.y) + NumCast(this.rootDoc._height); + Doc.AddDocToList(overlayDoc, undefined, newDoc); + } else { + this.props.addDocument?.(newDoc); + } e.stopPropagation(); } @@ -611,7 +612,6 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp renderDepth={this.props.renderDepth + 1} startTag={"_timecodeToShow" /* audioStart */} endTag={"_timecodeToHide" /* audioEnd */} - focus={DocUtils.DefaultFocus} bringToFront={emptyFunction} CollectionView={undefined} playFrom={this.playFrom} @@ -654,7 +654,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp className="audiobox-container" onContextMenu={this.specificContextMenu} onClick={!this.path && !this._recorder ? this.recordAudioAnnotation : undefined} - style={{ pointerEvents: this.props.layerProvider?.(this.layoutDoc) === false ? "none" : undefined }} + style={{ pointerEvents: this.layoutDoc._lockedPosition ? "none" : undefined }} > {!this.path ? this.recordingControls : this.playbackControls} </div>; diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 3f16d3c49..5a0ab9110 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -21,7 +21,6 @@ import React = require("react"); export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { dataProvider?: (doc: Doc, replica: string) => { x: number, y: number, zIndex?: number, opacity?: number, highlight?: boolean, z: number, transition?: string } | undefined; sizeProvider?: (doc: Doc, replica: string) => { width: number, height: number } | undefined; - layerProvider: ((doc: Doc, assign?: boolean) => boolean) | undefined; renderCutoffProvider: (doc: Doc) => boolean; zIndex?: number; highlight?: boolean; @@ -38,7 +37,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF @observable _contentView: DocumentView | undefined | null; get displayName() { return "CollectionFreeFormDocumentView(" + this.rootDoc.title + ")"; } // this makes mobx trace() statements more descriptive get maskCentering() { return this.props.Document.isInkMask ? InkingStroke.MaskDim / 2 : 0; } - get transform() { return `translate(${this.X - this.maskCentering}px, ${this.Y - this.maskCentering}px) rotate(${this.props.jitterRotation}deg)`; } + get transform() { return `translate(${this.X - this.maskCentering}px, ${this.Y - this.maskCentering}px) rotate(${NumCast(this.Document.jitterRotation, this.props.jitterRotation)}deg)`; } get X() { return this.dataProvider ? this.dataProvider.x : NumCast(this.Document.x); } get Y() { return this.dataProvider ? this.dataProvider.y : NumCast(this.Document.y); } get ZInd() { return this.dataProvider ? this.dataProvider.zIndex : NumCast(this.Document.zIndex); } @@ -159,7 +158,6 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF ...this.props, CollectionFreeFormDocumentView: this.returnThis, styleProvider: this.styleProvider, - layerProvider: this.props.layerProvider, ScreenToLocalTransform: this.screenToLocalTransform, PanelWidth: this.panelWidth, PanelHeight: this.panelHeight, diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index 78608b681..5ea6d567a 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -3,7 +3,7 @@ import { action, observable } from 'mobx'; import { observer } from "mobx-react"; import { Doc, Opt } from '../../../fields/Doc'; import { Cast, NumCast, StrCast } from '../../../fields/Types'; -import { emptyFunction, OmitKeys, returnFalse, setupMoveUpEvents } from '../../../Utils'; +import { emptyFunction, OmitKeys, returnFalse, returnNone, setupMoveUpEvents } from '../../../Utils'; import { DragManager } from '../../util/DragManager'; import { SnappingManager } from '../../util/SnappingManager'; import { undoBatch } from '../../util/UndoManager'; @@ -76,7 +76,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl 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, `compareBox-${which}`)}> + onClick={e => this.clearDoc(e, which)}> <FontAwesomeIcon className={`clear-button ${which}`} icon={"times"} size="sm" /> </div>; }; @@ -87,7 +87,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl return whichDoc ? <> <DocumentView ref={(r) => { - whichDoc !== targetDoc && r?.focus(targetDoc); + whichDoc !== targetDoc && r?.focus(whichDoc); }} {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} isContentActive={returnFalse} @@ -96,7 +96,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl Document={targetDoc} DataDoc={undefined} hideLinkButton={true} - pointerEvents={"none"} /> + pointerEvents={returnNone} /> {clearButton(which)} </> : // placeholder image if doc is missing <div className="placeholder"> diff --git a/src/client/views/nodes/DocumentIcon.tsx b/src/client/views/nodes/DocumentIcon.tsx index a9c998757..56de2d1fc 100644 --- a/src/client/views/nodes/DocumentIcon.tsx +++ b/src/client/views/nodes/DocumentIcon.tsx @@ -52,7 +52,7 @@ export class DocumentIconContainer extends React.Component { }; }, getVars() { - const docs = DocumentManager.Instance.DocumentViews; + const docs = Array.from(DocumentManager.Instance.DocumentViews); const capturedVariables: { [name: string]: Field } = {}; usedDocuments.forEach(index => capturedVariables[`d${index}`] = docs[index].props.Document); return { capturedVariables }; @@ -60,6 +60,6 @@ export class DocumentIconContainer extends React.Component { }; } render() { - return DocumentManager.Instance.DocumentViews.map((dv, i) => <DocumentIcon key={i} index={i} view={dv} />); + return Array.from(DocumentManager.Instance.DocumentViews).map((dv, i) => <DocumentIcon key={i} index={i} view={dv} />); } }
\ No newline at end of file diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index 7f69adf6c..78d35ab99 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -17,7 +17,6 @@ import { DocumentView } from "./DocumentView"; import { LinkDescriptionPopup } from "./LinkDescriptionPopup"; import { TaskCompletionBox } from "./TaskCompletedBox"; import React = require("react"); -import { DocumentType } from "../../documents/DocumentTypes"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 8570f6fff..073f734c0 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -2,7 +2,7 @@ import { IconProp } from "@fortawesome/fontawesome-svg-core"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { AclAdmin, AclEdit, AclPrivate, DataSym, Doc, DocListCast, DocListCastAsync, Field, Opt, StrListCast, WidthSym } from "../../../fields/Doc"; +import { AclAdmin, AclEdit, AclPrivate, DataSym, Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, StrListCast, WidthSym } from "../../../fields/Doc"; import { Document } from '../../../fields/documentSchemas'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; @@ -14,7 +14,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, returnTrue, returnVal, simulateMouseClick, Utils } from "../../../Utils"; +import { emptyFunction, hasDescendantTarget, lightOrDark, OmitKeys, returnEmptyString, returnTrue, returnFalse, returnVal, simulateMouseClick, Utils } from "../../../Utils"; import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; import { Docs, DocUtils } from "../../documents/Documents"; import { DocumentType } from '../../documents/DocumentTypes'; @@ -37,7 +37,7 @@ import { DocComponent } from "../DocComponent"; import { EditableView } from '../EditableView'; import { InkingStroke } from "../InkingStroke"; import { LightboxView } from "../LightboxView"; -import { StyleLayers, StyleProp } from "../StyleProvider"; +import { StyleProp } from "../StyleProvider"; import { CollectionFreeFormDocumentView } from "./CollectionFreeFormDocumentView"; import { DocumentContentsView } from "./DocumentContentsView"; import { DocumentLinksButton } from './DocumentLinksButton'; @@ -50,6 +50,7 @@ import { ScriptingBox } from "./ScriptingBox"; import { PresBox } from './trails/PresBox'; import React = require("react"); import { DocServer } from "../../DocServer"; +import { FieldViewProps } from "./FieldView"; const { Howl } = require('howler'); interface Window { @@ -122,8 +123,9 @@ export interface DocumentViewSharedProps { PanelWidth: () => number; PanelHeight: () => number; docViewPath: () => DocumentView[]; + childHideDecorationTitle?: () => boolean; + childHideResizeHandles?: () => boolean; dataTransition?: string; // specifies animation transition - used by collectionPile and potentially other layout engines when changing the size of documents so that the change won't be abrupt - layerProvider: undefined | ((doc: Doc, assign?: boolean) => boolean); styleProvider: Opt<StyleProviderFunc>; focus: DocFocusFunc; fitWidth?: (doc: Doc) => boolean; @@ -148,7 +150,7 @@ export interface DocumentViewSharedProps { ignoreAutoHeight?: boolean; forceAutoHeight?: boolean; disableDocBrushing?: boolean; // should highlighting for this view be disabled when same document in another view is hovered over. - pointerEvents?: string; + pointerEvents?: () => Opt<string>; scriptContext?: any; // can be assigned anything and will be passed as 'scriptContext' to any OnClick script that executes on this document createNewFilterDoc?: () => void; updateFilterDoc?: (doc: Doc) => void; @@ -168,6 +170,7 @@ export interface DocumentViewProps extends DocumentViewSharedProps { radialMenu?: String[]; LayoutTemplateString?: string; dontCenter?: "x" | "y" | "xy"; + dontScaleFilter?: (doc: Doc) => boolean; // decides whether a document can be scaled to fit its container vs native size with scrolling ContentScaling?: () => number; // scaling the DocumentView does to transform its contents into its panel & needed by ScreenToLocal NativeWidth?: () => number; NativeHeight?: () => number; @@ -177,6 +180,8 @@ export interface DocumentViewProps extends DocumentViewSharedProps { onDoubleClick?: () => ScriptField; onPointerDown?: () => ScriptField; onPointerUp?: () => ScriptField; + onBrowseClick?: () => (ScriptField | undefined); + onKey?: (e: React.KeyboardEvent, fieldProps: FieldViewProps) => (boolean | undefined); } // these props are only available in DocumentViewIntenral @@ -232,12 +237,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @computed get finalLayoutKey() { return StrCast(this.Document.layoutKey, "layout"); } @computed get nativeWidth() { return this.props.NativeWidth(); } @computed get nativeHeight() { return this.props.NativeHeight(); } - @computed get onClickHandler() { return this.props.onClick?.() ?? Cast(this.Document.onClick, ScriptField, Cast(this.layoutDoc.onClick, ScriptField, null)); } + @computed get onClickHandler() { return this.props.onClick?.() ?? (this.props.onBrowseClick?.() ?? Cast(this.Document.onClick, ScriptField, Cast(this.layoutDoc.onClick, ScriptField, null))); } @computed get onDoubleClickHandler() { return this.props.onDoubleClick?.() ?? (Cast(this.layoutDoc.onDoubleClick, ScriptField, null) ?? this.Document.onDoubleClick); } @computed get onPointerDownHandler() { return this.props.onPointerDown?.() ?? ScriptCast(this.Document.onPointerDown); } @computed get onPointerUpHandler() { return this.props.onPointerUp?.() ?? ScriptCast(this.Document.onPointerUp); } - componentWillUnmount() { this.cleanupHandlers(true); } componentDidMount() { this.setupHandlers(); } //componentDidUpdate() { this.setupHandlers(); } @@ -249,6 +253,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps this._holdDisposer = InteractionUtils.MakeHoldTouchTarget(this._mainCont.current, this.handle1PointerHoldStart.bind(this)); } } + @action cleanupHandlers(unbrush: boolean) { this._dropDisposer?.(); this._multiTouchDisposer?.(); @@ -424,6 +429,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const dragData = new DragManager.DocumentDragData([this.props.Document]); const [left, top] = this.props.ScreenToLocalTransform().scale(this.ContentScale).inverse().transformPoint(0, 0); dragData.offset = this.props.ScreenToLocalTransform().scale(this.ContentScale).transformDirection(x - left, y - top); + dragData.offset[0] = Math.min(this.rootDoc[WidthSym](), dragData.offset[0]); + dragData.offset[1] = Math.min(this.rootDoc[HeightSym](), dragData.offset[1]); dragData.dropAction = dropAction; dragData.treeViewDoc = this.props.treeViewDoc; dragData.removeDocument = this.props.removeDocument; @@ -472,7 +479,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) { let stopPropagate = true; let preventDefault = true; - !StrListCast(this.props.Document._layerTags).includes(StyleLayers.Background) && (this.rootDoc._raiseWhenDragged === undefined ? Doc.UserDoc()._raiseWhenDragged : this.rootDoc._raiseWhenDragged) && this.props.bringToFront(this.rootDoc); + const isScriptBox = () => StrCast(Doc.LayoutField(this.layoutDoc))?.includes(ScriptingBox.name); + (this.rootDoc._raiseWhenDragged === undefined ? Doc.UserDoc()._raiseWhenDragged : this.rootDoc._raiseWhenDragged) && this.props.bringToFront(this.rootDoc); if (this._doubleTap && (this.props.Document.type !== DocumentType.FONTICON || this.onDoubleClickHandler)) {// && !this.onClickHandler?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click if (this._timeout) { clearTimeout(this._timeout); @@ -492,12 +500,12 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps UndoManager.RunInBatch(() => func().result?.select === true ? this.props.select(false) : "", "on double click"); } else if (!Doc.IsSystem(this.rootDoc)) { UndoManager.RunInBatch(() => - LightboxView.AddDocTab(this.rootDoc, "lightbox", this.props.LayoutTemplate?.()) + LightboxView.AddDocTab(this.rootDoc, "lightbox", this.props.LayoutTemplate?.(), this.props.addDocTab) , "double tap"); SelectionManager.DeselectAll(); Doc.UnBrushDoc(this.props.Document); } - } else if (this.onClickHandler?.script && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes(ScriptingBox.name)) { // bcz: hack? don't execute script if you're clicking on a scripting box itself + } 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 func = () => this.onClickHandler.script.run({ this: this.layoutDoc, @@ -513,7 +521,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps runInAction(() => this._pendingDoubleClick = true); this._timeout = setTimeout(() => { this._timeout = undefined; clickFunc(); }, 350); } else clickFunc(); - } else if (this.allLinks && this.Document.type !== DocumentType.LINK && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) { + } else if (this.allLinks && this.Document.type !== DocumentType.LINK && !isScriptBox() && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) { this.allLinks.length && LinkManager.FollowLink(undefined, this.props.Document, this.props, e.altKey); } else { if ((this.layoutDoc.onDragStart || this.props.Document.rootDocument) && !(e.ctrlKey || e.button > 0)) { // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTemplaetForField implies we're clicking on part of a template instance and we want to select the whole template, not the part @@ -531,8 +539,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps }); onPointerDown = (e: React.PointerEvent): void => { + if (this.rootDoc.type === DocumentType.INK && CurrentUserUtils.SelectedTool === InkTool.Eraser) return; // continue if the event hasn't been canceled AND we are using a mouse or this has an onClick or onDragStart function (meaning it is a button document) - if (!(InteractionUtils.IsType(e, InteractionUtils.MOUSETYPE) || [InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.SelectedTool))) { + if (!(InteractionUtils.IsType(e, InteractionUtils.MOUSETYPE) || [InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(CurrentUserUtils.SelectedTool))) { if (!InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) { e.stopPropagation(); if (SelectionManager.IsSelected(this.props.DocumentView(), true) && this.props.Document._viewType !== CollectionViewType.Docking) e.preventDefault(); // goldenlayout needs to be able to move its tabs, so can't preventDefault for it @@ -546,6 +555,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps // if this is part of a template, let the event go up to the tempalte root unless right/ctrl clicking !(this.props.Document.rootDocument && !(e.ctrlKey || e.button > 0))) { if ((this.props.isDocumentActive?.() || this.layoutDoc.onDragStart) && + !this.props.onBrowseClick?.() && !this.Document.ignoreClick && !e.ctrlKey && (e.button === 0 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) && @@ -565,7 +575,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps onPointerMove = (e: PointerEvent): void => { if (e.cancelBubble) return; - if ((InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || [InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.SelectedTool))) return; + if ((InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || [InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(CurrentUserUtils.SelectedTool))) return; if ((this.props.isDocumentActive?.() || this.layoutDoc.onDragStart) && !this.layoutDoc._lockedPosition && !CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) { if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) { @@ -599,7 +609,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } @undoBatch @action - toggleFollowLink = (location: Opt<string>, zoom: boolean, setPushpin: boolean): void => { + toggleFollowLink = (location: Opt<string>, zoom?: boolean, setPushpin?: boolean): void => { this.Document.ignoreClick = false; if (setPushpin) { this.Document.isPushpin = !this.Document.isPushpin; @@ -608,7 +618,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps this.Document._isLinkButton = !this.Document._isLinkButton; } if (this.Document._isLinkButton && !this.onClickHandler) { - this.Document.followLinkZoom = zoom; + zoom !== undefined && (this.Document.followLinkZoom = zoom); this.Document.followLinkLocation = location; } else if (this.Document._isLinkButton && this.onClickHandler) { this.Document._isLinkButton = false; @@ -663,7 +673,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } if (de.complete.annoDragData || this.rootDoc !== linkdrag.linkSourceDoc.context) { const dropDoc = de.complete.annoDragData?.dropDocument ?? this._componentView?.getAnchor?.() ?? this.props.Document; - de.complete.linkDocument = DocUtils.MakeLink({ doc: linkdrag.linkSourceDoc }, { doc: dropDoc }, undefined, undefined, undefined, undefined, [de.x, de.y]); + de.complete.linkDocument = DocUtils.MakeLink({ doc: linkdrag.linkSourceDoc }, { doc: dropDoc }, undefined, undefined, undefined, undefined, [de.x, de.y - 50]); } } } @@ -739,7 +749,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const alias = Doc.MakeAlias(this.rootDoc); alias.layout = FormattedTextBox.LayoutString("newfield"); alias.title = "newfield"; - alias._yMargin = 10; alias._height = 35; alias._width = 100; alias.syncLayoutFieldWithTitle = true; @@ -856,8 +865,12 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps this.props.isContentActive()) ? true : undefined; } @observable _retryThumb = 1; - thumbShown = () => !this.props.isSelected() && LightboxView.LightboxDoc !== this.rootDoc && this.thumb && - !this._componentView?.isAnyChildContentActive?.() ? true : false + thumbShown = () => { + return !this.props.isSelected() && LightboxView.LightboxDoc !== this.rootDoc && this.thumb && + !Doc.AreProtosEqual(DocumentLinksButton.StartLink, this.rootDoc) && + !Doc.isBrushedHighlightedDegree(this.props.Document) && + !this._componentView?.isAnyChildContentActive?.() ? true : false; + } @computed get contents() { TraceMobx(); const audioView = !this.layoutDoc._showAudio ? (null) : @@ -869,11 +882,14 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps return <div className="documentView-contentsView" style={{ - pointerEvents: this.props.pointerEvents as any ?? this.rootDoc.layoutKey === "layout_icon" ? "none" : (this.rootDoc.type !== DocumentType.INK && ((this.props.contentPointerEvents as any) || (this.isContentActive())) ? "all" : "none"), + pointerEvents: this.props.pointerEvents?.() as any ?? this.rootDoc.layoutKey === "layout_icon" ? "none" : + this.props.contentPointerEvents as any ? this.props.contentPointerEvents as any : + this.rootDoc.type !== DocumentType.INK && this.isContentActive() ? "all" : + "none", height: this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined, }}> {!this._retryThumb || !this.thumbShown() ? (null) : - <img style={{ background: "white", top: 0, position: "absolute" }} src={this.thumb} // + '?d=' + (new Date()).getTime()} + <img style={{ background: "white", top: 0, position: "relative" }} src={this.thumb} // + '?d=' + (new Date()).getTime()} width={this.props.PanelWidth()} height={this.props.PanelHeight()} onError={(e: any) => { setTimeout(action(() => this._retryThumb = 0), 0); @@ -945,6 +961,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps return filtered.map((link, i) => <div className="documentView-anchorCont" key={link[Id]}> <DocumentView {...this.props} + isContentActive={returnFalse} Document={link} PanelWidth={this.anchorPanelWidth} PanelHeight={this.anchorPanelHeight} @@ -1107,10 +1124,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } render() { TraceMobx(); - const highlightIndex = this.props.LayoutTemplateString ? (Doc.IsHighlighted(this.props.Document) ? 6 : 0) : 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"][highlightIndex]; - const highlightStyle = ["solid", "dashed", "solid", "solid"][highlightIndex]; - const excludeTypes = !this.props.treeViewDoc && highlightIndex < 3 ? [DocumentType.FONTICON, DocumentType.INK] : [DocumentType.FONTICON]; + 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 @@ -1125,8 +1142,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps onKeyDown={this.onKeyDown} onPointerDown={this.onPointerDown} onClick={this.onClick} - onPointerEnter={e => !SnappingManager.GetIsDragging() && Doc.BrushDoc(this.props.Document)} - onPointerLeave={e => !hasDescendantTarget(e.nativeEvent.x, e.nativeEvent.y, this.ContentDiv) && Doc.UnBrushDoc(this.props.Document)} + 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))} style={{ display: this.hidden ? "inline" : undefined, borderRadius: this.borderRounding, @@ -1206,7 +1223,7 @@ export class DocumentView extends React.Component<DocumentViewProps> { } @computed get shouldNotScale() { return (this.fitWidth && !this.nativeWidth) || - this.props.ContainingCollectionView?.collectionViewType === CollectionViewType.Time || + this.props.dontScaleFilter?.(this.Document) || this.props.treeViewDoc || [CollectionViewType.Docking].includes(this.Document._viewType as any); } @computed get effectiveNativeWidth() { return this.shouldNotScale ? 0 : (this.nativeWidth || NumCast(this.layoutDoc.width)); } @@ -1235,7 +1252,7 @@ export class DocumentView extends React.Component<DocumentViewProps> { toggleNativeDimensions = () => this.docView && Doc.toggleNativeDimensions(this.layoutDoc, this.docView.ContentScale, this.props.PanelWidth(), this.props.PanelHeight()); focus = (doc: Doc, options?: DocFocusOptions) => this.docView?.focus(doc, options); getBounds = () => { - if (!this.docView || !this.docView.ContentDiv || this.docView.props.renderDepth === 0 || this.docView.props.treeViewDoc || Doc.AreProtosEqual(this.props.Document, Doc.UserDoc())) { + if (!this.docView || !this.docView.ContentDiv || this.props.Document.presBox || this.docView.props.treeViewDoc || Doc.AreProtosEqual(this.props.Document, Doc.UserDoc())) { return undefined; } const xf = (this.docView?.props.ScreenToLocalTransform().scale(this.nativeScaling)).inverse(); @@ -1290,7 +1307,9 @@ export class DocumentView extends React.Component<DocumentViewProps> { ContentScale = () => this.nativeScaling; selfView = () => this; screenToLocalTransform = () => { - return this.props.ScreenToLocalTransform().translate(-this.centeringX, -this.centeringY).scale(1 / this.nativeScaling); + const oshift = this.fitWidth && this.ComponentView instanceof FormattedTextBox; + const shift = oshift ? -(this.props.PanelHeight() - this.rootDoc[HeightSym]()) / 2 : 0; + return this.props.ScreenToLocalTransform().translate(-this.centeringX, -this.centeringY).translate(0, shift).scale(1 / this.nativeScaling); } componentDidMount() { this._disposers.reactionScript = reaction( @@ -1324,6 +1343,7 @@ export class DocumentView extends React.Component<DocumentViewProps> { transition: this.props.dataTransition, position: this.props.Document.isInkMask ? "absolute" : undefined, transform: isButton ? undefined : `translate(${this.centeringX}px, ${this.centeringY}px)`, + margin: this.fitWidth ? "auto" : undefined, width: isButton || isPresTreeElement ? "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()}%`), diff --git a/src/client/views/nodes/EquationBox.scss b/src/client/views/nodes/EquationBox.scss index 6c9d53d10..c6a497831 100644 --- a/src/client/views/nodes/EquationBox.scss +++ b/src/client/views/nodes/EquationBox.scss @@ -1,3 +1,5 @@ +@import "../global/globalCssVariables.scss"; + .equationBox-cont { transform-origin: top left; }
\ No newline at end of file diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index ae9acfe3f..79c1f1c40 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -2,10 +2,11 @@ import React = require("react"); import { computed } from "mobx"; import { observer } from "mobx-react"; import { DateField } from "../../../fields/DateField"; -import { Doc, Field, FieldResult } from "../../../fields/Doc"; +import { Doc, Field, FieldResult, Opt } from "../../../fields/Doc"; import { List } from "../../../fields/List"; import { WebField } from "../../../fields/URLField"; -import { DocumentViewSharedProps } from "./DocumentView"; +import { DocumentView, DocumentViewSharedProps } from "./DocumentView"; +import { ScriptField } from "../../../fields/ScriptField"; // // these properties get assigned through the render() method of the DocumentView when it creates this node. @@ -23,9 +24,11 @@ export interface FieldViewProps extends DocumentViewSharedProps { isSelected: (outsideReaction?: boolean) => boolean; scaling?: () => number; setHeight?: (height: number) => void; + onBrowseClick?: () => (ScriptField | undefined); + onKey?: (e: React.KeyboardEvent, fieldProps: FieldViewProps) => (boolean | undefined); // properties intended to be used from within layout strings (otherwise use the function equivalents that work more efficiently with React) - pointerEvents?: string; + pointerEvents?: () => Opt<string>; fontSize?: number; height?: number; width?: number; diff --git a/src/client/views/nodes/FilterBox.tsx b/src/client/views/nodes/FilterBox.tsx index ba65acee0..28834a202 100644 --- a/src/client/views/nodes/FilterBox.tsx +++ b/src/client/views/nodes/FilterBox.tsx @@ -96,17 +96,15 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() { runInAction(() => this._loaded = true); }, { fireImmediately: true }); } - @observable _allDocs = [] as Doc[]; + @computed get allDocs() { // trace(); + const allDocs = new Set<Doc>(); const targetDoc = FilterBox.targetDoc; if (this._loaded && targetDoc) { - const allDocs = new Set<Doc>(); - const activeTabs = FilterBox.targetDocChildren; - SearchBox.foreachRecursiveDoc(activeTabs, (depth, doc) => allDocs.add(doc)); - setTimeout(action(() => this._allDocs = Array.from(allDocs))); + SearchBox.foreachRecursiveDoc(FilterBox.targetDocChildren, (depth, doc) => allDocs.add(doc)); } - return this._allDocs; + return Array.from(allDocs); } @computed get _allFacets() { @@ -208,7 +206,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() { facetValues.strings.map(val => { const num = val ? Number(val) : Number.NaN; if (Number.isNaN(num)) { - nonNumbers++; + num && nonNumbers++; } else { minVal = Math.min(num, minVal); maxVal = Math.max(num, maxVal); @@ -218,8 +216,9 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() { if (facetHeader === "text" || facetValues.rtFields / allCollectionDocs.length > 0.1) { newFacet = Docs.Create.TextDocument("", { title: facetHeader, system: true, target: targetDoc, _width: 100, _height: 25, _stayInCollection: true, - treeViewExpandedView: "layout", _treeViewOpen: true, _forceActive: true, ignoreClick: true + treeViewExpandedView: "layout", _treeViewOpen: true, _forceActive: true, ignoreClick: true, }); + Doc.GetProto(newFacet).forceActive = true; // required for FormattedTextBox to be able to gain focus since it will never be 'selected' Doc.GetProto(newFacet).type = DocumentType.COL; // forces item to show an open/close button instead ofa checkbox newFacet._textBoxPaddingX = newFacet._textBoxPaddingY = 4; const scriptText = `setDocFilter(this?.target, "${facetHeader}", text, "match")`; @@ -240,6 +239,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() { Doc.GetProto(newFacet)[newFacetField + "-maxThumb"] = extendedMaxVal; const scriptText = `setDocRangeFilter(this?.target, "${facetHeader}", range)`; newFacet.onThumbChanged = ScriptField.MakeScript(scriptText, { this: Doc.name, range: "number" }); + newFacet.data = ComputedField.MakeFunction(`readNumFacetData(self.target, self, "${FilterBox.targetDocChildKey}", "${facetHeader}")`); } else { newFacet = new Doc(); newFacet.system = true; @@ -386,6 +386,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() { options={options} isMulti={false} onChange={val => this.facetClick((val as UserOptions).value)} + onKeyDown={e => e.stopPropagation()} value={null} closeMenuOnSelect={true} /> @@ -402,6 +403,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() { docFilters={returnEmptyFilter} docRangeFilters={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} + childDocumentsActive={returnTrue} ContainingCollectionDoc={this.props.ContainingCollectionDoc} ContainingCollectionView={this.props.ContainingCollectionView} PanelWidth={this.props.PanelWidth} @@ -420,10 +422,10 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() { whenChildContentsActiveChanged={returnFalse} treeViewHideTitle={true} focus={returnFalse} + onCheckedClick={this.scriptField} treeViewHideHeaderFields={false} dontRegisterView={true} styleProvider={this.FilterStyleProvider} - layerProvider={this.props.layerProvider} docViewPath={this.props.docViewPath} scriptContext={this.props.scriptContext} moveDocument={returnFalse} @@ -480,6 +482,32 @@ ScriptingGlobals.add(function determineCheckedState(layoutDoc: Doc, facetHeader: } return undefined; }); +ScriptingGlobals.add(function readNumFacetData(layoutDoc: Doc, facetDoc: Doc, childKey: string, facetHeader: string) { + const allCollectionDocs = new Set<Doc>(); + const activeTabs = DocListCast(layoutDoc[childKey]); + SearchBox.foreachRecursiveDoc(activeTabs, (depth: number, doc: Doc) => allCollectionDocs.add(doc)); + const set = new Set<string>(); + if (facetHeader === "tags") allCollectionDocs.forEach(child => Field.toString(child[facetHeader] as Field).split(":").forEach(key => set.add(key))); + else allCollectionDocs.forEach(child => set.add(Field.toString(child[facetHeader] as Field))); + const facetValues = Array.from(set).filter(v => v); + + let minVal = Number.MAX_VALUE, maxVal = -Number.MAX_VALUE; + facetValues.map(val => { + const num = val ? Number(val) : Number.NaN; + if (!Number.isNaN(num)) { + minVal = Math.min(num, minVal); + maxVal = Math.max(num, maxVal); + } + }); + const newFacetField = Doc.LayoutFieldKey(facetDoc); + const ranged = Doc.readDocRangeFilter(layoutDoc, facetHeader); + const extendedMinVal = minVal - Math.min(1, Math.floor(Math.abs(maxVal - minVal) * .1)); + const extendedMaxVal = Math.max(minVal + 1, maxVal + Math.min(1, Math.ceil(Math.abs(maxVal - minVal) * .05))); + facetDoc[newFacetField + "-min"] = ranged === undefined ? extendedMinVal : ranged[0]; + facetDoc[newFacetField + "-max"] = ranged === undefined ? extendedMaxVal : ranged[1]; + Doc.GetProto(facetDoc)[newFacetField + "-minThumb"] = extendedMinVal; + Doc.GetProto(facetDoc)[newFacetField + "-maxThumb"] = extendedMaxVal; +}) ScriptingGlobals.add(function readFacetData(layoutDoc: Doc, childKey: string, facetHeader: string) { const allCollectionDocs = new Set<Doc>(); const activeTabs = DocListCast(layoutDoc[childKey]); diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index d061cdd0b..ab4ed6b33 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,7 +1,7 @@ import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from 'mobx'; import { observer } from "mobx-react"; import { extname } from 'path'; -import { DataSym, Doc, DocListCast, WidthSym } from '../../../fields/Doc'; +import { DataSym, Doc, DocListCast, Opt, WidthSym } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; @@ -49,6 +49,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ImageBox, fieldKey); } private _dropDisposer?: DragManager.DragDropDisposer; private _disposers: { [name: string]: IReactionDisposer } = {}; + private _getAnchor: (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => Opt<Doc> = () => undefined; @observable _curSuffix = ""; @observable _uploadIcon = uploadIcons.idle; @@ -62,7 +63,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp getAnchor = () => { - const anchor = AnchorMenu.Instance?.GetAnchor(this._savedAnnotations); + const anchor = this._getAnchor?.(this._savedAnnotations); anchor && this.addDocument(anchor); return anchor ?? this.rootDoc; } @@ -356,7 +357,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp return <div className="imageBox-annotationLayer" style={{ height: this.props.PanelHeight() }} ref={this._annotationLayer} />; } marqueeDown = (e: React.PointerEvent) => { - if (!e.altKey && e.button === 0 && this.layoutDoc._viewScale === 1 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.SelectedTool)) { + if (!e.altKey && e.button === 0 && this.layoutDoc._viewScale === 1 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(CurrentUserUtils.SelectedTool)) { setupMoveUpEvents(this, e, action(e => { MarqueeAnnotator.clearAnnotations(this._savedAnnotations); this._marqueeing = [e.clientX, e.clientY]; @@ -366,9 +367,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } @action finishMarquee = () => { + this._getAnchor = AnchorMenu.Instance?.GetAnchor; this._marqueeing = undefined; this.props.select(false); } + savedAnnotations = () => this._savedAnnotations; render() { TraceMobx(); @@ -378,7 +381,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp style={{ width: this.props.PanelWidth() ? undefined : `100%`, height: this.props.PanelWidth() ? undefined : `100%`, - pointerEvents: this.props.layerProvider?.(this.layoutDoc) === false ? "none" : undefined, + pointerEvents: this.layoutDoc._lockedPosition ? "none" : undefined, borderRadius }} > <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} @@ -408,7 +411,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp docView={this.props.docViewPath().slice(-1)[0]} addDocument={this.addDocument} finishMarquee={this.finishMarquee} - savedAnnotations={this._savedAnnotations} + savedAnnotations={this.savedAnnotations} annotationLayer={this._annotationLayer.current} mainCont={this._mainCont.current} anchorMenuCrop={this.crop} diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index 881cbf2bb..80def3025 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -60,7 +60,6 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { docRangeFilters: returnEmptyFilter, searchFilterDocs: returnEmptyDoclist, styleProvider: DefaultStyleProvider, - layerProvider: undefined, docViewPath: returnEmptyDoclist, ContainingCollectionView: undefined, ContainingCollectionDoc: undefined, diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx index d0d61fd79..b0b050cea 100644 --- a/src/client/views/nodes/LabelBox.tsx +++ b/src/client/views/nodes/LabelBox.tsx @@ -23,8 +23,8 @@ export interface LabelBoxProps { @observer export class LabelBox extends ViewBoxBaseComponent<(FieldViewProps & LabelBoxProps)>() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(LabelBox, fieldKey); } - public static LayoutStringWithTitle(fieldType: { name: string }, fieldStr: string, label: string) { - return `<${fieldType.name} fieldKey={'${fieldStr}'} label={'${label}'} {...props} />`; //e.g., "<ImageBox {...props} fieldKey={"data} />" + public static LayoutStringWithTitle(fieldStr: string, label?: string) { + return !label ? LabelBox.LayoutString(fieldStr) : `<LabelBox fieldKey={'${fieldStr}'} label={'${label}'} {...props} />`; //e.g., "<ImageBox {...props} fieldKey={"data} />" } private dropDisposer?: DragManager.DragDropDisposer; private _timeout: any; @@ -79,17 +79,18 @@ export class LabelBox extends ViewBoxBaseComponent<(FieldViewProps & LabelBoxPro @computed get hoverColor() { return this._mouseOver ? StrCast(this.layoutDoc._hoverBackgroundColor) : "unset"; } fitTextToBox = (r: any): any => { + const singleLine = BoolCast(this.rootDoc._singleLine, true); const params = { rotateText: null, fontSizeFactor: 1, - minimumFontSize: NumCast(this.rootDoc._minFontSize, 2), + minimumFontSize: NumCast(this.rootDoc._minFontSize, 8), maximumFontSize: NumCast(this.rootDoc._maxFontSize, 1000), limitingDimension: "both", horizontalAlign: "center", verticalAlign: "center", textAlign: "center", - singleLine: BoolCast(this.rootDoc._singleLine), - whiteSpace: this.rootDoc._singleLine ? "nowrap" : "pre-wrap" + singleLine, + whiteSpace: singleLine ? "nowrap" : "pre-wrap" }; this._timeout = undefined; if (!r) return params; @@ -99,15 +100,16 @@ export class LabelBox extends ViewBoxBaseComponent<(FieldViewProps & LabelBoxPro parentStyle.display = ""; parentStyle.alignItems = ""; r.setAttribute("style", ""); - r.style.width = this.rootDoc._singleLine ? "" : "100%"; + r.style.width = singleLine ? "" : "100%"; r.style.textOverflow = "ellipsis"; r.style.overflow = "hidden"; BigText(r, params); + return params; } // (!missingParams || !missingParams.length ? "" : "(" + missingParams.map(m => m + ":").join(" ") + ")") render() { - this.fitTextToBox(null);// this causes mobx to trigger re-render when data changes + const boxParams = this.fitTextToBox(null);// this causes mobx to trigger re-render when data changes const params = Cast(this.paramsDoc["onClick-paramFieldKeys"], listSpec("string"), []); const missingParams = params?.filter(p => !this.paramsDoc[p]); params?.map(p => DocListCast(this.paramsDoc[p])); // bcz: really hacky form of prefetching ... @@ -130,9 +132,9 @@ export class LabelBox extends ViewBoxBaseComponent<(FieldViewProps & LabelBoxPro paddingBottom: NumCast(this.rootDoc._yPadding), width: this.props.PanelWidth(), height: this.props.PanelHeight(), - whiteSpace: this.rootDoc._singleLine ? "pre" : "pre-wrap" + whiteSpace: boxParams.singleLine ? "pre" : "pre-wrap" }} > - <span style={{ width: this.layoutDoc._singleLine ? "" : "100%" }} ref={action((r: any) => this.fitTextToBox(r))}> + <span style={{ width: boxParams.singleLine ? "" : "100%" }} ref={action((r: any) => this.fitTextToBox(r))}> {label.startsWith("#") ? (null) : label.replace(/([^a-zA-Z])/g, "$1\u200b")} </span> </div> diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx index 437d29f39..7fd289a97 100644 --- a/src/client/views/nodes/LinkAnchorBox.tsx +++ b/src/client/views/nodes/LinkAnchorBox.tsx @@ -89,7 +89,6 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() { openLinkTargetOnRight = (e: React.MouseEvent) => { const alias = Doc.MakeAlias(Cast(this.layoutDoc[this.fieldKey], Doc, null)); alias._isLinkButton = undefined; - alias._layerTags = undefined; alias.layoutKey = "layout"; this.props.addDocTab(alias, "add:right"); } diff --git a/src/client/views/nodes/LinkDescriptionPopup.tsx b/src/client/views/nodes/LinkDescriptionPopup.tsx index a9d33f161..ccac66996 100644 --- a/src/client/views/nodes/LinkDescriptionPopup.tsx +++ b/src/client/views/nodes/LinkDescriptionPopup.tsx @@ -54,6 +54,7 @@ export class LinkDescriptionPopup extends React.Component<{}> { top: LinkDescriptionPopup.popupY ? LinkDescriptionPopup.popupY : 350, }}> <input className="linkDescriptionPopup-input" + onKeyDown={e => e.stopPropagation()} onKeyPress={e => e.key === "Enter" && this.onDismiss(true)} placeholder={"(Optional) Enter link description..."} onChange={(e) => this.descriptionChanged(e)}> diff --git a/src/client/views/nodes/LinkDocPreview.scss b/src/client/views/nodes/LinkDocPreview.scss index 06ae466f0..3febbcecb 100644 --- a/src/client/views/nodes/LinkDocPreview.scss +++ b/src/client/views/nodes/LinkDocPreview.scss @@ -1,4 +1,4 @@ - .linkDocPreview { +.linkDocPreview { position: absolute; pointer-events: all; background-color: lightblue; @@ -8,11 +8,14 @@ border-bottom: 8px solid white; border-right: 8px solid white; z-index: 2004; + .linkDocPreview-inner { background-color: white; width: 100%; height: 100%; pointer-events: none; + display: flex; + flex-direction: column; .linkDocPreview-info { height: 37px; @@ -21,6 +24,7 @@ .linkDocPreview-buttonBar { float: right; } + .linkDocPreview-title { padding-right: 4px; float: left; diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx index 375434933..152288d8b 100644 --- a/src/client/views/nodes/LinkDocPreview.tsx +++ b/src/client/views/nodes/LinkDocPreview.tsx @@ -93,7 +93,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { } else { this._linkSrc = anchor; const linkTarget = LinkManager.getOppositeAnchor(this._linkDoc, this._linkSrc); - this._targetDoc = linkTarget?.type === DocumentType.MARKER && linkTarget?.annotationOn ? Cast(linkTarget.annotationOn, Doc, null) ?? linkTarget : linkTarget; + this._targetDoc = /*linkTarget?.type === DocumentType.MARKER &&*/ linkTarget?.annotationOn ? Cast(linkTarget.annotationOn, Doc, null) ?? linkTarget : linkTarget; } this._toolTipText = ""; } @@ -108,8 +108,11 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { } nextHref = (e: React.PointerEvent) => { setupMoveUpEvents(this, e, returnFalse, emptyFunction, action(() => { - this._linkDoc = undefined; - this._hrefInd = (this._hrefInd + 1) % (this.props.hrefs?.length || 1); + const nextHrefInd = (this._hrefInd + 1) % (this.props.hrefs?.length || 1); + if (nextHrefInd !== this._hrefInd) { + this._linkDoc = undefined; + this._hrefInd = nextHrefInd; + } }), true); } @@ -179,7 +182,6 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { moveDocument={returnFalse} rootSelected={returnFalse} styleProvider={this.props.docProps?.styleProvider} - layerProvider={this.props.docProps?.layerProvider} docViewPath={returnEmptyDoclist} ScreenToLocalTransform={Transform.Identity} isDocumentActive={returnFalse} diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx index 59e39e051..9f1c019fe 100644 --- a/src/client/views/nodes/MapBox/MapBox.tsx +++ b/src/client/views/nodes/MapBox/MapBox.tsx @@ -471,7 +471,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @action onMarqueeDown = (e: React.PointerEvent) => { - if (!e.altKey && e.button === 0 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.SelectedTool)) { + if (!e.altKey && e.button === 0 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(CurrentUserUtils.SelectedTool)) { setupMoveUpEvents(this, e, action(e => { MarqueeAnnotator.clearAnnotations(this._savedAnnotations); this._marqueeing = [e.clientX, e.clientY]; @@ -489,13 +489,17 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps return this.addDocument(doc, annotationKey); } + pointerEvents = () => { + return this.props.isContentActive() && this.props.pointerEvents?.() !== "none" && !MarqueeOptionsMenu.Instance.isShown() ? + "all" : + SnappingManager.GetIsDragging() ? + undefined : "none"; + } @computed get annotationLayer() { - const pe = this.props.isContentActive() && this.props.pointerEvents !== "none" && !MarqueeOptionsMenu.Instance.isShown() ? "all" : - SnappingManager.GetIsDragging() ? undefined : "none"; return <div className="mapBox-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer}> {this.inlineTextAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map(anno => <Annotation key={`${anno[Id]}-annotation`} {...this.props} - fieldKey={this.annotationKey} pointerEvents={pe} showInfo={this.showInfo} dataDoc={this.dataDoc} anno={anno} />)} + fieldKey={this.annotationKey} pointerEvents={this.pointerEvents} showInfo={this.showInfo} dataDoc={this.dataDoc} anno={anno} />)} </div>; } @@ -537,7 +541,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps infoWidth = () => this.props.PanelWidth() / 5; infoHeight = () => this.props.PanelHeight() / 5; anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick; - + savedAnnotations = () => this._savedAnnotations; render() { const renderAnnotations = (docFilters?: () => string[]) => (null); // bcz: commmented this out. Otherwise, any documents that are rendered with an InfoWindow of a marker @@ -618,7 +622,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps addDocument={this.addDocumentWrapper} docView={this.props.docViewPath().lastElement()} finishMarquee={this.finishMarquee} - savedAnnotations={this._savedAnnotations} + savedAnnotations={this.savedAnnotations} annotationLayer={this._annotationLayer.current} mainCont={this._mainCont.current} />} </div> diff --git a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx index 0d5fedb7b..db7dcb09d 100644 --- a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx +++ b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx @@ -4,7 +4,7 @@ import { observer } from "mobx-react"; import * as React from "react"; import { Doc } from '../../../../fields/Doc'; import { Id } from '../../../../fields/FieldSymbols'; -import { emptyFunction, OmitKeys, returnEmptyFilter, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents } from '../../../../Utils'; +import { emptyFunction, OmitKeys, returnAll, returnEmptyFilter, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents } from '../../../../Utils'; import { Docs } from '../../../documents/Documents'; import { DocumentType } from '../../../documents/DocumentTypes'; import { CollectionStackingView } from '../../collections/CollectionStackingView'; @@ -79,7 +79,7 @@ export class MapBoxInfoWindow extends React.Component<MapBoxInfoWindowProps & Vi addDocument={(doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((p, d) => p && Doc.AddDocToList(this.props.place, "data", d), true as boolean)} renderDepth={this.props.renderDepth + 1} viewType={CollectionViewType.Stacking} - pointerEvents="all" + pointerEvents={returnAll} /> </div> <hr /> diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 69662d53a..f2ca6c96e 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -2,28 +2,30 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; import { observer } from "mobx-react"; import * as Pdfjs from "pdfjs-dist"; -import { CreateImage } from "../nodes/WebBoxRenderer"; import "pdfjs-dist/web/pdf_viewer.css"; import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../fields/Doc"; -import { Cast, NumCast, StrCast, ImageCast } from '../../../fields/Types'; +import { Id } from '../../../fields/FieldSymbols'; +import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types'; import { ImageField, PdfField } from "../../../fields/URLField"; import { TraceMobx } from '../../../fields/util'; import { emptyFunction, returnOne, setupMoveUpEvents, Utils } from '../../../Utils'; -import { Docs } from '../../documents/Documents'; +import { Docs, DocUtils } from '../../documents/Documents'; +import { DocumentType } from '../../documents/DocumentTypes'; import { KeyCodes } from '../../util/KeyCodes'; import { undoBatch, UndoManager } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from "../DocComponent"; import { Colors } from '../global/globalEnums'; +import { CreateImage } from "../nodes/WebBoxRenderer"; import { AnchorMenu } from '../pdf/AnchorMenu'; import { PDFViewer } from "../pdf/PDFViewer"; import { SidebarAnnos } from '../SidebarAnnos'; import { FieldView, FieldViewProps } from './FieldView'; +import { ImageBox } from './ImageBox'; import "./PDFBox.scss"; -import React = require("react"); -import { Id } from '../../../fields/FieldSymbols'; import { VideoBox } from './VideoBox'; +import React = require("react"); @observer export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() { @@ -61,10 +63,16 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps this.replaceCanvases(oldDiv.childNodes[i] as HTMLElement, newDiv.childNodes[i] as HTMLElement); } } + + if (oldDiv.className === "pdfBox-ui" || + oldDiv.className === "pdfViewerDash-overlay-inking") { + newDiv.style.display = "none"; + } + if (newDiv && newDiv.style) newDiv.style.overflow = "hidden"; if (oldDiv instanceof HTMLCanvasElement) { const canvas = oldDiv; const img = document.createElement('img'); // create a Image Element - img.src = canvas.toDataURL(); //image source + img.src = canvas.toDataURL(); //image sourcez img.style.width = canvas.style.width; img.style.height = canvas.style.height; const newCan = newDiv as HTMLCanvasElement; @@ -74,7 +82,72 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } } + crop = (region: Doc | undefined, addCrop?: boolean) => { + if (!region) return; + const cropping = Doc.MakeCopy(region, true); + Doc.GetProto(region).lockedPosition = true; + Doc.GetProto(region).title = "region:" + this.rootDoc.title; + Doc.GetProto(region).isPushpin = true; + this.addDocument(region); + + const docViewContent = this.props.docViewPath().lastElement().ContentDiv!; + const newDiv = docViewContent.cloneNode(true) as HTMLDivElement; + newDiv.style.width = (this.layoutDoc[WidthSym]()).toString(); + newDiv.style.height = (this.layoutDoc[HeightSym]()).toString(); + this.replaceCanvases(docViewContent, newDiv); + const htmlString = this._pdfViewer?._mainCont.current && new XMLSerializer().serializeToString(newDiv); + + const anchx = NumCast(cropping.x); + const anchy = NumCast(cropping.y); + const anchw = cropping[WidthSym]() * (this.props.scaling?.() || 1); + const anchh = cropping[HeightSym]() * (this.props.scaling?.() || 1); + const viewScale = 1; + cropping.title = "crop: " + this.rootDoc.title; + cropping.x = NumCast(this.rootDoc.x) + NumCast(this.rootDoc._width); + cropping.y = NumCast(this.rootDoc.y); + cropping._width = anchw; + cropping._height = anchh; + cropping.isLinkButton = undefined; + const croppingProto = Doc.GetProto(cropping); + croppingProto.annotationOn = undefined; + croppingProto.isPrototype = true; + croppingProto.proto = Cast(this.rootDoc.proto, Doc, null)?.proto; // set proto of cropping's data doc to be IMAGE_PROTO + croppingProto.type = DocumentType.IMG; + croppingProto.layout = ImageBox.LayoutString("data"); + croppingProto.data = new ImageField(Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")); + croppingProto["data-nativeWidth"] = anchw; + croppingProto["data-nativeHeight"] = anchh; + if (addCrop) { + DocUtils.MakeLink({ doc: region }, { doc: cropping }, "cropped image", ""); + } + this.props.bringToFront(cropping); + + CreateImage( + "", + document.styleSheets, + htmlString, + anchw, + anchh, + NumCast(region.y) * this.props.PanelWidth() / NumCast(this.rootDoc[this.fieldKey + "-nativeWidth"]), + NumCast(region.x) * this.props.PanelWidth() / NumCast(this.rootDoc[this.fieldKey + "-nativeWidth"]), + 4 + ).then + ((data_url: any) => { + VideoBox.convertDataUri(data_url, region[Id]).then( + returnedfilename => setTimeout(action(() => { + croppingProto.data = new ImageField(returnedfilename); + }), 500)); + }) + .catch(function (error: any) { + console.error('oops, something went wrong!', error); + }); + + + return cropping; + } + updateIcon = () => { + return; // currently we render pdf icons as text labels const docViewContent = this.props.docViewPath().lastElement().ContentDiv!; const newDiv = docViewContent.cloneNode(true) as HTMLDivElement; newDiv.style.width = (this.layoutDoc[WidthSym]()).toString(); @@ -125,7 +198,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } getAnchor = () => { const anchor = - AnchorMenu.Instance?.GetAnchor() ?? + this._pdfViewer?._getAnchor(this._pdfViewer.savedAnnotations()) ?? Docs.Create.TextanchorDocument({ title: StrCast(this.rootDoc.title + "@" + NumCast(this.layoutDoc._scrollTop)?.toFixed(0)), y: NumCast(this.layoutDoc._scrollTop), @@ -241,13 +314,16 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps </>; const searchTitle = `${!this._searching ? "Open" : "Close"} Search Bar`; const curPage = NumCast(this.Document._curPage) || 1; - return !this.props.isContentActive() ? (null) : + return !this.props.isContentActive() || this._pdfViewer?.isAnnotating ? (null) : <div className="pdfBox-ui" onKeyDown={e => [KeyCodes.BACKSPACE, KeyCodes.DELETE].includes(e.keyCode) ? e.stopPropagation() : true} onPointerDown={e => e.stopPropagation()} style={{ display: this.props.isContentActive() ? "flex" : "none" }}> <div className="pdfBox-overlayCont" onPointerDown={(e) => e.stopPropagation()} style={{ left: `${this._searching ? 0 : 100}%` }}> <button className="pdfBox-overlayButton" title={searchTitle} /> <input className="pdfBox-searchBar" placeholder="Search" ref={this._searchRef} onChange={this.searchStringChanged} - onKeyDown={e => e.keyCode === KeyCodes.ENTER && this.search(this._searchString, e.shiftKey)} /> + onKeyDown={e => { + e.stopPropagation(); + e.keyCode === KeyCodes.ENTER && this.search(this._searchString, e.shiftKey); + }} /> <button className="pdfBox-search" title="Search" onClick={e => this.search(this._searchString, e.shiftKey)}> <FontAwesomeIcon icon="search" size="sm" /> </button> @@ -269,6 +345,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps <div className="pdfBox-pageNums"> <input value={curPage} style={{ width: `${curPage > 99 ? 4 : 3}ch`, pointerEvents: "all" }} onChange={e => this.Document._curPage = Number(e.currentTarget.value)} + onKeyDown={e => e.stopPropagation()} onClick={action(() => this._pageControls = !this._pageControls)} /> {this._pageControls ? pageBtns : (null)} </div> @@ -327,7 +404,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps transform: `scale(${scale})`, position: "absolute", transformOrigin: "top left", - top: 0 + top: 0, }}> <PDFViewer {...this.props} rootDoc={this.rootDoc} @@ -343,7 +420,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps moveDocument={this.moveDocument} removeDocument={this.removeDocument} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} - startupLive={true} + crop={this.crop} ContentScaling={returnOne} /> </div> @@ -385,4 +462,5 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } return this.renderTitleBox; } -}
\ No newline at end of file +} + diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss index b97e90bd9..c7108d290 100644 --- a/src/client/views/nodes/VideoBox.scss +++ b/src/client/views/nodes/VideoBox.scss @@ -15,6 +15,7 @@ width: 100%; height: 100%; position: relative; + .videoBox-viewer { display: flex; flex-direction: column; @@ -23,6 +24,7 @@ opacity: 0.99; // hack! overcomes some kind of Chrome weirdness where buttons (e.g., snapshot) disappear at some point as the video is resized larger background: $dark-gray; } + .inkingCanvas-paths-markers { opacity: 0.4; // we shouldn't have to do this, but since chrome crawls to a halt with z-index unset in videoBox-content, this is a workaround } @@ -52,17 +54,20 @@ width: 100%; z-index: -1; // 0; // logically this should be 0 (or unset) which would give us transparent brush strokes over videos. However, this makes Chrome crawl to a halt position: absolute; + video { width: auto; - height: 100%; - display: flex; + height: 100%; + display: flex; margin: auto; } } -.videoBox-content, .videoBox-content-interactive, .videoBox-content-fullScreen { +.videoBox-content, +.videoBox-content-interactive, +.videoBox-content-fullScreen { width: 100%; - height: 100%; + height: 100%; left: 0px; } @@ -148,6 +153,7 @@ pointer-events: all; padding-right: 5px; cursor: pointer; + &:hover { background-color: $medium-gray; } diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index a98cd8cf4..5fe1ad5ed 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -153,6 +153,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // plays video @action public Play = (update: boolean = true) => { + if (this._playRegionTimer) return; this._playing = true; const eleTime = this.player?.currentTime || 0; if (this.timeline) { @@ -204,7 +205,10 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this._youtubePlayer && SelectionManager.DeselectAll(); // if we don't deselect the player, then we get an annoying YouTube spinner I guess telling us we're paused. this._playTimer = undefined; this.updateTimecode(); - if (!this._finished) clearTimeout(this._playRegionTimer); // if paused in the middle of playback, prevents restart on next play + if (!this._finished) { + clearTimeout(this._playRegionTimer); // if paused in the middle of playback, prevents restart on next play + } + this._playRegionTimer = undefined; } // toggles video full screen @@ -339,7 +343,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this.layoutDoc._height = NumCast(this.layoutDoc._width) / aspect; if (Number.isFinite(this.player!.duration)) { this.rawDuration = this.player!.duration; - } + } else this.rawDuration = NumCast(this.dataDoc[this.fieldKey + "-duration"]); }); @@ -584,6 +588,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @action playFrom = (seekTimeInSeconds: number, endTime?: number, fullPlay: boolean = false) => { clearTimeout(this._playRegionTimer); + this._playRegionTimer = undefined; if (Number.isNaN(this.player?.duration)) { setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500); } @@ -810,7 +815,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp onPointerDown={(e) => { e.stopPropagation(); this.toggleMute(); }}> <FontAwesomeIcon icon={this._muted ? "volume-mute" : "volume-up"} /> </div> - <input type="range" step="0.1" min="0" max="1" value={this._muted ? 0 : this._volume} + <input type="range" style={{ width: `min(25%, 50px)` }} step="0.1" min="0" max="1" value={this._muted ? 0 : this._volume} className="toolbar-slider volume" onPointerDown={(e: React.PointerEvent) => e.stopPropagation()} onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setVolume(Number(e.target.value))} @@ -856,7 +861,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp playLink={this.playLink} PanelHeight={this.timelineHeight} rawDuration={this.rawDuration} - vref={this._videoRef} /> </div>; } @@ -866,12 +870,13 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp return <div className="videoBox-annotationLayer" style={{ transition: this.transition, height: `${this.heightPercent}%` }} ref={this._annotationLayer} />; } + savedAnnotations = () => this._savedAnnotations; render() { const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding); const borderRadius = borderRad?.includes("px") ? `${Number(borderRad.split("px")[0]) / this.scaling()}px` : borderRad; return (<div className="videoBox" onContextMenu={this.specificContextMenu} ref={this._mainCont} style={{ - pointerEvents: this.props.layerProvider?.(this.layoutDoc) === false ? "none" : undefined, + pointerEvents: this.layoutDoc._lockedPosition ? "none" : undefined, borderRadius, overflow: this.props.docViewPath?.().slice(-1)[0].fitWidth ? "auto" : undefined }} onWheel={e => { e.stopPropagation(); e.preventDefault(); }}> @@ -913,7 +918,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp containerOffset={this.marqueeOffset} addDocument={this.addDocWithTimecode} finishMarquee={this.finishMarquee} - savedAnnotations={this._savedAnnotations} + savedAnnotations={this.savedAnnotations} annotationLayer={this._annotationLayer.current} mainCont={this._mainCont.current} />} diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss index ff38e37dd..d8dd074a5 100644 --- a/src/client/views/nodes/WebBox.scss +++ b/src/client/views/nodes/WebBox.scss @@ -5,17 +5,18 @@ height: 100%; position: relative; display: flex; - .webBox-sideResizer { + + .webBox-sideResizer { position: absolute; width: 100%; height: 100%; cursor: ew-resize; background: darkgray; } - .webBox-background { + + .webBox-background { width: 100%; height: 100%; - background: lightGray; } .webBox-ui { @@ -120,7 +121,7 @@ box-shadow: $standard-box-shadow; transition: 0.2s; - &:hover{ + &:hover { filter: brightness(0.85); } } @@ -155,11 +156,15 @@ position: absolute; top: 0; left: 0; + cursor: text; + padding: 15px; + height: 100% } .webBox-cont { pointer-events: none; } + .webBox-cont, .webBox-cont-interactive { padding: 0vw; @@ -193,6 +198,7 @@ top: 0; left: 0; overflow: auto; + .webBox-innerContent { position: relative; } diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 5c72417cc..10974ca03 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -15,7 +15,6 @@ import { TraceMobx } from "../../../fields/util"; import { emptyFunction, getWordAtPoint, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, smoothScroll, Utils } from "../../../Utils"; import { Docs } from "../../documents/Documents"; import { CurrentUserUtils } from "../../util/CurrentUserUtils"; -import { KeyCodes } from "../../util/KeyCodes"; import { ScriptingGlobals } from "../../util/ScriptingGlobals"; import { SnappingManager } from "../../util/SnappingManager"; import { undoBatch } from "../../util/UndoManager"; @@ -41,7 +40,6 @@ import React = require("react"); const { CreateImage } = require("./WebBoxRenderer"); const _global = (window /* browser */ || global /* node */) as any; const htmlToText = require("html-to-text"); - @observer export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(WebBox, fieldKey); } @@ -53,7 +51,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps private _disposers: { [name: string]: IReactionDisposer } = {}; private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); private _keyInput = React.createRef<HTMLInputElement>(); - private _initialScroll: Opt<number>; + private _initialScroll: Opt<number> = NumCast(this.layoutDoc.thumbScrollTop, NumCast(this.layoutDoc.scrollTop)); + private _getAnchor: (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => Opt<Doc> = () => undefined; private _sidebarRef = React.createRef<SidebarAnnos>(); private _searchRef = React.createRef<HTMLInputElement>(); private _searchString = ""; @@ -69,19 +68,22 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @observable private _iframeClick: HTMLIFrameElement | undefined = undefined; @observable private _iframe: HTMLIFrameElement | null = null; @observable private _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); - @observable private _scrollHeight = NumCast(this.layoutDoc.scrollHeight, 1500); + @observable private _scrollHeight = NumCast(this.layoutDoc.scrollHeight); @computed get _url() { return this.webField?.toString() || ""; } @computed get _urlHash() { return this._url ? WebBox.urlHash(this._url) + "" : ""; } - @computed get scrollHeight() { return this._scrollHeight; } + @computed get scrollHeight() { return Math.max(this.layoutDoc[HeightSym](), this._scrollHeight); } @computed get allAnnotations() { return DocListCast(this.dataDoc[this.annotationKey]); } @computed get inlineTextAnnotations() { return this.allAnnotations.filter(a => a.textInlineAnnotations); } @computed get webField() { return Cast(this.dataDoc[this.props.fieldKey], WebField)?.url; } - @computed get webThumb() { return ImageCast(this.layoutDoc["thumb-frozen"], ImageCast(this.layoutDoc.thumb))?.url; } + @computed get webThumb() { + return this.props.thumbShown?.() && + ImageCast(this.layoutDoc["thumb-frozen"], + ImageCast(this.layoutDoc.thumbScrollTop === this.layoutDoc._scrollTop && this.layoutDoc.thumbNativeWidth === NumCast(this.layoutDoc.nativeWidth) && + this.layoutDoc.thumbNativeHeight === NumCast(this.layoutDoc.nativeHeight) ? this.layoutDoc.thumb : undefined))?.url; + } constructor(props: any) { super(props); - Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(this.dataDoc) || 850); - Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(this.dataDoc) || this.Document[HeightSym]() / this.Document[WidthSym]() * 850); runInAction(() => this._webUrl = this._url); // setting the weburl will change the src parameter of the embedded iframe and force a navigation to it. } @@ -103,14 +105,52 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } return true; } + @action setScrollPos = (pos: number) => { if (!this._outerRef.current || this._outerRef.current.scrollHeight < pos) { - setTimeout(() => this.setScrollPos(pos), 250); + if (this._webPageHasBeenRendered) setTimeout(() => this.setScrollPos(pos), 250); } else { this._outerRef.current.scrollTop = pos; this._initialScroll = undefined; } } + + lockout = false; + updateThumb = async () => { + const imageBitmap = ImageCast(this.layoutDoc["thumb-frozen"])?.url.href; + const scrollTop = NumCast(this.layoutDoc._scrollTop); + const nativeWidth = NumCast(this.layoutDoc.nativeWidth); + const nativeHeight = nativeWidth * this.props.PanelHeight() / this.props.PanelWidth(); + if (!this.lockout && this._iframe && !imageBitmap && (scrollTop !== this.layoutDoc.thumbScrollTop || nativeWidth !== this.layoutDoc.thumbNativeWidth || nativeHeight !== this.layoutDoc.thumbNativeHeight)) { + var htmlString = this._iframe.contentDocument && new XMLSerializer().serializeToString(this._iframe.contentDocument); + if (!htmlString) { + htmlString = await (await fetch(Utils.CorsProxy(this.webField!.href))).text(); + } + this.layoutDoc.thumb = undefined; + this.lockout = 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) => { + VideoBox.convertDataUri(data_url, this.layoutDoc[Id] + "-icon" + (new Date()).getTime(), true, this.layoutDoc[Id] + "-icon").then( + returnedfilename => setTimeout(action(() => { + this.lockout = false; + this.layoutDoc.thumb = new ImageField(returnedfilename); + this.layoutDoc.thumbScrollTop = scrollTop; + this.layoutDoc.thumbNativeWidth = nativeWidth; + this.layoutDoc.thumbNativeHeight = nativeHeight; + }), 500)); + }) + .catch(function (error: any) { + console.error('oops, something went wrong!', error); + }); + } + } async componentDidMount() { this.props.setContentView?.(this); // this tells the DocumentView that this WebBox is the "content" of the document. this allows the DocumentView to call WebBox relevant methods to configure the UI (eg, show back/forward buttons) @@ -120,45 +160,16 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps this.dataDoc[this.fieldKey + "-annotations"] = ComputedField.MakeFunction(`copyField(this["${this.fieldKey}-"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"-annotations"`); this.dataDoc[this.fieldKey + "-sidebar"] = ComputedField.MakeFunction(`copyField(this["${this.fieldKey}-"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"-sidebar"`); }); - reaction(() => this.props.isSelected(), + reaction(() => this.props.isSelected() || this.isAnyChildContentActive() || Doc.isBrushedHighlightedDegree(this.props.Document), async (selected) => { if (selected) { this._webPageHasBeenRendered = true; - setTimeout(action(() => { - this._scrollHeight = Math.max(this.scrollHeight, this._iframe?.contentDocument?.body.scrollHeight || 0); - if (this._initialScroll !== undefined) { - this.setScrollPos(this._initialScroll); - } - })); } else if ((!this.props.isContentActive() || SnappingManager.GetIsDragging()) && // update thumnail when unselected AND (no child annotation is active OR we've started dragging the document in which case no additional deselect will occur so this is the only chance to update the thumbnail) !this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick && // don't create a thumbnail when double-clicking to enter lightbox because thumbnail will be empty LightboxView.LightboxDoc !== this.rootDoc) { // don't create a thumbnail if entering Lightbox from maximize either, since thumb will be empty. - const imageBitmap = ImageCast(this.layoutDoc["thumb-frozen"])?.url.href; - if (this._iframe && !imageBitmap) { - var htmlString = this._iframe.contentDocument && new XMLSerializer().serializeToString(this._iframe.contentDocument); - if (!htmlString) { - htmlString = await (await fetch(Utils.CorsProxy(this.webField!.href))).text(); - } - this.layoutDoc.thumb = undefined; - const nativeWidth = NumCast(this.layoutDoc.nativeWidth); - CreateImage( - this._webUrl.endsWith("/") ? this._webUrl.substring(0, this._webUrl.length - 1) : this._webUrl, - this._iframe.contentDocument?.styleSheets ?? [], - htmlString, - nativeWidth, - nativeWidth * this.props.PanelHeight() / this.props.PanelWidth(), - NumCast(this.layoutDoc._scrollTop) - ).then - ((data_url: any) => { - VideoBox.convertDataUri(data_url, this.layoutDoc[Id] + "-icon" + (new Date()).getTime(), true, this.layoutDoc[Id] + "-icon").then( - returnedfilename => setTimeout(action(() => this.layoutDoc.thumb = new ImageField(returnedfilename)), 500)); - }) - .catch(function (error: any) { - console.error('oops, something went wrong!', error); - }); - } + this.updateThumb(); } - }, { fireImmediately: this.props.isSelected() }); + }, { fireImmediately: this.props.isSelected() || this.isAnyChildContentActive() || (Doc.isBrushedHighlightedDegree(this.props.Document) ? true : false) }); this._disposers.autoHeight = reaction(() => this.layoutDoc._autoHeight, autoHeight => { @@ -186,21 +197,16 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } } - var quickScroll = true; this._disposers.scrollReaction = reaction(() => NumCast(this.layoutDoc._scrollTop), (scrollTop) => { - if (quickScroll) this._initialScroll = scrollTop; - else { - const viewTrans = StrCast(this.Document._viewTransition); - const durationMiliStr = viewTrans.match(/([0-9]*)ms/); - const durationSecStr = viewTrans.match(/([0-9.]*)s/); - const duration = durationMiliStr ? Number(durationMiliStr[1]) : durationSecStr ? Number(durationSecStr[1]) * 1000 : 0; - this.goTo(scrollTop, duration); - } + const viewTrans = StrCast(this.Document._viewTransition); + const durationMiliStr = viewTrans.match(/([0-9]*)ms/); + const durationSecStr = viewTrans.match(/([0-9.]*)s/); + const duration = durationMiliStr ? Number(durationMiliStr[1]) : durationSecStr ? Number(durationSecStr[1]) * 1000 : 0; + this.goTo(scrollTop, duration); }, { fireImmediately: true } ); - quickScroll = false; } @action componentWillUnmount() { Object.values(this._disposers).forEach(disposer => disposer?.()); @@ -209,8 +215,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } @action - createTextAnnotation = (sel: Selection, selRange: Range) => { - if (this._mainCont.current) { + createTextAnnotation = (sel: Selection, selRange: Range | undefined) => { + if (this._mainCont.current && selRange) { const clientRects = selRange.getClientRects(); for (let i = 0; i < clientRects.length; i++) { const rect = clientRects.item(i); @@ -231,6 +237,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps // clear selection if (sel.empty) sel.empty();// Chrome else if (sel.removeAllRanges) sel.removeAllRanges(); // Firefox + return this._savedAnnotations; } menuControls = () => this.urlEditor; // controls to be added to the top bar when a document of this type is selected @@ -243,21 +250,22 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps if (this._sidebarRef?.current?.makeDocUnfiltered(doc)) return 1; if (doc !== this.rootDoc && this._outerRef.current) { const windowHeight = this.props.PanelHeight() / (this.props.scaling?.() || 1); - const scrollTo = Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.layoutDoc._scrollTop), windowHeight, windowHeight * .1, this.getScrollHeight()); - if (scrollTo !== undefined) { + const scrollTo = Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.layoutDoc._scrollTop), windowHeight, windowHeight * .1, + Math.max(NumCast(doc.y) + doc[HeightSym](), this.getScrollHeight())); + if (scrollTo !== undefined && this._initialScroll === undefined) { const focusSpeed = smooth ? 500 : 0; - this._initialScroll !== undefined && (this._initialScroll = scrollTo); this.goTo(scrollTo, focusSpeed); return focusSpeed; + } else if (!this._webPageHasBeenRendered || !this.getScrollHeight() || this._initialScroll !== undefined) { + this._initialScroll = scrollTo; } } - this._initialScroll = NumCast(this.layoutDoc._scrollTop); - return 0; + return undefined; } getAnchor = () => { const anchor = - AnchorMenu.Instance?.GetAnchor(this._savedAnnotations) ?? + this._getAnchor(this._savedAnnotations) ?? Docs.Create.WebanchorDocument(this._url, { title: StrCast(this.rootDoc.title + " " + this.layoutDoc._scrollTop), y: NumCast(this.layoutDoc._scrollTop), @@ -267,15 +275,19 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps return anchor; } + _textAnnotationCreator: (() => ObservableMap<number, HTMLDivElement[]>) | undefined; + savedAnnotationsCreator: (() => ObservableMap<number, HTMLDivElement[]>) = () => this._textAnnotationCreator?.() || this._savedAnnotations; + @action iframeUp = (e: PointerEvent) => { + this._textAnnotationCreator = undefined; this.props.docViewPath().lastElement()?.docView?.cleanupPointerEvents(); // pointerup events aren't generated on containing document view, so we have to invoke it here. if (this._iframe?.contentWindow && this._iframe.contentDocument && !this._iframe.contentWindow.getSelection()?.isCollapsed) { const mainContBounds = Utils.GetScreenTransform(this._mainCont.current!); const scale = (this.props.scaling?.() || 1) * mainContBounds.scale; const sel = this._iframe.contentWindow.getSelection(); if (sel) { - this.createTextAnnotation(sel, sel.getRangeAt(0)); + this._textAnnotationCreator = () => this.createTextAnnotation(sel, !sel.isCollapsed ? sel.getRangeAt(0) : undefined); AnchorMenu.Instance.jumpTo(e.clientX * scale + mainContBounds.translateX, e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._scrollTop) * scale); } @@ -283,6 +295,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } @action iframeDown = (e: PointerEvent) => { + const sel = this._iframe?.contentWindow?.getSelection?.(); const mainContBounds = Utils.GetScreenTransform(this._mainCont.current!); const scale = (this.props.scaling?.() || 1) * mainContBounds.scale; const word = getWordAtPoint(e.target, e.clientX, e.clientY); @@ -320,6 +333,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @action iframeLoaded = (e: any) => { const iframe = this._iframe; + if (this._initialScroll !== undefined) { + this.setScrollPos(this._initialScroll); + } let requrlraw = decodeURIComponent(iframe?.contentWindow?.location.href.replace(Utils.prepend("") + "/corsProxy/", "") ?? this._url.toString()); if (requrlraw !== this._url.toString()) { if (requrlraw.match(/q=.*&/)?.length && this._url.toString().match(/q=.*&/)?.length) { @@ -338,8 +354,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps if (iframe?.contentDocument) { iframe.contentDocument.addEventListener("pointerup", this.iframeUp); iframe.contentDocument.addEventListener("pointerdown", this.iframeDown); - this._scrollHeight = Math.max(this.scrollHeight, iframe?.contentDocument.body.scrollHeight); - setTimeout(action(() => this._scrollHeight = Math.max(this.scrollHeight, iframe?.contentDocument?.body.scrollHeight || 0)), 5000); + this._scrollHeight = Math.max(this._scrollHeight, iframe?.contentDocument.body.scrollHeight); + setTimeout(action(() => this._scrollHeight = Math.max(this._scrollHeight, iframe?.contentDocument?.body.scrollHeight || 0)), 5000); iframe.setAttribute("enable-annotation", "true"); iframe.contentDocument.addEventListener("click", undoBatch(action((e: MouseEvent) => { let href = ""; @@ -370,29 +386,32 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @action setDashScrollTop = (scrollTop: number, timeout: number = 250) => { - const iframeHeight = Math.max(1000, this._scrollHeight - this.panelHeight()); - timeout = scrollTop > iframeHeight ? 0 : timeout; + const iframeHeight = Math.max(scrollTop, this._scrollHeight - this.panelHeight()); this._scrollTimer && clearTimeout(this._scrollTimer); this._scrollTimer = setTimeout(action(() => { this._scrollTimer = undefined; - if (!LinkDocPreview.LinkInfo && this._outerRef.current && + const newScrollTop = scrollTop > iframeHeight ? iframeHeight : scrollTop; + if (!LinkDocPreview.LinkInfo && this._outerRef.current && newScrollTop !== this.layoutDoc.thumbScrollTop && (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath()))) { - this.layoutDoc._scrollTop = this._outerRef.current.scrollTop = scrollTop > iframeHeight ? iframeHeight : scrollTop; - } + this.layoutDoc.thumb = undefined; + this.layoutDoc.thumbScrollTop = undefined; + this.layoutDoc.thumbNativeWidth = undefined; + this.layoutDoc.thumbNativeHeight = undefined; + this.layoutDoc.scrollTop = this._outerRef.current.scrollTop = newScrollTop; + } else if (this._outerRef.current) this._outerRef.current.scrollTop = newScrollTop; }), timeout); } goTo = (scrollTop: number, duration: number) => { if (this._outerRef.current) { - const iframeHeight = Math.max(1000, this._scrollHeight - this.panelHeight()); - scrollTop = scrollTop > iframeHeight + 50 ? iframeHeight : scrollTop; + const iframeHeight = Math.max(scrollTop, this._scrollHeight - this.panelHeight()); if (duration) { smoothScroll(duration, [this._outerRef.current], scrollTop); this.setDashScrollTop(scrollTop, duration); } else { this.setDashScrollTop(scrollTop); } - } + } else this._initialScroll = scrollTop; } forward = (checkAvailable?: boolean) => { @@ -452,6 +471,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps if (url && !preview) { this.dataDoc[this.fieldKey + "-history"] = new List<string>([...(history || []), url]); this.layoutDoc._scrollTop = 0; + if (this._webPageHasBeenRendered) { + this.layoutDoc.thumb = undefined; + this.layoutDoc.thumbScrollTop = undefined; + this.layoutDoc.thumbNativeWidth = undefined; + this.layoutDoc.thumbNativeHeight = undefined; + } future && (future.length = 0); } if (!preview) { @@ -534,7 +559,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @action onMarqueeDown = (e: React.PointerEvent) => { - if (!e.altKey && e.button === 0 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.SelectedTool)) { + if (!e.altKey && e.button === 0 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(CurrentUserUtils.SelectedTool)) { setupMoveUpEvents(this, e, action(e => { MarqueeAnnotator.clearAnnotations(this._savedAnnotations); this._marqueeing = [e.clientX, e.clientY]; @@ -543,9 +568,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } } @action finishMarquee = (x?: number, y?: number, e?: PointerEvent) => { + this._getAnchor = AnchorMenu.Instance?.GetAnchor; this._marqueeing = undefined; this._isAnnotating = false; this._iframeClick = undefined; + const sel = this._iframe?.contentDocument?.getSelection(); + if (sel?.empty) sel.empty();// Chrome + else if (sel?.removeAllRanges) sel.removeAllRanges(); // Firefox if (x !== undefined && y !== undefined) { this._setPreviewCursor?.(x, y, false, false); ContextMenu.Instance.closeMenu(); @@ -559,10 +588,11 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @computed get urlContent() { if (this._hackHide || (this.webThumb && (!this._webPageHasBeenRendered && LightboxView.LightboxDoc !== this.rootDoc))) return (null); + this.props.thumbShown?.(); const field = this.dataDoc[this.props.fieldKey]; let view; if (field instanceof HtmlField) { - view = <span className="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: field.html }} />; + view = <span className="webBox-htmlSpan" contentEditable onPointerDown={e => e.stopPropagation()} dangerouslySetInnerHTML={{ __html: field.html }} />; } else if (field instanceof WebField) { const url = this.layoutDoc.useCors ? Utils.CorsProxy(this._webUrl) : this._webUrl; view = <iframe className="webBox-iframe" enable-annotation={"true"} @@ -576,7 +606,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps style={{ pointerEvents: this._scrollTimer ? "none" : undefined }} // if we allow pointer events when scrolling is on, then reversing direction does not work smoothly ref={action((r: HTMLIFrameElement | null) => this._iframe = r)} src={"https://crossorigin.me/https://cs.brown.edu"} />; } - setTimeout(action(() => this._webPageHasBeenRendered = true)); + setTimeout(action(() => { + this._scrollHeight = Math.max(this._scrollHeight, this._iframe && this._iframe.contentDocument && this._iframe.contentDocument.body ? this._iframe.contentDocument.body.scrollHeight : 0); + if (this._initialScroll === undefined && !this._webPageHasBeenRendered) { + this.setScrollPos(NumCast(this.layoutDoc.thumbScrollTop, NumCast(this.layoutDoc.scrollTop))); + } + this._webPageHasBeenRendered = true; + })); return view; } @@ -610,7 +646,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @observable _previewNativeWidth: Opt<number> = undefined; @observable _previewWidth: Opt<number> = undefined; toggleSidebar = action((preview: boolean = false) => { - const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]); + var nativeWidth = NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]); + if (!nativeWidth) { + const defaultNativeWidth = this.dataDoc[this.fieldKey] instanceof WebField ? 850 : this.Document[WidthSym](); + Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(this.dataDoc) || defaultNativeWidth); + Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(this.dataDoc) || this.Document[HeightSym]() / this.Document[WidthSym]() * defaultNativeWidth); + nativeWidth = NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]); + } const sideratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? WebBox.openSidebarWidth : 0) + nativeWidth) / nativeWidth; const pdfratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? WebBox.openSidebarWidth + WebBox.sidebarResizerWidth : 0) + nativeWidth) / nativeWidth; const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth); @@ -620,9 +662,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps this._showSidebar = true; } else { - this.layoutDoc.nativeWidth = nativeWidth * pdfratio; + this.layoutDoc._showSidebar = !this.layoutDoc._showSidebar; this.layoutDoc._width = this.layoutDoc[WidthSym]() * nativeWidth * pdfratio / curNativeWidth; - this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth; + if (!this.layoutDoc._showSidebar && !(this.dataDoc[this.fieldKey] instanceof WebField)) { + this.layoutDoc.nativeWidth = this.dataDoc[this.fieldKey + "-nativeWidth"] = undefined; + } else { + this.layoutDoc.nativeWidth = nativeWidth * pdfratio; + } } }); sidebarWidth = () => !this.SidebarShown ? 0 : @@ -630,8 +676,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps (NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc)) * this.props.PanelWidth() / NumCast(this.layoutDoc.nativeWidth)) @computed get content() { - const interactive = !this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick && this.props.isContentActive() && this.props.pointerEvents !== "none" && CurrentUserUtils.SelectedTool === InkTool.None && !DocumentDecorations.Instance?.Interacting; + const interactive = !this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick && this.props.isContentActive() && this.props.pointerEvents?.() !== "none" && CurrentUserUtils.SelectedTool === InkTool.None && !DocumentDecorations.Instance?.Interacting; return <div className={"webBox-cont" + (interactive ? "-interactive" : "")} + onKeyDown={e => e.stopPropagation()} style={{ width: !this.layoutDoc.forceReflow ? NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]) || `100%` : "100%", }}> {this.urlContent} </div>; @@ -639,10 +686,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @computed get annotationLayer() { TraceMobx(); - const pe = this.pointerEvents(); return <div className="webBox-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer}> {this.inlineTextAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map(anno => - <Annotation {...this.props} fieldKey={this.annotationKey} pointerEvents={pe} showInfo={this.showInfo} dataDoc={this.dataDoc} anno={anno} key={`${anno[Id]}-annotation`} />)} + <Annotation {...this.props} fieldKey={this.annotationKey} pointerEvents={this.pointerEvents} showInfo={this.showInfo} dataDoc={this.dataDoc} anno={anno} key={`${anno[Id]}-annotation`} />)} </div>; } @@ -654,7 +700,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps <div className="webBox-overlayCont" onPointerDown={(e) => e.stopPropagation()} style={{ left: `${this._searching ? 0 : 100}%` }}> <button className="webBox-overlayButton" title={"search"} /> <input className="webBox-searchBar" placeholder="Search" ref={this._searchRef} onChange={this.searchStringChanged} - onKeyDown={e => e.keyCode === KeyCodes.ENTER && this.search(this._searchString, e.shiftKey)} /> + onKeyDown={e => { e.key === "Enter" && this.search(this._searchString, e.shiftKey); e.stopPropagation(); }} /> <button className="webBox-search" title="Search" onClick={e => this.search(this._searchString, e.shiftKey)}> <FontAwesomeIcon icon="search" size="sm" /> </button> @@ -684,9 +730,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } return this.props.styleProvider?.(doc, props, property); } - pointerEvents = () => !this._draggingSidebar && this.props.isContentActive() && this.props.pointerEvents !== "none" && !MarqueeOptionsMenu.Instance.isShown() ? "all" : SnappingManager.GetIsDragging() ? undefined : "none"; + pointerEvents = () => !this._draggingSidebar && this.props.isContentActive() && this.props.pointerEvents?.() !== "none" && !MarqueeOptionsMenu.Instance.isShown() ? "all" : SnappingManager.GetIsDragging() ? undefined : "none"; + annotationPointerEvents = () => this._isAnnotating || SnappingManager.GetIsDragging() ? "all" : "none"; render() { - const pointerEvents = this.props.layerProvider?.(this.layoutDoc) === false ? "none" : this.props.pointerEvents ? this.props.pointerEvents as any : undefined; + const pointerEvents = this.layoutDoc._lockedPosition ? "none" : this.props.pointerEvents?.() as any; const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1; const scale = previewScale * (this.props.scaling?.() || 1); const renderAnnotations = (docFilters?: () => string[]) => @@ -709,14 +756,14 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} removeDocument={this.removeDocument} moveDocument={this.moveDocument} - addDocument={this.sidebarAddDocument} + addDocument={this.addDocument} styleProvider={this.childStyleProvider} childPointerEvents={this.props.isContentActive() ? "all" : undefined} - pointerEvents={this._isAnnotating || SnappingManager.GetIsDragging() ? "all" : "none"} />; + pointerEvents={this.annotationPointerEvents} />; return ( <div className="webBox" ref={this._mainCont} style={{ pointerEvents: this.pointerEvents(), display: this.props.thumbShown?.() ? "none" : undefined }} > - <div className="webBox-background" /> + <div className="webBox-background" style={{ backgroundColor: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor) }} /> <div className="webBox-container" style={{ width: `calc(${100 / scale}% - ${!this.SidebarShown ? 0 : (this.sidebarWidth() - WebBox.sidebarResizerWidth) / scale * (this._previewWidth ? scale : 1)}px)`, transform: `scale(${scale})`, @@ -731,7 +778,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps onScroll={e => this.setDashScrollTop(this._outerRef.current?.scrollTop || 0)} onPointerDown={this.onMarqueeDown} > - <div className={"webBox-innerContent"} style={{ height: NumCast(this.scrollHeight, 50), pointerEvents }}> + <div className={"webBox-innerContent"} style={{ height: this._webPageHasBeenRendered ? NumCast(this.scrollHeight, 50) : "100%", pointerEvents }}> {this.content} <div style={{ mixBlendMode: "multiply" }}> {renderAnnotations(this.transparentFilter)} @@ -752,7 +799,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps addDocument={this.addDocumentWrapper} docView={this.props.docViewPath().lastElement()} finishMarquee={this.finishMarquee} - savedAnnotations={this._savedAnnotations} + savedAnnotations={this.savedAnnotationsCreator} annotationLayer={this._annotationLayer.current} mainCont={this._mainCont.current} /> </div>} </div > diff --git a/src/client/views/nodes/WebBoxRenderer.js b/src/client/views/nodes/WebBoxRenderer.js index d2b701cf9..d9524dd6e 100644 --- a/src/client/views/nodes/WebBoxRenderer.js +++ b/src/client/views/nodes/WebBoxRenderer.js @@ -176,7 +176,7 @@ var ForeignHtmlRenderer = function (styleSheets) { * * @returns {Promise<String>} */ - const buildSvgDataUri = async function (webUrl, contentHtml, width, height, scroll) { + const buildSvgDataUri = async function (webUrl, contentHtml, width, height, scroll, xoff) { return new Promise(async function (resolve, reject) { @@ -240,7 +240,7 @@ var ForeignHtmlRenderer = function (styleSheets) { // build SVG string const svg = `<svg xmlns='http://www.w3.org/2000/svg' width='${width}' height='${height}'> - <foreignObject x='0' y='${-scroll}' width='${width}' height='${scroll + height}'> + <foreignObject x='${-xoff}' y='${-scroll}' width='${xoff + width}' height='${scroll + height}'> ${contentRootElemString} </foreignObject> </svg>`; @@ -259,11 +259,11 @@ var ForeignHtmlRenderer = function (styleSheets) { * * @return {Promise<Image>} */ - this.renderToImage = async function (webUrl, html, width, height, scroll) { + 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); - img.src = await buildSvgDataUri(webUrl, html, width, height, scroll); + img.src = await buildSvgDataUri(webUrl, html, width, height, scroll, xoff); img.onload = function () { console.log("IMAGE SVG created:" + webUrl); @@ -279,16 +279,16 @@ var ForeignHtmlRenderer = function (styleSheets) { * * @return {Promise<Image>} */ - this.renderToCanvas = async function (webUrl, html, width, height, scroll) { + this.renderToCanvas = async function (webUrl, html, width, height, scroll, xoff, oversample) { return new Promise(async function (resolve, reject) { - const img = await self.renderToImage(webUrl, html, width, height, scroll); + const img = await self.renderToImage(webUrl, html, width, height, scroll, xoff); const canvas = document.createElement('canvas'); - canvas.width = img.width; - canvas.height = img.height; + canvas.width = img.width * oversample; + canvas.height = img.height * oversample; const canvasCtx = canvas.getContext('2d'); - canvasCtx.drawImage(img, 0, 0, img.width, img.height); + canvasCtx.drawImage(img, 0, 0, img.width * oversample, img.height * oversample); resolve(canvas); }); @@ -301,9 +301,9 @@ var ForeignHtmlRenderer = function (styleSheets) { * * @return {Promise<String>} */ - this.renderToBase64Png = async function (webUrl, html, width, height, scroll) { + this.renderToBase64Png = async function (webUrl, html, width, height, scroll, xoff, oversample) { return new Promise(async function (resolve, reject) { - const canvas = await self.renderToCanvas(webUrl, html, width, height, scroll); + const canvas = await self.renderToCanvas(webUrl, html, width, height, scroll, xoff, oversample); resolve(canvas.toDataURL('image/png')); }); }; @@ -311,8 +311,8 @@ var ForeignHtmlRenderer = function (styleSheets) { }; -export function CreateImage(webUrl, styleSheets, html, width, height, scroll) { - 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); +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; } diff --git a/src/client/views/nodes/button/ButtonScripts.ts b/src/client/views/nodes/button/ButtonScripts.ts index f3731b8f9..b4a382faf 100644 --- a/src/client/views/nodes/button/ButtonScripts.ts +++ b/src/client/views/nodes/button/ButtonScripts.ts @@ -1,5 +1,6 @@ import { ScriptingGlobals } from "../../../util/ScriptingGlobals"; import { SelectionManager } from "../../../util/SelectionManager"; +import { Colors } from "../../global/globalEnums"; // toggle: Set overlay status of selected document ScriptingGlobals.add(function changeView(view: string) { @@ -8,7 +9,8 @@ ScriptingGlobals.add(function changeView(view: string) { }); // toggle: Set overlay status of selected document -ScriptingGlobals.add(function toggleOverlay() { +ScriptingGlobals.add(function toggleOverlay(readOnly?: boolean) { const selected = SelectionManager.Views().length ? SelectionManager.Views()[0] : undefined; + if (readOnly) return selected?.Document.z ? Colors.MEDIUM_BLUE : "transparent"; selected ? selected.props.CollectionFreeFormDocumentView?.().float() : console.log("failed"); });
\ No newline at end of file diff --git a/src/client/views/nodes/button/FontIconBox.scss b/src/client/views/nodes/button/FontIconBox.scss index e3e2be515..6cd56f84e 100644 --- a/src/client/views/nodes/button/FontIconBox.scss +++ b/src/client/views/nodes/button/FontIconBox.scss @@ -18,12 +18,12 @@ .fontIconBox-label { color: $white; - position: relative; + bottom: 0; + position: absolute; text-align: center; font-size: 7px; letter-spacing: normal; background-color: inherit; - margin-top: 5px; border-radius: 8px; padding: 0; width: 100%; @@ -40,8 +40,10 @@ height: 80%; } - &.clickBtn { + &.clickBtn, + &.clickBtnLabel { cursor: pointer; + flex-direction: column; &:hover { background-color: rgba(0, 0, 0, 0.3) !important; @@ -53,6 +55,12 @@ } } + &.clickBtnLabel { + svg { + margin-top: -4px; + } + } + &.textBtn { display: grid; /* grid-row: auto; */ @@ -64,12 +72,14 @@ justify-items: center; &:hover { - filter:brightness(0.85) !important; + filter: brightness(0.85) !important; } } - &.tglBtn { + &.tglBtn, + &.tglBtnLabel { cursor: pointer; + flex-direction: column; &.switch { //TOGGLE @@ -146,10 +156,19 @@ } } - &.toolBtn { + &.tglBtnLabel { + svg { + margin-top: -4px; + } + } + + &.toolBtn, + &.toolBtnLabel { cursor: pointer; width: 100%; border-radius: 100%; + flex-direction: column; + margin-top: -4px; svg { width: 60% !important; @@ -157,6 +176,12 @@ } } + &.toolBtnLabel { + svg { + margin-top: -4px; + } + } + &.menuBtn { cursor: pointer !important; border-radius: 0px; @@ -166,15 +191,16 @@ width: 45% !important; height: 45%; } - - &:hover{ + + &:hover { filter: brightness(0.85); } } - &.colorBtn { + &.colorBtn, + &.colorBtnLabel { color: black; cursor: pointer; flex-direction: column; @@ -204,6 +230,12 @@ } } + &.colorBtnLabel { + svg { + margin-top: -4px; + } + } + &.drpdownList { width: 100%; display: grid; @@ -278,10 +310,12 @@ background-color: #e3e3e3; box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3); border-radius: $standard-border-radius; + input[type=range]::-webkit-slider-runnable-track { background: gray; height: 3px; } + input[type=range]::-webkit-slider-thumb { box-shadow: 1px 1px 1px #000000; border: 1px solid #000000; diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx index ca13590de..42c650a09 100644 --- a/src/client/views/nodes/button/FontIconBox.tsx +++ b/src/client/views/nodes/button/FontIconBox.tsx @@ -5,17 +5,19 @@ import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { ColorState, SketchPicker } from 'react-color'; -import { Doc, StrListCast } from '../../../../fields/Doc'; +import { Doc, HeightSym, StrListCast, WidthSym } from '../../../../fields/Doc'; import { InkTool } from '../../../../fields/InkField'; import { createSchema } from '../../../../fields/Schema'; import { ScriptField } from '../../../../fields/ScriptField'; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; import { WebField } from '../../../../fields/URLField'; -import { Utils } from '../../../../Utils'; +import { aggregateBounds, Utils } from '../../../../Utils'; import { DocumentType } from '../../../documents/DocumentTypes'; +import { CurrentUserUtils } from '../../../util/CurrentUserUtils'; import { ScriptingGlobals } from "../../../util/ScriptingGlobals"; import { SelectionManager } from '../../../util/SelectionManager'; import { undoBatch, UndoManager } from '../../../util/UndoManager'; +import { CollectionFreeFormView } from '../../collections/collectionFreeForm'; import { CollectionViewType } from '../../collections/CollectionView'; import { ContextMenu } from '../../ContextMenu'; import { DocComponent } from '../../DocComponent'; @@ -23,6 +25,7 @@ import { EditableView } from '../../EditableView'; import { GestureOverlay } from '../../GestureOverlay'; import { Colors } from '../../global/globalEnums'; import { ActiveFillColor, ActiveInkColor, ActiveInkWidth, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth } from '../../InkingStroke'; +import { InkTranscription } from '../../InkTranscription'; import { StyleProp } from '../../StyleProvider'; import { FieldView, FieldViewProps } from '.././FieldView'; import { RichTextMenu } from '../formattedText/RichTextMenu'; @@ -107,25 +110,27 @@ export class FontIconBox extends DocComponent<ButtonProps>() { // Script for checking the outcome of the toggle const checkResult: number = numScript?.script.run({ value: 0, _readOnly_: true }).result || 0; + const label = !Doc.UserDoc()._showLabel ? (null) : + <div className="fontIconBox-label"> + {this.label} + </div>; + if (numBtnType === NumButtonType.Slider) { - const dropdown = - <div - className="menuButton-dropdownBox" - onPointerDown={e => e.stopPropagation()} - > - <input type="range" step="1" min={NumCast(this.rootDoc.numBtnMin, 0)} max={NumCast(this.rootDoc.numBtnMax, 100)} value={checkResult} - className={"menu-slider"} id="slider" - onPointerDown={() => this._batch = UndoManager.StartBatch("presDuration")} - onPointerUp={() => this._batch?.end()} - onChange={e => { e.stopPropagation(); setValue(Number(e.target.value)); }} - /> - </div>; + const dropdown = <div className="menuButton-dropdownBox" onPointerDown={e => e.stopPropagation()} > + <input type="range" step="1" min={NumCast(this.rootDoc.numBtnMin, 0)} max={NumCast(this.rootDoc.numBtnMax, 100)} value={checkResult} + className={"menu-slider"} id="slider" + onPointerDown={() => this._batch = UndoManager.StartBatch("presDuration")} + onPointerUp={() => this._batch?.end()} + onChange={e => { e.stopPropagation(); setValue(Number(e.target.value)); }} + /> + </div>; return ( <div className={`menuButton ${this.type} ${numBtnType}`} onClick={action(() => this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen)} > {checkResult} + {label} {this.rootDoc.dropDownOpen ? dropdown : null} </div> ); @@ -229,6 +234,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); const script = ScriptCast(this.rootDoc.script); + if (!script) { return null; } let noviceList: string[] = []; let text: string | undefined; @@ -277,7 +283,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { }); const label = !this.label || !Doc.UserDoc()._showLabel ? (null) : - <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor, position: "absolute" }}> + <div className="fontIconBox-label" style={{ bottom: 0, position: "absolute", color: color, backgroundColor: backgroundColor }}> {this.label} </div>; @@ -331,7 +337,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { const curColor = this.colorScript?.script.run({ value: undefined, _readOnly_: true }).result ?? "transparent"; const label = !this.label || !Doc.UserDoc()._showLabel ? (null) : - <div className="fontIconBox-label" style={{ color, backgroundColor, position: "absolute" }}> + <div className="fontIconBox-label" style={{ color, backgroundColor }}> {this.label} </div>; @@ -342,7 +348,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { </div>; setTimeout(() => this.colorPicker(curColor)); // cause an update to the color picker rendered in MainView return ( - <div className={`menuButton ${this.type} ${this.colorPickerClosed}`} + <div className={`menuButton ${this.type + (Doc.UserDoc()._showLabel ? "Label" : "")} ${this.colorPickerClosed}`} style={{ color: color, borderBottomLeftRadius: this.dropdown ? 0 : undefined }} onClick={action(() => this.colorPickerClosed = !this.colorPickerClosed)} onPointerDown={e => e.stopPropagation()}> @@ -375,7 +381,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { // Button label const label = !this.label || !Doc.UserDoc()._showLabel ? (null) : - <div className="fontIconBox-label" style={{ color, backgroundColor, position: "absolute" }}> + <div className="fontIconBox-label" style={{ color, backgroundColor }}> {this.label} </div>; @@ -387,13 +393,13 @@ export class FontIconBox extends DocComponent<ButtonProps>() { <input type="checkbox" checked={backgroundColor === Colors.MEDIUM_BLUE} /> - <span className="slider round"></span> + <span className="slider round" /> </label> </div> ); } else { return ( - <div className={`menuButton ${this.type}`} + <div className={`menuButton ${this.type + (Doc.UserDoc()._showLabel ? "Label" : "")}`} style={{ opacity: 1, backgroundColor, color }}> <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} /> {label} @@ -416,7 +422,8 @@ export class FontIconBox extends DocComponent<ButtonProps>() { style={{ backgroundColor: "transparent", borderBottomLeftRadius: this.dropdown ? 0 : undefined }}> <div className="menuButton-wrap"> <FontAwesomeIcon className={`menuButton-icon-${this.type}`} icon={this.icon} color={"black"} size={"sm"} /> - {!this.label || !Doc.UserDoc()._showLabel ? (null) : <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor }}> {this.label} </div>} + {!this.label || !Doc.UserDoc()._showLabel ? (null) : + <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor }}> {this.label} </div>} </div> </div> ); @@ -443,7 +450,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); const label = !this.label || !Doc.UserDoc()._showLabel ? (null) : - <div className="fontIconBox-label" style={{ color, backgroundColor, position: "absolute" }}> + <div className="fontIconBox-label" style={{ color, backgroundColor }}> {this.label} </div>; @@ -455,7 +462,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { const buttonText = StrCast(this.rootDoc.buttonText); // TODO:glr Add label of button type - let button = this.defaultButton; + let button: JSX.Element | null = this.defaultButton; switch (this.type) { case ButtonType.TextButton: @@ -489,7 +496,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { break; case ButtonType.ToolButton: button = ( - <div className={`menuButton ${this.type}`} style={{ opacity: 1, backgroundColor, color }}> + <div className={`menuButton ${this.type + (Doc.UserDoc()._showLabel ? "Label" : "")}`} style={{ opacity: 1, backgroundColor, color }}> <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} /> {label} </div> @@ -501,7 +508,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { break; case ButtonType.ClickButton: button = ( - <div className={`menuButton ${this.type}`} style={{ color, backgroundColor, opacity: 1 }}> + <div className={`menuButton ${this.type + (Doc.UserDoc()._showLabel ? "Label" : "")}`} style={{ color, backgroundColor, opacity: 1 }}> <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} /> {label} </div> @@ -523,9 +530,10 @@ export class FontIconBox extends DocComponent<ButtonProps>() { } return !this.layoutDoc.toolTip || this.type === ButtonType.DropdownList || this.type === ButtonType.ColorButton || this.type === ButtonType.NumberButton || this.type === ButtonType.EditableText ? button : - <Tooltip title={<div className="dash-tooltip">{StrCast(this.layoutDoc.toolTip)}</div>}> - {button} - </Tooltip>; + button !== null ? + <Tooltip title={<div className="dash-tooltip">{StrCast(this.layoutDoc.toolTip)}</div>}> + {button} + </Tooltip > : null } } @@ -656,13 +664,21 @@ ScriptingGlobals.add(function setFontHighlight(color?: string, checkResult?: boo ScriptingGlobals.add(function setFontSize(size: string | number, checkResult?: boolean) { const editorView = RichTextMenu.Instance?.TextView?.EditorView; if (checkResult) { - return (editorView ? RichTextMenu.Instance.fontSize : StrCast(Doc.UserDoc().fontSize, "10px")).replace("px", ""); + return RichTextMenu.Instance.fontSize.replace("px", ""); } if (typeof size === "number") size = size.toString(); if (size && Number(size).toString() === size) size += "px"; if (editorView) RichTextMenu.Instance.setFontSize(size); else Doc.UserDoc()._fontSize = size; }); +ScriptingGlobals.add(function toggleNoAutoLinkAnchor(checkResult?: boolean) { + const editorView = RichTextMenu.Instance?.TextView?.EditorView; + if (checkResult) { + return (editorView ? RichTextMenu.Instance.noAutoLink : Doc.UserDoc().noAutoLink) ? Colors.MEDIUM_BLUE : "transparent"; + } + if (editorView) RichTextMenu.Instance?.toggleNoAutoLinkAnchor(); + else Doc.UserDoc().noAutoLink = Doc.UserDoc().noAutoLink ? true : false; +}); ScriptingGlobals.add(function toggleBold(checkResult?: boolean) { const editorView = RichTextMenu.Instance?.TextView?.EditorView; @@ -694,6 +710,82 @@ ScriptingGlobals.add(function toggleItalic(checkResult?: boolean) { +export function checkInksToGroup() { + // console.log("getting here to inks group"); + if (CurrentUserUtils.SelectedTool === InkTool.Write) { + CollectionFreeFormView.collectionsWithUnprocessedInk.forEach(ffView => { + // TODO: nda - will probably want to go through ffView unprocessed docs and then see if any of the inksToGroup docs are in it and only use those + // find all inkDocs in ffView.unprocessedDocs that are within 200 pixels of each other + const inksToGroup = ffView.unprocessedDocs.filter(inkDoc => { + // console.log(inkDoc.x, inkDoc.y); + }); + }); + } +} + +export function createInkGroup(inksToGroup?: Doc[], isSubGroup?: boolean) { + // TODO nda - if document being added to is a inkGrouping then we can just add to that group + if (CurrentUserUtils.SelectedTool === InkTool.Write) { + CollectionFreeFormView.collectionsWithUnprocessedInk.forEach(ffView => { + // TODO: nda - will probably want to go through ffView unprocessed docs and then see if any of the inksToGroup docs are in it and only use those + const selected = ffView.unprocessedDocs; + // loop through selected an get the bound + const bounds: { x: number, y: number, width?: number, height?: number }[] = [] + + selected.map(action(d => { + const x = NumCast(d.x); + const y = NumCast(d.y); + const width = d[WidthSym](); + const height = d[HeightSym](); + bounds.push({ x, y, width, height }); + })) + + const aggregBounds = aggregateBounds(bounds, 0, 0); + const marqViewRef = ffView._marqueeViewRef.current; + + // set the vals for bounds in marqueeView + if (marqViewRef) { + marqViewRef._downX = aggregBounds.x; + marqViewRef._downY = aggregBounds.y; + marqViewRef._lastX = aggregBounds.r; + marqViewRef._lastY = aggregBounds.b; + } + + selected.map(action(d => { + const dx = NumCast(d.x); + const dy = NumCast(d.y); + delete d.x; + delete d.y; + delete d.activeFrame; + delete d._timecodeToShow; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection + delete d._timecodeToHide; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection + // calculate pos based on bounds + if (marqViewRef?.Bounds) { + d.x = dx - marqViewRef.Bounds.left - marqViewRef.Bounds.width / 2; + d.y = dy - marqViewRef.Bounds.top - marqViewRef.Bounds.height / 2; + } + return d; + })); + ffView.props.removeDocument?.(selected); + // TODO: nda - this is the code to actually get a new grouped collection + const newCollection = marqViewRef?.getCollection(selected, undefined, true); + if (newCollection) { + newCollection.height = newCollection[HeightSym](); + newCollection.width = newCollection[WidthSym](); + } + + // nda - bug: when deleting a stroke before leaving writing mode, delete the stroke from unprocessed ink docs + newCollection && ffView.props.addDocument?.(newCollection); + // TODO: nda - will probably need to go through and only remove the unprocessed selected docs + ffView.unprocessedDocs = []; + + InkTranscription.Instance.transcribeInk(newCollection, selected, false, ffView); + }); + } + CollectionFreeFormView.collectionsWithUnprocessedInk.clear(); +} + + /** INK * setActiveInkTool * setStrokeWidth @@ -701,6 +793,8 @@ ScriptingGlobals.add(function toggleItalic(checkResult?: boolean) { **/ ScriptingGlobals.add(function setActiveInkTool(tool: string, checkResult?: boolean) { + createInkGroup(); + if (checkResult) { return ((Doc.UserDoc().activeInkTool === tool && !GestureOverlay.Instance?.InkShape) || GestureOverlay.Instance?.InkShape === tool) ? Colors.MEDIUM_BLUE : "transparent"; @@ -716,6 +810,10 @@ ScriptingGlobals.add(function setActiveInkTool(tool: string, checkResult?: boole } else if (tool) { // pen or eraser if (Doc.UserDoc().activeInkTool === tool && !GestureOverlay.Instance.InkShape) { Doc.UserDoc().activeInkTool = InkTool.None; + } else if (tool == "write") { + // console.log("write mode selected - create groupDoc here!", tool) + Doc.UserDoc().activeInkTool = tool; + GestureOverlay.Instance.InkShape = ""; } else { Doc.UserDoc().activeInkTool = tool; GestureOverlay.Instance.InkShape = ""; diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx index 364be461f..1d8e3a2cf 100644 --- a/src/client/views/nodes/formattedText/DashDocView.tsx +++ b/src/client/views/nodes/formattedText/DashDocView.tsx @@ -182,7 +182,6 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> { removeDocument={this.removeDoc} isDocumentActive={returnFalse} isContentActive={this._textBox.props.isContentActive} - layerProvider={this._textBox.props.layerProvider} styleProvider={this._textBox.props.styleProvider} docViewPath={this._textBox.props.docViewPath} ScreenToLocalTransform={this.getDocTransform} diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx index 34908e54b..bb3791f1e 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.tsx +++ b/src/client/views/nodes/formattedText/DashFieldView.tsx @@ -8,35 +8,41 @@ import { SchemaHeaderField } from "../../../../fields/SchemaHeaderField"; import { ComputedField } from "../../../../fields/ScriptField"; import { Cast, StrCast } from "../../../../fields/Types"; import { DocServer } from "../../../DocServer"; -import { DocUtils } from "../../../documents/Documents"; import { CollectionViewType } from "../../collections/CollectionView"; import "./DashFieldView.scss"; import { FormattedTextBox } from "./FormattedTextBox"; import React = require("react"); +import { emptyFunction, returnFalse, setupMoveUpEvents } from "../../../../Utils"; +import { AntimodeMenu, AntimodeMenuProps } from "../../AntimodeMenu"; +import { Tooltip } from "@material-ui/core"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; export class DashFieldView { _fieldWrapper: HTMLDivElement; // container for label and value constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) { + const { boolVal, strVal } = DashFieldViewInternal.fieldContent(tbox.props.Document, tbox.rootDoc, node.attrs.fieldKey); + this._fieldWrapper = document.createElement("div"); this._fieldWrapper.style.width = node.attrs.width; this._fieldWrapper.style.height = node.attrs.height; this._fieldWrapper.style.fontWeight = "bold"; this._fieldWrapper.style.position = "relative"; this._fieldWrapper.style.display = "inline-block"; + this._fieldWrapper.textContent = node.attrs.fieldKey.startsWith("#") ? node.attrs.fieldKey : node.attrs.fieldKey + " " + strVal; this._fieldWrapper.onkeypress = function (e: any) { e.stopPropagation(); }; this._fieldWrapper.onkeydown = function (e: any) { e.stopPropagation(); }; this._fieldWrapper.onkeyup = function (e: any) { e.stopPropagation(); }; this._fieldWrapper.onmousedown = function (e: any) { e.stopPropagation(); }; - ReactDOM.render(<DashFieldViewInternal + 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._fieldWrapper); + />, this._fieldWrapper)); (this as any).dom = this._fieldWrapper; } destroy() { ReactDOM.unmountComponentAtNode(this._fieldWrapper); } @@ -58,7 +64,6 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna _textBoxDoc: Doc; _fieldKey: string; _fieldStringRef = React.createRef<HTMLSpanElement>(); - @observable _showEnumerables: boolean = false; @observable _dashDoc: Doc | undefined; constructor(props: IDashFieldViewInternal) { @@ -77,16 +82,17 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna this._reactionDisposer?.(); } - multiValueDelimeter = ";"; + public static multiValueDelimeter = ";"; + public static fieldContent(textBoxDoc: Doc, dashDoc: Doc, fieldKey: string) { + const dashVal = dashDoc[fieldKey] ?? dashDoc[DataSym][fieldKey] ?? (fieldKey === "PARAMS" ? textBoxDoc[fieldKey] : ""); + const fval = dashVal instanceof List ? dashVal.join(DashFieldViewInternal.multiValueDelimeter) : StrCast(dashVal).startsWith(":=") || dashVal === "" ? Doc.Layout(textBoxDoc)[fieldKey] : dashVal; + return { boolVal: Cast(fval, "boolean", null), strVal: Field.toString(fval as Field) || "" }; + } // set the display of the field's value (checkbox for booleans, span of text for strings) @computed get fieldValueContent() { if (this._dashDoc) { - const dashVal = this._dashDoc[this._fieldKey] ?? this._dashDoc[DataSym][this._fieldKey] ?? (this._fieldKey === "PARAMS" ? this._textBoxDoc[this._fieldKey] : ""); - const fval = dashVal instanceof List ? dashVal.join(this.multiValueDelimeter) : StrCast(dashVal).startsWith(":=") || dashVal === "" ? Doc.Layout(this._textBoxDoc)[this._fieldKey] : dashVal; - const boolVal = Cast(fval, "boolean", null); - const strVal = Field.toString(fval as Field) || ""; - + const { boolVal, strVal } = DashFieldViewInternal.fieldContent(this._textBoxDoc, this._dashDoc, this._fieldKey); // field value is a boolean, so use a checkbox or similar widget to display it if (boolVal === true || boolVal === false) { return <input @@ -109,10 +115,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna ref={r => { r?.addEventListener("keydown", e => this.fieldSpanKeyDown(e, r)); r?.addEventListener("blur", e => r && this.updateText(r.textContent!, false)); - r?.addEventListener("pointerdown", action((e) => { - this._showEnumerables = true; - e.stopPropagation(); - })); + r?.addEventListener("pointerdown", action(e => e.stopPropagation())); }} > {strVal} </span>; @@ -124,7 +127,6 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna @action fieldSpanKeyDown = (e: KeyboardEvent, span: HTMLSpanElement) => { if (e.key === "Enter") { // handle the enter key by "submitting" the current text to Dash's database. - e.ctrlKey && DocUtils.addFieldEnumerations(this._textBoxDoc, this._fieldKey, [{ title: span.textContent! }]); this.updateText(span.textContent!, true); e.preventDefault();// prevent default to avoid a newline from being generated and wiping out this field view } @@ -142,7 +144,6 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna @action updateText = (nodeText: string, forceMatch: boolean) => { - this._showEnumerables = false; if (nodeText) { const newText = nodeText.startsWith(":=") || nodeText.startsWith("=:=") ? ":=-computed-" : nodeText; @@ -154,7 +155,6 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna (options instanceof Doc) && DocListCast(options.data).forEach(opt => (forceMatch ? StrCast(opt.title).startsWith(newText) : StrCast(opt.title) === newText) && (modText = StrCast(opt.title))); if (modText) { // elementfieldSpan.innerHTML = this._dashDoc![this._fieldKey as string] = modText; - DocUtils.addFieldEnumerations(this._textBoxDoc, this._fieldKey, []); Doc.SetInPlace(this._dashDoc!, this._fieldKey, modText, true); } // if the text starts with a ':=' then treat it as an expression by making a computed field from its value storing it in the key else if (nodeText.startsWith(":=")) { @@ -166,7 +166,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna if (this._fieldKey.startsWith("_")) Doc.Layout(this._textBoxDoc)[this._fieldKey] = Number(newText); Doc.SetInPlace(this._dashDoc!, this._fieldKey, newText, true); } else { - const splits = newText.split(this.multiValueDelimeter); + const splits = newText.split(DashFieldViewInternal.multiValueDelimeter); if (this._fieldKey !== "PARAMS" || !this._textBoxDoc[this._fieldKey] || this._dashDoc?.PARAMS) { const strVal = splits.length > 1 ? new List<string>(splits) : newText; if (this._fieldKey.startsWith("_")) Doc.Layout(this._textBoxDoc)[this._fieldKey] = strVal; @@ -178,18 +178,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna } } - // display a collection of all the enumerable values for this field - onPointerDownEnumerables = async (e: any) => { - e.stopPropagation(); - const collview = await DocUtils.addFieldEnumerations(this._textBoxDoc, this._fieldKey, [{ title: this._fieldKey }]); - collview instanceof Doc && this.props.tbox.props.addDocTab(collview, "add:right"); - } - - - // clicking on the label creates a pivot view collection of all documents - // in the same collection. The pivot field is the fieldKey of this label - onPointerDownLabelSpan = (e: any) => { - e.stopPropagation(); + createPivotForField = (e: React.MouseEvent) => { let container = this.props.tbox.props.ContainingCollectionView; while (container?.props.Document.isTemplateForField || container?.props.Document.isTemplateDoc) { container = container.props.ContainingCollectionView; @@ -208,6 +197,16 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna } } + + // clicking on the label creates a pivot view collection of all documents + // in the same collection. The pivot field is the fieldKey of this label + onPointerDownLabelSpan = (e: any) => { + setupMoveUpEvents(this, e, returnFalse, returnFalse, (e) => { + DashFieldViewMenu.createFieldView = this.createPivotForField; + DashFieldViewMenu.Instance.show(e.clientX, e.clientY + 16); + }); + } + render() { return <div className="dashFieldView" style={{ width: this.props.width, @@ -220,8 +219,40 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna {this.props.fieldKey.startsWith("#") ? (null) : this.fieldValueContent} - {!this._showEnumerables ? (null) : <div className="dashFieldView-enumerables" onPointerDown={this.onPointerDownEnumerables} />} - </div >; } +} +@observer +export class DashFieldViewMenu extends AntimodeMenu<AntimodeMenuProps> { + static Instance: DashFieldViewMenu; + static createFieldView: (e: React.MouseEvent) => void = emptyFunction; + constructor(props: any) { + super(props); + DashFieldViewMenu.Instance = this; + } + @action + showFields = (e: React.MouseEvent) => { + DashFieldViewMenu.createFieldView(e); + DashFieldViewMenu.Instance.fadeOut(true); + } + + public show = (x: number, y: number) => { + this.jumpTo(x, y, true); + const hideMenu = () => { + this.fadeOut(true); + document.removeEventListener("pointerdown", hideMenu); + }; + document.addEventListener("pointerdown", hideMenu); + } + render() { + const buttons = [ + <Tooltip key="trash" title={<div className="dash-tooltip">{"Remove Link Anchor"}</div>}> + <button className="antimodeMenu-button" onPointerDown={this.showFields}> + <FontAwesomeIcon icon="eye" size="lg" /> + </button> + </Tooltip>, + ]; + + return this.getElement(buttons); + } }
\ No newline at end of file diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 29117794e..7a500ac88 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -17,7 +17,8 @@ import { InkTool } from '../../../../fields/InkField'; import { PrefetchProxy } from '../../../../fields/Proxy'; import { RichTextField } from "../../../../fields/RichTextField"; import { RichTextUtils } from '../../../../fields/RichTextUtils'; -import { Cast, NumCast, ScriptCast, StrCast } from "../../../../fields/Types"; +import { ComputedField } from '../../../../fields/ScriptField'; +import { Cast, FieldValue, NumCast, ScriptCast, StrCast } from "../../../../fields/Types"; import { GetEffectiveAcl, TraceMobx } from '../../../../fields/util'; import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, OmitKeys, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils'; import { GoogleApiClientUtils, Pulls, Pushes } from '../../../apis/google_docs/GoogleApiClientUtils'; @@ -82,7 +83,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp public static blankState = () => EditorState.create(FormattedTextBox.Instance.config); public static Instance: FormattedTextBox; public static LiveTextUndo: UndoManager.Batch | undefined; - static _highlights: string[] = ["Audio Tags", "Text from Others", "Todo Items", "Important Items", "Disagree Items", "Ignore Items"]; + static _globalHighlights: string[] = ["Audio Tags", "Text from Others", "Todo Items", "Important Items", "Disagree Items", "Ignore Items"]; static _highlightStyleSheet: any = addStyleSheet(); static _bulletStyleSheet: any = addStyleSheet(); static _userStyleSheet: any = addStyleSheet(); @@ -244,8 +245,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const state = this._editorView.state.apply(tx); this._editorView.updateState(state); - const tsel = this._editorView.state.selection.$from; - //tsel.marks().filter(m => m.type === this._editorView!.state.schema.marks.user_mark).map(m => AudioBox.SetScrubTime(Math.max(0, m.attrs.modified * 1000))); 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 @@ -284,6 +283,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this.dataDoc[this.props.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 + ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: curText }); unchanged = false; } this._applyingChange = ""; @@ -347,7 +347,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } autoLink = () => { - if (this._editorView) { + if (this._editorView?.state.doc.textContent) { const newAutoLinks = new Set<Doc>(); const oldAutoLinks = DocListCast(this.props.Document.links).filter(link => link.linkRelationship === LinkManager.AutoKeywords); const f = this._editorView.state.selection.from; @@ -371,9 +371,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp while (node.firstChild && node.firstChild.type.name !== "text") node = node.firstChild; const str = node.textContent; const prefix = str.startsWith("@") ? "" : "-"; - this.dataDoc.title = prefix + str.substring(0, Math.min(40, str.length)) + (str.length > 40 ? "..." : ""); - if (str.startsWith("@") && str.length > 1) { - Doc.AddDocToList(Doc.UserDoc(), "myPublishedDocs", this.rootDoc); + + const cfield = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc.title)); + if (!(cfield instanceof ComputedField)) { + this.dataDoc.title = prefix + str.substring(0, Math.min(40, str.length)) + (str.length > 40 ? "..." : ""); + if (str.startsWith("@") && str.length > 1) { + Doc.AddDocToList(Doc.UserDoc(), "myPublishedDocs", this.rootDoc); + } } } } @@ -388,16 +392,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp flattened1.forEach((flat, i) => { const flattened = this.findInNode(this._editorView!, this._editorView!.state.doc, autoLinkTerm); this._searchIndex = ++this._searchIndex > flattened.length - 1 ? 0 : this._searchIndex; - alink = alink ?? (DocListCast(this.Document.links).find(link => - Doc.AreProtosEqual(Cast(link.anchor1, Doc, null), this.rootDoc) && - Doc.AreProtosEqual(Cast(link.anchor2, Doc, null), target)) || DocUtils.MakeLink({ doc: this.props.Document }, { doc: target }, - LinkManager.AutoKeywords)!); - newAutoLinks.add(alink); const splitter = editorView.state.schema.marks.splitter.create({ id: Utils.GenerateGuid() }); const sel = flattened[i]; tr = tr.addMark(sel.from, sel.to, splitter); tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => { - if (node.firstChild === null && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) { + if (node.firstChild === null && !node.marks.find((m: Mark) => m.type.name === schema.marks.noAutoLinkAnchor.name) && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) { + alink = alink ?? (DocListCast(this.Document.links).find(link => + Doc.AreProtosEqual(Cast(link.anchor1, Doc, null), this.rootDoc) && + Doc.AreProtosEqual(Cast(link.anchor2, Doc, null), target)) || DocUtils.MakeLink({ doc: this.props.Document }, { doc: target }, + LinkManager.AutoKeywords)!); + newAutoLinks.add(alink); const allAnchors = [{ href: Doc.localServerPath(target), title: "a link", anchorId: this.props.Document[Id] }]; allAnchors.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.autoLinkAnchor.name)?.attrs.allAnchors ?? [])); const link = editorView.state.schema.marks.autoLinkAnchor.create({ allAnchors, title: "auto term", location: "add:right" }); @@ -409,6 +413,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } return tr; } + @action + search = (searchString: string, bwd?: boolean, clear: boolean = false) => { + if (clear) this.unhighlightSearchTerms(); + else this.highlightSearchTerms([searchString], bwd!); + return true; + } highlightSearchTerms = (terms: string[], backward: boolean) => { if (this._editorView && (this._editorView as any).docView && terms.some(t => t)) { const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight); @@ -545,35 +555,40 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } updateHighlights = () => { + const highlights = FormattedTextBox._globalHighlights; clearStyleSheetRules(FormattedTextBox._userStyleSheet); - if (FormattedTextBox._highlights.indexOf("Audio Tags") === -1) { + if (highlights.indexOf("Audio Tags") === -1) { addStyleSheetRule(FormattedTextBox._userStyleSheet, "audiotag", { display: "none" }, ""); } - if (FormattedTextBox._highlights.indexOf("Text from Others") !== -1) { + if (highlights.indexOf("Text from Others") !== -1) { addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-remote", { background: "yellow" }); } - if (FormattedTextBox._highlights.indexOf("My Text") !== -1) { + if (highlights.indexOf("My Text") !== -1) { addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { background: "moccasin" }); } - if (FormattedTextBox._highlights.indexOf("Todo Items") !== -1) { + if (highlights.indexOf("Todo Items") !== -1) { addStyleSheetRule(FormattedTextBox._userStyleSheet, "UT-" + "todo", { outline: "black solid 1px" }); } - if (FormattedTextBox._highlights.indexOf("Important Items") !== -1) { + if (highlights.indexOf("Important Items") !== -1) { addStyleSheetRule(FormattedTextBox._userStyleSheet, "UT-" + "important", { "font-size": "larger" }); } - if (FormattedTextBox._highlights.indexOf("Disagree Items") !== -1) { + if (highlights.indexOf("Bold Text") !== -1) { + addStyleSheetRule(FormattedTextBox._userStyleSheet, ".formattedTextBox-inner-selected .ProseMirror strong > span", { "font-size": "large" }, ""); + addStyleSheetRule(FormattedTextBox._userStyleSheet, ".formattedTextBox-inner-selected .ProseMirror :not(strong > span)", { "font-size": "0px" }, ""); + } + if (highlights.indexOf("Disagree Items") !== -1) { addStyleSheetRule(FormattedTextBox._userStyleSheet, "UT-" + "disagree", { "text-decoration": "line-through" }); } - if (FormattedTextBox._highlights.indexOf("Ignore Items") !== -1) { + if (highlights.indexOf("Ignore Items") !== -1) { addStyleSheetRule(FormattedTextBox._userStyleSheet, "UT-" + "ignore", { "font-size": "1" }); } - if (FormattedTextBox._highlights.indexOf("By Recent Minute") !== -1) { + if (highlights.indexOf("By Recent Minute") !== -1) { addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { opacity: "0.1" }); const min = Math.round(Date.now() / 1000 / 60); numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-min-" + (min - i), { opacity: ((10 - i - 1) / 10).toString() })); setTimeout(this.updateHighlights); } - if (FormattedTextBox._highlights.indexOf("By Recent Hour") !== -1) { + if (highlights.indexOf("By Recent Hour") !== -1) { addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { opacity: "0.1" }); const hr = Math.round(Date.now() / 1000 / 60 / 60); numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-hr-" + (hr - i), { opacity: ((10 - i - 1) / 10).toString() })); @@ -630,19 +645,19 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp }), icon: icon }); }); - !Doc.UserDoc().noviceMode && changeItems.push({ description: "FreeForm", event: () => DocUtils.makeCustomViewClicked(this.rootDoc, Docs.Create.FreeformDocument, "freeform"), icon: "eye" }); const highlighting: ContextMenuProps[] = []; - const noviceHighlighting = ["Audio Tags", "My Text", "Text from Others"]; + const noviceHighlighting = ["Audio Tags", "My Text", "Text from Others", "Bold Text"]; const expertHighlighting = [...noviceHighlighting, "Important Items", "Ignore Items", "Disagree Items", "By Recent Minute", "By Recent Hour"]; (Doc.UserDoc().noviceMode ? noviceHighlighting : expertHighlighting).forEach(option => highlighting.push({ - description: (FormattedTextBox._highlights.indexOf(option) === -1 ? "Highlight " : "Unhighlight ") + option, event: () => { + description: (FormattedTextBox._globalHighlights.indexOf(option) === -1 ? "Highlight " : "Unhighlight ") + option, event: () => { e.stopPropagation(); - if (FormattedTextBox._highlights.indexOf(option) === -1) { - FormattedTextBox._highlights.push(option); + if (FormattedTextBox._globalHighlights.indexOf(option) === -1) { + FormattedTextBox._globalHighlights.push(option); } else { - FormattedTextBox._highlights.splice(FormattedTextBox._highlights.indexOf(option), 1); + FormattedTextBox._globalHighlights.splice(FormattedTextBox._globalHighlights.indexOf(option), 1); } + runInAction(() => this.layoutDoc._highlights = FormattedTextBox._globalHighlights.join("")); this.updateHighlights(); }, icon: "expand-arrows-alt" })); @@ -752,6 +767,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } } + // TODO: nda -- Look at how link anchors are added makeLinkAnchor(anchorDoc?: Doc, location?: string, targetHref?: string, title?: string) { const state = this._editorView?.state; if (state) { @@ -859,7 +875,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this._disposers.componentHeights = reaction( // set the document height when one of the component heights changes and autoHeight is on () => ({ sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, autoHeight: this.autoHeight, marginsHeight: this.autoHeightMargins }), ({ sidebarHeight, textHeight, autoHeight, marginsHeight }) => { - autoHeight && this.props.setHeight?.(marginsHeight + Math.max(sidebarHeight, textHeight)); + autoHeight && this.props.setHeight?.((this.props.scaling?.() || 1) * (marginsHeight + Math.max(sidebarHeight, textHeight))); }, { fireImmediately: true }); this._disposers.links = reaction(() => DocListCast(this.dataDoc.links), // if a link is deleted, then remove all hyperlinks that reference it from the text's marks newLinks => { @@ -921,6 +937,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this._disposers.selected = reaction(() => this.props.isSelected(), action(selected => { + this.layoutDoc._highlights = selected ? FormattedTextBox._globalHighlights.join("") : ""; if (RichTextMenu.Instance?.view === this._editorView && !selected) { RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined); } @@ -958,7 +975,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp }, { fireImmediately: true } ); quickScroll = undefined; - setTimeout(this.tryUpdateScrollHeight, 10); + this.tryUpdateScrollHeight(); } pushToGoogleDoc = async () => { @@ -1189,19 +1206,21 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const selectOnLoad = this.rootDoc[Id] === FormattedTextBox.SelectOnLoad && (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath())); if (selectOnLoad && !this.props.dontRegisterView && !this.props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) { + const selLoadChar = FormattedTextBox.SelectOnLoadChar; FormattedTextBox.SelectOnLoad = ""; this.props.select(false); - if (FormattedTextBox.SelectOnLoadChar && this._editorView) { + if (selLoadChar && this._editorView) { const $from = this._editorView.state.selection.anchor ? this._editorView.state.doc.resolve(this._editorView.state.selection.anchor - 1) : undefined; const mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) }); const curMarks = this._editorView.state.storedMarks ?? $from?.marksAcross(this._editorView.state.selection.$head) ?? []; const storedMarks = [...curMarks.filter(m => m.type !== mark.type), mark]; const tr = this._editorView.state.tr.setStoredMarks(storedMarks).insertText(FormattedTextBox.SelectOnLoadChar, this._editorView.state.doc.content.size - 1, this._editorView.state.doc.content.size).setStoredMarks(storedMarks); this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size)))); - FormattedTextBox.SelectOnLoadChar = ""; - } else if (curText && !FormattedTextBox.DontSelectInitialText) { - selectAll(this._editorView!.state, this._editorView?.dispatch); + } else if (this._editorView && curText && !FormattedTextBox.DontSelectInitialText) { + selectAll(this._editorView.state, this._editorView?.dispatch); this.startUndoTypingBatch(); + } 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; } @@ -1230,6 +1249,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } onPointerDown = (e: React.PointerEvent): void => { + if (this.Document.forceActive) e.stopPropagation(); this.tryUpdateScrollHeight(); // if a doc a fitwidth doc is being viewed in different context (eg freeform & lightbox), then it will have conflicting heights. so when the doc is clicked on, we want to make sure it has the appropriate height for the selected view. if ((e.target as any).tagName === "AUDIOTAG") { e.preventDefault(); @@ -1466,11 +1486,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp }, "titler"); } } + onKeyDown = (e: React.KeyboardEvent) => { - // single line text boxes need to pass through tab/enter/backspace so that their containers can respond (eg, an outline container) - if (this.rootDoc._singleLine && ((e.key === "Backspace" && !this.dataDoc[this.fieldKey]?.Text) || ["Tab", "Enter"].includes(e.key))) { - return; - } if (e.altKey) { e.preventDefault(); return; @@ -1626,11 +1643,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } render() { TraceMobx(); - const selected = this.props.isSelected(); + const selected = this.props.isSelected() || this.Document.forceActive; const active = this.props.isContentActive(); const scale = (this.props.scaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1); const rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : ""; - const interactive = (CurrentUserUtils.SelectedTool === InkTool.None || SnappingManager.GetIsDragging()) && (this.layoutDoc.z || this.props.layerProvider?.(this.layoutDoc) !== false); + const interactive = (CurrentUserUtils.SelectedTool === InkTool.None || SnappingManager.GetIsDragging()) && (this.layoutDoc.z || !this.layoutDoc._lockedPosition); if (!selected && FormattedTextBoxComment.textBox === this) setTimeout(FormattedTextBoxComment.Hide); const minimal = this.props.ignoreAutoHeight; const paddingX = NumCast(this.layoutDoc._xMargin, this.props.xPadding || 0); @@ -1652,7 +1669,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp <div className={`formattedTextBox-cont`} ref={this._ref} style={{ overflow: this.autoHeight ? "hidden" : undefined, - height: this.props.height || (this.autoHeight && this.props.renderDepth ? "max-content" : undefined), + height: this.props.height || (this.autoHeight && this.props.renderDepth && !this.props.suppressSetHeight ? "max-content" : undefined), background: this.props.background ? this.props.background : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor), color: this.props.color ? this.props.color : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color), fontSize: this.props.fontSize ? this.props.fontSize : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontSize), diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts index c76eda859..fb49b0698 100644 --- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts +++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts @@ -1,20 +1,15 @@ -import { chainCommands, exitCode, joinDown, joinUp, lift, deleteSelection, joinBackward, selectNodeBackward, setBlockType, splitBlockKeepMarks, toggleMark, wrapIn, newlineInCode } from "prosemirror-commands"; -import { liftTarget } from "prosemirror-transform"; +import { chainCommands, deleteSelection, exitCode, joinBackward, joinDown, joinUp, lift, newlineInCode, selectNodeBackward, setBlockType, splitBlockKeepMarks, toggleMark, wrapIn } from "prosemirror-commands"; import { redo, undo } from "prosemirror-history"; import { Schema } from "prosemirror-model"; -import { liftListItem, sinkListItem } from "./prosemirrorPatches.js"; -import { splitListItem, wrapInList, } from "prosemirror-schema-list"; -import { EditorState, Transaction, TextSelection } from "prosemirror-state"; -import { SelectionManager } from "../../../util/SelectionManager"; -import { NumCast, BoolCast, Cast, StrCast } from "../../../../fields/Types"; -import { Doc, DataSym, DocListCast, AclAugment, AclSelfEdit } from "../../../../fields/Doc"; -import { FormattedTextBox } from "./FormattedTextBox"; -import { Id } from "../../../../fields/FieldSymbols"; -import { Docs } from "../../../documents/Documents"; -import { Utils } from "../../../../Utils"; -import { listSpec } from "../../../../fields/Schema"; -import { List } from "../../../../fields/List"; +import { splitListItem, wrapInList } from "prosemirror-schema-list"; +import { EditorState, TextSelection, Transaction } from "prosemirror-state"; +import { liftTarget } from "prosemirror-transform"; +import { AclAugment, AclSelfEdit, Doc } from "../../../../fields/Doc"; import { GetEffectiveAcl } from "../../../../fields/util"; +import { Utils } from "../../../../Utils"; +import { Docs } from "../../../documents/Documents"; +import { SelectionManager } from "../../../util/SelectionManager"; +import { liftListItem, sinkListItem } from "./prosemirrorPatches.js"; const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false; @@ -48,29 +43,6 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey keys[key] = cmd; } - /// bcz; Argh!! replace with an onEnter func that conditionally handles Enter - const addTextBox = (below: boolean, force?: boolean) => { - if (props.Document.treeViewType === "outline") return true; // bcz: Arghh .. need to determine if this is an treeViewOutlineBox in which case Enter's are ignored.. - const layoutDoc = props.Document; - const originalDoc = layoutDoc.rootDocument || layoutDoc; - if (force || props.Document._singleLine) { - const layoutKey = StrCast(originalDoc.layoutKey); - const newDoc = Doc.MakeCopy(originalDoc, true); - const dataField = originalDoc[Doc.LayoutFieldKey(newDoc)]; - newDoc[DataSym][Doc.LayoutFieldKey(newDoc)] = dataField === undefined || Cast(dataField, listSpec(Doc), null)?.length !== undefined ? new List<Doc>([]) : undefined; - if (below) newDoc.y = NumCast(originalDoc.y) + NumCast(originalDoc._height) + 10; - else newDoc.x = NumCast(originalDoc.x) + NumCast(originalDoc._width) + 10; - if (layoutKey !== "layout" && originalDoc[layoutKey] instanceof Doc) { - newDoc[layoutKey] = originalDoc[layoutKey]; - } - Doc.GetProto(newDoc).text = undefined; - FormattedTextBox.SelectOnLoad = newDoc[Id]; - props.addDocument(newDoc); - return true; - } - return false; - }; - const canEdit = (state: any) => { switch (GetEffectiveAcl(props.Document)) { case AclAugment: return false; @@ -108,12 +80,12 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey //Commands for lists bind("Ctrl-i", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && wrapInList(schema.nodes.ordered_list)(state as any, dispatch as any)); + bind("Ctrl-Tab", () => props.onKey?.(event, props) ? true : true); + bind("Alt-Tab", () => props.onKey?.(event, props) ? true : true); + bind("Meta-Tab", () => props.onKey?.(event, props) ? true : true); + bind("Meta-Enter", () => props.onKey?.(event, props) ? true : true); bind("Tab", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { - /// bcz; Argh!! replace layotuTEmpalteString with a onTab prop conditionally handles Tab); - if (props.Document._singleLine) { - if (!props.LayoutTemplateString) return addTextBox(false, true); - return true; - } + if (props.onKey?.(event, props)) return true; if (!canEdit(state)) return true; const ref = state.selection; const range = ref.$from.blockRange(ref.$to); @@ -138,8 +110,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey }); bind("Shift-Tab", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { - /// bcz; Argh!! replace with a onShiftTab prop conditionally handles Tab); - if (props.Document._singleLine) return true; + if (props.onKey?.(event, props)) return true; if (!canEdit(state)) return true; const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks()); @@ -187,14 +158,13 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey return tx; }; - //Command to create a text document to the right of the selected textbox - bind("Alt-Enter", () => addTextBox(false, true)); - //Command to create a text document to the bottom of the selected textbox - bind("Ctrl-Enter", () => addTextBox(true, true)); + bind("Alt-Enter", () => props.onKey?.(event, props) ? true : true); + bind("Ctrl-Enter", () => props.onKey?.(event, props) ? true : true); // backspace = chainCommands(deleteSelection, joinBackward, selectNodeBackward); bind("Backspace", (state: EditorState<S>, dispatch: (tx: Transaction<Schema<any, any>>) => void) => { + if (props.onKey?.(event, props)) return true; if (!canEdit(state)) return true; if (!deleteSelection(state, (tx: Transaction<S>) => { @@ -216,8 +186,8 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey //newlineInCode, createParagraphNear, liftEmptyBlock, splitBlock //command to break line bind("Enter", (state: EditorState<S>, dispatch: (tx: Transaction<Schema<any, any>>) => void) => { - if (addTextBox(true, false)) return true; + if (props.onKey?.(event, props)) return true; if (!canEdit(state)) return true; const trange = state.selection.$from.blockRange(state.selection.$to); @@ -276,8 +246,6 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey return false; }); - // mac && bind("Ctrl-Enter", cmd); - // bind("Mod-Enter", cmd); bind("Shift-Enter", cmd); return keys; diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 4814d6e9a..9bc2e5628 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -1,10 +1,10 @@ import React = require("react"); import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Tooltip } from "@material-ui/core"; -import { action, IReactionDisposer, observable, reaction, runInAction, computed } from "mobx"; +import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import { lift, wrapIn } from "prosemirror-commands"; -import { Mark, MarkType, Node as ProsNode, NodeType, ResolvedPos } from "prosemirror-model"; +import { Mark, MarkType, Node as ProsNode, ResolvedPos } from "prosemirror-model"; import { wrapInList } from "prosemirror-schema-list"; import { EditorState, NodeSelection, TextSelection } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; @@ -35,6 +35,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { public _brushMap: Map<string, Set<Mark>> = new Map(); @observable private collapsed: boolean = false; + @observable private _noLinkActive: boolean = false; @observable private _boldActive: boolean = false; @observable private _italicsActive: boolean = false; @observable private _underlineActive: boolean = false; @@ -79,6 +80,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this._reaction?.(); } + @computed get noAutoLink() { return this._noLinkActive; } @computed get bold() { return this._boldActive; } @computed get underline() { return this._underlineActive; } @computed get italics() { return this._italicsActive; } @@ -118,7 +120,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this.activeListType = this.getActiveListStyle(); this._activeAlignment = this.getActiveAlignment(); this._activeFontFamily = !activeFamilies.length ? "Arial" : activeFamilies.length === 1 ? String(activeFamilies[0]) : "various"; - this._activeFontSize = !activeSizes.length ? "13px" : activeSizes[0]; + this._activeFontSize = !activeSizes.length ? StrCast(this.TextView.Document.fontSize, StrCast(Doc.UserDoc().fontSize, "10px")) : activeSizes[0]; this._activeFontColor = !activeColors.length ? "black" : activeColors.length > 0 ? String(activeColors[0]) : "..."; this.activeHighlightColor = !activeHighlights.length ? "" : activeHighlights.length > 0 ? String(activeHighlights[0]) : "..."; @@ -220,7 +222,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { let activeMarks: MarkType[] = []; if (!this.view || !this.TextView.props.isSelected(true)) return activeMarks; - const markGroup = [schema.marks.strong, schema.marks.em, schema.marks.underline, schema.marks.strikethrough, schema.marks.superscript, schema.marks.subscript]; + const markGroup = [schema.marks.noAutoLinkAnchor, schema.marks.strong, schema.marks.em, schema.marks.underline, schema.marks.strikethrough, schema.marks.superscript, schema.marks.subscript]; if (this.view.state.storedMarks) return this.view.state.storedMarks.map(mark => mark.type); //current selection const { empty, ranges, $to } = this.view.state.selection as TextSelection; @@ -264,6 +266,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { setActiveMarkButtons(activeMarks: MarkType[] | undefined) { if (!activeMarks) return; + this._noLinkActive = false; this._boldActive = false; this._italicsActive = false; this._underlineActive = false; @@ -273,6 +276,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { activeMarks.forEach(mark => { switch (mark.name) { + case "noAutoLinkAnchor": this._noLinkActive = true; break; case "strong": this._boldActive = true; break; case "em": this._italicsActive = true; break; case "underline": this._underlineActive = true; break; @@ -283,6 +287,14 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { }); } + toggleNoAutoLinkAnchor = () => { + if (this.view) { + const mark = this.view.state.schema.mark(this.view.state.schema.marks.noAutoLinkAnchor); + this.setMark(mark, this.view.state, this.view.dispatch, false); + this.TextView.autoLink(); + this.view.focus(); + } + } toggleBold = () => { if (this.view) { const mark = this.view.state.schema.mark(this.view.state.schema.marks.strong); @@ -310,10 +322,17 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { setFontSize = (fontSize: string) => { if (this.view) { - const fmark = this.view.state.schema.marks.pFontSize.create({ fontSize }); - this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true); - this.view.focus(); - this.updateMenu(this.view, undefined, this.props); + if (this.view.state.selection.from === 1 && this.view.state.selection.empty && + (!this.view.state.doc.nodeAt(1) || !this.view.state.doc.nodeAt(1)?.marks.some(m => m.type.name === fontSize))) { + this.TextView.dataDoc.fontSize = fontSize; + this.view.focus(); + this.updateMenu(this.view, undefined, this.props); + } else { + const fmark = this.view.state.schema.marks.pFontSize.create({ fontSize }); + this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true); + this.view.focus(); + this.updateMenu(this.view, undefined, this.props); + } } } diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index 427e05edb..8851d52e4 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -348,7 +348,7 @@ export class RichTextRules { this.Document[DataSym].tags = `${tags + "#" + tag + ':'}`; } const fieldView = state.schema.nodes.dashField.create({ fieldKey: "#" + tag }); - return state.tr.deleteRange(start, end).insert(start, fieldView); + return state.tr.deleteRange(start, end).insert(start, fieldView).insertText(" "); }), diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts index 1f6ce014f..2fde5c7ba 100644 --- a/src/client/views/nodes/formattedText/marks_rts.ts +++ b/src/client/views/nodes/formattedText/marks_rts.ts @@ -48,6 +48,20 @@ export const marks: { [index: string]: MarkSpec } = { return ["a", { class: anchorids, "data-targethrefs": targethrefs, "data-linkdoc": node.attrs.linkDoc, title: node.attrs.title, location: node.attrs.location, style: `background: lightBlue` }, 0]; } }, + noAutoLinkAnchor: { + attrs: {}, + inclusive: false, + parseDOM: [{ + tag: "div", getAttrs(dom: any) { + return { + noAutoLink: dom.getAttribute("data-noAutoLink"), + }; + } + }], + toDOM(node: any) { + return ["span", { "data-noAutoLink": "true" }, 0]; + } + }, // :: MarkSpec A linkAnchor. The anchor can have multiple links, where each linkAnchor specifies an href to the URL of the source selection Marker text, // and a title for use in menus and hover. `title` // defaults to the empty string. Rendered and parsed as an `<a>` diff --git a/src/client/views/nodes/trails/PresBox.scss b/src/client/views/nodes/trails/PresBox.scss index 06932d145..a0a2dd4f8 100644 --- a/src/client/views/nodes/trails/PresBox.scss +++ b/src/client/views/nodes/trails/PresBox.scss @@ -1078,11 +1078,13 @@ .miniPres { cursor: grab; position: absolute; - right: 10; - top: 10; + top: 0; + left: 0; opacity: 0.1; transition: all 0.4s; color: $white; + width: 100%; + height: 100%; } .miniPres:hover { @@ -1101,7 +1103,6 @@ align-items: center; display: flex; position: absolute; - right: 10px; transition: all 0.2s; .presPanel-button-text { diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index 30ad43562..e32f2826f 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -12,7 +12,7 @@ import { PrefetchProxy } from "../../../../fields/Proxy"; import { listSpec } from "../../../../fields/Schema"; import { ScriptField } from "../../../../fields/ScriptField"; import { BoolCast, Cast, NumCast, StrCast } from "../../../../fields/Types"; -import { emptyFunction, returnFalse, returnOne, returnTrue } from '../../../../Utils'; +import { emptyFunction, returnFalse, returnOne, returnTrue, setupMoveUpEvents } from '../../../../Utils'; import { Docs } from "../../../documents/Documents"; import { DocumentType } from "../../../documents/DocumentTypes"; import { CurrentUserUtils } from "../../../util/CurrentUserUtils"; @@ -47,17 +47,17 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { * @param renderDoc * @param layoutDoc */ - static renderEffectsDoc(renderDoc: any, layoutDoc: Doc) { + static renderEffectsDoc(renderDoc: any, layoutDoc: Doc, presDoc: Doc) { const effectProps = { - left: layoutDoc.presEffectDirection === PresEffect.Left, - right: layoutDoc.presEffectDirection === PresEffect.Right, - top: layoutDoc.presEffectDirection === PresEffect.Top, - bottom: layoutDoc.presEffectDirection === PresEffect.Bottom, + left: presDoc.presEffectDirection === PresEffect.Left, + right: presDoc.presEffectDirection === PresEffect.Right, + top: presDoc.presEffectDirection === PresEffect.Top, + bottom: presDoc.presEffectDirection === PresEffect.Bottom, opposite: true, - delay: layoutDoc.presTransition, + delay: presDoc.presTransition, // when: this.layoutDoc === PresBox.Instance.childDocs[PresBox.Instance.itemIndex]?.presentationTargetDoc, }; - switch (layoutDoc.presEffect) { + switch (presDoc.presEffect) { case PresEffect.Zoom: return (<Zoom {...effectProps}>{renderDoc}</Zoom>); case PresEffect.Fade: return (<Fade {...effectProps}>{renderDoc}</Fade>); case PresEffect.Flip: return (<Flip {...effectProps}>{renderDoc}</Flip>); @@ -71,7 +71,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } public static EffectsProvider(layoutDoc: Doc, renderDoc: any) { return PresBox.Instance && layoutDoc === PresBox.Instance.childDocs[PresBox.Instance.itemIndex]?.presentationTargetDoc ? - PresBox.renderEffectsDoc(renderDoc, layoutDoc) + PresBox.renderEffectsDoc(renderDoc, layoutDoc, PresBox.Instance.childDocs[PresBox.Instance.itemIndex]) : renderDoc; } @@ -90,13 +90,21 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @observable _expandBoolean: boolean = false; private _disposers: { [name: string]: IReactionDisposer } = {}; + + @observable static startMarquee: boolean = false; // onclick "+ new slide" in presentation mode, set as true, then when marquee selection finish, onPointerUp automatically triggers PinWithView @observable private transitionTools: boolean = false; @observable private newDocumentTools: boolean = false; @observable private progressivizeTools: boolean = false; @observable private openMovementDropdown: boolean = false; @observable private openEffectDropdown: boolean = false; @observable private presentTools: boolean = false; - @computed get childDocs() { return DocListCast(this.rootDoc[this.fieldKey]); } + // @computed get childDocs() { return DocListCast(this.rootDoc[this.fieldKey]); } + // TODO: [AL] add childDocs() and treeMap fields + // @computed get childDocs() { return DocListCast(this.rootDoc.presentationaLinearizedDocuments); } + //_treeViewMap:Map<number[], Doc> + @computed get childDocs() { return this.layoutDoc._viewType === CollectionViewType.Tree ? DocListCast(this.rootDoc.presentationLinearizedDocuments) : DocListCast(this.rootDoc[this.fieldKey]); } + @observable _treeViewMap: Map<number, Doc> = new Map<number, Doc>(); + @computed get tagDocs() { const tagDocs: Doc[] = []; for (const doc of this.childDocs) { @@ -132,6 +140,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { { field: "string", data: Doc.name, container: Doc.name }); } this.props.Document.presentationFieldKey = this.fieldKey; // provide info to the presElement script so that it can look up rendering information about the presBox + } @computed get selectedDocumentView() { if (SelectionManager.Views().length) return SelectionManager.Views()[0]; @@ -212,6 +221,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } //TODO: al: it seems currently that tempMedia doesn't stop onslidechange after clicking the button; the time the tempmedia stop depends on the start & end time + // TODO: to handle child slides (entering into subtrail and exiting), also the next() and back() functions // No more frames in current doc and next slide is defined, therefore move to next slide nextSlide = (activeNext: Doc) => { const targetNext = Cast(activeNext.presentationTargetDoc, Doc, null); @@ -297,6 +307,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.startTempMedia(targetDoc, activeItem); } if (targetDoc) { + Doc.linkFollowHighlight((targetDoc.annotationOn instanceof Doc) ? [targetDoc, targetDoc.annotationOn] : targetDoc); targetDoc && runInAction(() => { if (activeItem.presMovement === PresMovement.Jump) targetDoc.focusSpeed = 0; else targetDoc.focusSpeed = activeItem.presTransition ? activeItem.presTransition : 500; @@ -308,7 +319,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } if (!group) this._selectedArray.clear(); this.childDocs[index] && this._selectedArray.set(this.childDocs[index], undefined); //Update selected array - if ([CollectionViewType.Stacking, CollectionViewType.Tree].includes(this.layoutDoc._viewType as any) && !group) this.navigateToElement(this.childDocs[index]); //Handles movement to element only when presTrail is list + this.navigateToElement(this.childDocs[index]); //Handles movement to element only when presTrail is list this.onHideDocument(); //Handles hide after/before } }); @@ -324,7 +335,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } else if (bestTarget.type === DocumentType.COMPARISON) { bestTarget._clipWidth = activeItem.presPinClipWidth; } else if (bestTarget.type === DocumentType.VID) { - bestTarget._currentTimecode = activeItem.presPinTimecode; + bestTarget._currentTimecode = activeItem.presStartTime; } else { bestTarget._viewTransition = activeItem.presTransition ? `transform ${activeItem.presTransition}ms` : 'all 0.5s'; bestTarget._panX = activeItem.presPinViewX; @@ -346,7 +357,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { navigateToElement = async (curDoc: Doc) => { const activeItem: Doc = this.activeItem; const targetDoc: Doc = this.targetDoc; - const srcContext = Cast(targetDoc.context, Doc, null); + const srcContext = Cast(targetDoc.context, Doc, null) ?? Cast(Cast(targetDoc.annotationOn, Doc, null)?.context, Doc, null); const presCollection = Cast(this.layoutDoc.presCollection, Doc, null); const collectionDocView = presCollection ? DocumentManager.Instance.getDocumentView(presCollection) : undefined; const includesDoc: boolean = DocListCast(presCollection?.data).includes(targetDoc); @@ -374,7 +385,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { self._eleArray.splice(0, self._eleArray.length, ...eleViewCache); }); const openInTab = (doc: Doc, finished?: () => void) => { - collectionDocView ? collectionDocView.props.addDocTab(doc, "") : this.props.addDocTab(doc, ":left"); + collectionDocView ? collectionDocView.props.addDocTab(doc, "") : this.props.addDocTab(doc, ""); this.layoutDoc.presCollection = targetDoc; // this still needs some fixing setTimeout(resetSelection, 500); @@ -387,13 +398,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { // If openDocument is selected then it should open the document for the user if (activeItem.openDocument) { LightboxView.SetLightboxDoc(targetDoc); + // openInTab(targetDoc); } else if (curDoc.presMovement === PresMovement.Pan && targetDoc) { LightboxView.SetLightboxDoc(undefined); - await DocumentManager.Instance.jumpToDocument(targetDoc, false, openInTab, srcContext, undefined, undefined, undefined, includesDoc || tab ? undefined : resetSelection, undefined, true); // documents open in new tab instead of on right + await DocumentManager.Instance.jumpToDocument(targetDoc, false, openInTab, [srcContext], undefined, undefined, undefined, includesDoc || tab ? undefined : resetSelection, undefined, true); // documents open in new tab instead of on right } else if ((curDoc.presMovement === PresMovement.Zoom || curDoc.presMovement === PresMovement.Jump) && targetDoc) { LightboxView.SetLightboxDoc(undefined); //awaiting jump so that new scale can be found, since jumping is async - await DocumentManager.Instance.jumpToDocument(targetDoc, true, openInTab, srcContext, undefined, undefined, undefined, includesDoc || tab ? undefined : resetSelection, undefined, true, NumCast(curDoc.presZoom)); // documents open in new tab instead of on right + await DocumentManager.Instance.jumpToDocument(targetDoc, true, openInTab, [srcContext], undefined, undefined, undefined, includesDoc || tab ? undefined : resetSelection, undefined, true, NumCast(curDoc.presZoom)); // documents open in new tab instead of on right } // 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. @@ -587,27 +599,21 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { * The method called to open the presentation as a minimized view */ @action - updateMinimize = () => { - const docView = DocumentManager.Instance.getDocumentView(this.layoutDoc); + updateMinimize = async () => { if (CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) { - console.log("case 1"); this.layoutDoc.presStatus = PresStatus.Edit; Doc.RemoveDocFromList((Doc.UserDoc().myOverlayDocs as Doc), undefined, this.rootDoc); CollectionDockingView.AddSplit(this.rootDoc, "right"); - } else if ((true || this.layoutDoc.context) && docView) { - console.log("case 2"); + } else { this.layoutDoc.presStatus = PresStatus.Edit; clearTimeout(this._presTimer); const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); this.rootDoc.x = pt[0] + (this.props.PanelWidth() - 250); this.rootDoc.y = pt[1] + 10; - this.rootDoc._height = 35; - this.rootDoc._width = 250; - docView.props.removeDocument?.(this.layoutDoc); + this.rootDoc._height = 30; + this.rootDoc._width = 248; Doc.AddDocToList((Doc.UserDoc().myOverlayDocs as Doc), undefined, this.rootDoc); - } else { - console.log("case 3"); - // TODO glr: fix this case + this.props.removeDocument?.(this.layoutDoc); } } @@ -700,7 +706,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { removeDocument = (doc: Doc) => Doc.RemoveDocFromList(this.rootDoc, this.fieldKey, doc); getTransform = () => this.props.ScreenToLocalTransform().translate(-5, -65);// listBox padding-left and pres-box-cont minHeight panelHeight = () => this.props.PanelHeight() - 40; - isContentActive = (outsideReaction?: boolean) => ((CurrentUserUtils.SelectedTool === InkTool.None && this.props.layerProvider?.(this.layoutDoc) !== false) && + isContentActive = (outsideReaction?: boolean) => ((CurrentUserUtils.SelectedTool === InkTool.None && !this.layoutDoc._lockedPosition) && (this.layoutDoc.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false) /** @@ -1089,7 +1095,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { updateEffectDirection = (effect: any, all?: boolean) => { const array: any[] = all ? this.childDocs : Array.from(this._selectedArray.keys()); array.forEach((doc) => { - const tagDoc = Cast(doc.presentationTargetDoc, Doc, null); + const tagDoc = doc;// Cast(doc.presentationTargetDoc, Doc, null); switch (effect) { case PresEffect.Left: tagDoc.presEffectDirection = PresEffect.Left; @@ -1115,7 +1121,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { updateEffect = (effect: any, all?: boolean) => { const array: any[] = all ? this.childDocs : Array.from(this._selectedArray.keys()); array.forEach((doc) => { - const tagDoc = Cast(doc.presentationTargetDoc, Doc, null); + const tagDoc = doc;//Cast(doc.presentationTargetDoc, Doc, null); switch (effect) { case PresEffect.Bounce: tagDoc.presEffect = PresEffect.Bounce; @@ -1152,7 +1158,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const zoom = activeItem.presZoom ? NumCast(activeItem.presZoom) * 100 : 75; let duration = activeItem.presDuration ? NumCast(activeItem.presDuration) / 1000 : 2; if (activeItem.type === DocumentType.AUDIO) duration = NumCast(activeItem.duration); - const effect = targetDoc.presEffect ? targetDoc.presEffect : 'None'; + const effect = this.activeItem.presEffect ? this.activeItem.presEffect : 'None'; activeItem.presMovement = activeItem.presMovement ? activeItem.presMovement : 'Zoom'; return ( <div className={`presBox-ribbon ${this.transitionTools && this.layoutDoc.presStatus === PresStatus.Edit ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onClick={action(e => { e.stopPropagation(); this.openMovementDropdown = false; this.openEffectDropdown = false; })}> @@ -1204,6 +1210,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="ribbon-property"> <input className="presBox-input" type="number" value={transitionSpeed} + onKeyDown={e => e.stopPropagation()} onChange={action((e) => this.setTransitionTime(e.target.value))} /> s </div> <div className="ribbon-propertyUpDown"> @@ -1243,6 +1250,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="ribbon-property"> <input className="presBox-input" type="number" value={duration} + onKeyDown={e => e.stopPropagation()} onChange={action((e) => this.setDurationTime(e.target.value))} /> s </div> <div className="ribbon-propertyUpDown"> @@ -1274,12 +1282,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { {effect} <FontAwesomeIcon className='presBox-dropdownIcon' style={{ gridColumn: 2, color: this.openEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={"angle-down"} /> <div className={'presBox-dropdownOptions'} id={'presBoxMovementDropdown'} style={{ display: this.openEffectDropdown ? "grid" : "none" }} onPointerDown={e => e.stopPropagation()}> - <div className={`presBox-dropdownOption ${targetDoc.presEffect === PresEffect.None || !targetDoc.presEffect ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.None)}>None</div> - <div className={`presBox-dropdownOption ${targetDoc.presEffect === PresEffect.Fade ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.Fade)}>Fade In</div> - <div className={`presBox-dropdownOption ${targetDoc.presEffect === PresEffect.Flip ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.Flip)}>Flip</div> - <div className={`presBox-dropdownOption ${targetDoc.presEffect === PresEffect.Rotate ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.Rotate)}>Rotate</div> - <div className={`presBox-dropdownOption ${targetDoc.presEffect === PresEffect.Bounce ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.Bounce)}>Bounce</div> - <div className={`presBox-dropdownOption ${targetDoc.presEffect === PresEffect.Roll ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.Roll)}>Roll</div> + <div className={`presBox-dropdownOption ${this.activeItem.presEffect === PresEffect.None || !this.activeItem.presEffect ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.None)}>None</div> + <div className={`presBox-dropdownOption ${this.activeItem.presEffect === PresEffect.Fade ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.Fade)}>Fade In</div> + <div className={`presBox-dropdownOption ${this.activeItem.presEffect === PresEffect.Flip ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.Flip)}>Flip</div> + <div className={`presBox-dropdownOption ${this.activeItem.presEffect === PresEffect.Rotate ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.Rotate)}>Rotate</div> + <div className={`presBox-dropdownOption ${this.activeItem.presEffect === PresEffect.Bounce ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.Bounce)}>Bounce</div> + <div className={`presBox-dropdownOption ${this.activeItem.presEffect === PresEffect.Roll ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.Roll)}>Roll</div> </div> </div> <div className="ribbon-doubleButton" style={{ display: effect === 'None' ? "none" : "inline-flex" }}> @@ -1289,11 +1297,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> </div> <div className="effectDirection" style={{ display: effect === 'None' ? "none" : "grid", width: 40 }}> - <Tooltip title={<><div className="dash-tooltip">{"Enter from left"}</div></>}><div style={{ gridColumn: 1, gridRow: 2, justifySelf: 'center', color: targetDoc.presEffectDirection === PresEffect.Left ? Colors.LIGHT_BLUE : "black", cursor: "pointer" }} onClick={() => this.updateEffectDirection(PresEffect.Left)}><FontAwesomeIcon icon={"angle-right"} /></div></Tooltip> - <Tooltip title={<><div className="dash-tooltip">{"Enter from right"}</div></>}><div style={{ gridColumn: 3, gridRow: 2, justifySelf: 'center', color: targetDoc.presEffectDirection === PresEffect.Right ? Colors.LIGHT_BLUE : "black", cursor: "pointer" }} onClick={() => this.updateEffectDirection(PresEffect.Right)}><FontAwesomeIcon icon={"angle-left"} /></div></Tooltip> - <Tooltip title={<><div className="dash-tooltip">{"Enter from top"}</div></>}><div style={{ gridColumn: 2, gridRow: 1, justifySelf: 'center', color: targetDoc.presEffectDirection === PresEffect.Top ? Colors.LIGHT_BLUE : "black", cursor: "pointer" }} onClick={() => this.updateEffectDirection(PresEffect.Top)}><FontAwesomeIcon icon={"angle-down"} /></div></Tooltip> - <Tooltip title={<><div className="dash-tooltip">{"Enter from bottom"}</div></>}><div style={{ gridColumn: 2, gridRow: 3, justifySelf: 'center', color: targetDoc.presEffectDirection === PresEffect.Bottom ? Colors.LIGHT_BLUE : "black", cursor: "pointer" }} onClick={() => this.updateEffectDirection(PresEffect.Bottom)}><FontAwesomeIcon icon={"angle-up"} /></div></Tooltip> - <Tooltip title={<><div className="dash-tooltip">{"Enter from center"}</div></>}><div style={{ gridColumn: 2, gridRow: 2, width: 10, height: 10, alignSelf: 'center', justifySelf: 'center', border: targetDoc.presEffectDirection === PresEffect.Center || !targetDoc.presEffectDirection ? `solid 2px ${Colors.LIGHT_BLUE}` : "solid 2px black", borderRadius: "100%", cursor: "pointer" }} onClick={() => this.updateEffectDirection(PresEffect.Center)}></div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Enter from left"}</div></>}><div style={{ gridColumn: 1, gridRow: 2, justifySelf: 'center', color: this.activeItem.presEffectDirection === PresEffect.Left ? Colors.LIGHT_BLUE : "black", cursor: "pointer" }} onClick={() => this.updateEffectDirection(PresEffect.Left)}><FontAwesomeIcon icon={"angle-right"} /></div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Enter from right"}</div></>}><div style={{ gridColumn: 3, gridRow: 2, justifySelf: 'center', color: this.activeItem.presEffectDirection === PresEffect.Right ? Colors.LIGHT_BLUE : "black", cursor: "pointer" }} onClick={() => this.updateEffectDirection(PresEffect.Right)}><FontAwesomeIcon icon={"angle-left"} /></div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Enter from top"}</div></>}><div style={{ gridColumn: 2, gridRow: 1, justifySelf: 'center', color: this.activeItem.presEffectDirection === PresEffect.Top ? Colors.LIGHT_BLUE : "black", cursor: "pointer" }} onClick={() => this.updateEffectDirection(PresEffect.Top)}><FontAwesomeIcon icon={"angle-down"} /></div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Enter from bottom"}</div></>}><div style={{ gridColumn: 2, gridRow: 3, justifySelf: 'center', color: this.activeItem.presEffectDirection === PresEffect.Bottom ? Colors.LIGHT_BLUE : "black", cursor: "pointer" }} onClick={() => this.updateEffectDirection(PresEffect.Bottom)}><FontAwesomeIcon icon={"angle-up"} /></div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Enter from center"}</div></>}><div style={{ gridColumn: 2, gridRow: 2, width: 10, height: 10, alignSelf: 'center', justifySelf: 'center', border: this.activeItem.presEffectDirection === PresEffect.Center || !this.activeItem.presEffectDirection ? `solid 2px ${Colors.LIGHT_BLUE}` : "solid 2px black", borderRadius: "100%", cursor: "pointer" }} onClick={() => this.updateEffectDirection(PresEffect.Center)}></div></Tooltip> </div> </div>} <div className="ribbon-final-box"> @@ -1308,7 +1316,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @computed get effectDirection(): string { let effect = ''; - switch (this.targetDoc.presEffectDirection) { + switch (this.activeItem.presEffectDirection) { case 'left': effect = "Enter from left"; break; case 'right': effect = "Enter from right"; break; case 'top': effect = "Enter from top"; break; @@ -1324,8 +1332,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const activeItem: Doc = this.activeItem; const targetDoc: Doc = this.targetDoc; this.updateMovement(activeItem.presMovement, true); - this.updateEffect(targetDoc.presEffect, true); - this.updateEffectDirection(targetDoc.presEffectDirection, true); + this.updateEffect(activeItem.presEffect, true); + this.updateEffectDirection(activeItem.presEffectDirection, true); array.forEach((doc) => { const curDoc = Cast(doc, Doc, null); const tagDoc = Cast(curDoc.presentationTargetDoc, Doc, null); @@ -1356,7 +1364,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { activeItem.presPinView = true; activeItem.presPinViewScroll = scroll; } else if (targetDoc.type === DocumentType.VID) { - activeItem.presPinTimecode = targetDoc._currentTimecode; + activeItem.presStartTime = targetDoc._currentTimecode; + activeItem.presEndTime = NumCast(targetDoc._currentTimecode) + 0.1; } else if ((targetDoc.type === DocumentType.COL && targetDoc._viewType === CollectionViewType.Freeform) || targetDoc.type === DocumentType.IMG) { const x = targetDoc._panX; const y = targetDoc._panY; @@ -1378,7 +1387,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const scroll = targetDoc._scrollTop; activeItem.presPinViewScroll = scroll; } else if (targetDoc.type === DocumentType.VID) { - activeItem.presPinTimecode = targetDoc._currentTimecode; + activeItem.presStartTime = targetDoc._currentTimecode; + activeItem.presStartTime = NumCast(targetDoc._currentTimecode) + 0.1; } else if (targetDoc.type === DocumentType.COMPARISON) { const clipWidth = targetDoc._clipWidth; activeItem.presPinClipWidth = clipWidth; @@ -1408,6 +1418,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <input className="presBox-input" style={{ textAlign: 'left', width: 50 }} type="number" value={NumCast(activeItem.presPinViewX)} + onKeyDown={e => e.stopPropagation()} onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { const val = e.target.value; activeItem.presPinViewX = Number(val); })} /> </div> </div> @@ -1417,6 +1428,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <input className="presBox-input" style={{ textAlign: 'left', width: 50 }} type="number" value={NumCast(activeItem.presPinViewY)} + onKeyDown={e => e.stopPropagation()} onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { const val = e.target.value; activeItem.presPinViewY = Number(val); })} /> </div> </div> @@ -1426,6 +1438,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <input className="presBox-input" style={{ textAlign: 'left', width: 50 }} type="number" value={NumCast(activeItem.presPinViewScale)} + onKeyDown={e => e.stopPropagation()} onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { const val = e.target.value; activeItem.presPinViewScale = Number(val); })} /> </div> </div> @@ -1446,6 +1459,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <input className="presBox-input" style={{ textAlign: 'left', width: 50 }} type="number" value={NumCast(activeItem.presPinViewScroll)} + onKeyDown={e => e.stopPropagation()} onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { const val = e.target.value; activeItem.presPinViewScroll = Number(val); })} /> </div> </div> @@ -1492,6 +1506,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <input className="presBox-input" style={{ textAlign: 'center', width: 30, height: 15, fontSize: 10 }} type="number" value={NumCast(activeItem.presStartTime)} + onKeyDown={e => e.stopPropagation()} onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { activeItem.presStartTime = Number(e.target.value); })} /> </div> @@ -1510,6 +1525,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> <div id={"endTime"} className="slider-number" style={{ backgroundColor: Colors.LIGHT_GRAY }}> <input className="presBox-input" + onKeyDown={e => e.stopPropagation()} style={{ textAlign: 'center', width: 30, height: 15, fontSize: 10 }} type="number" value={NumCast(activeItem.presEndTime)} onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { activeItem.presEndTime = Number(e.target.value); })} @@ -2403,6 +2419,63 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { else this.pauseAutoPres(); } + @action + prevClicked = (e: PointerEvent) => { + this.back(); + if (this._presTimer) { + clearTimeout(this._presTimer); + this.layoutDoc.presStatus = PresStatus.Manual; + } + } + + @action + nextClicked = (e: PointerEvent) => { + this.next(); + if (this._presTimer) { + clearTimeout(this._presTimer); + this.layoutDoc.presStatus = PresStatus.Manual; + } + } + @undoBatch + @action + exitClicked = (e: PointerEvent) => { + this.updateMinimize(); + this.layoutDoc.presStatus = PresStatus.Edit; + clearTimeout(this._presTimer); + } + + @action + startMarqueeCreateSlide = () => { + PresBox.startMarquee = true; + } + + AddToMap = (treeViewDoc: Doc, index: number[]): Doc[] => { + var indexNum = 0; + for (let i = 0; i < index.length; i++) { + indexNum += (index[i] * (10 ** (-i))); + } + this._treeViewMap.set(indexNum, treeViewDoc); + this.props.Document.presentationLinearizedDocuments = new List<Doc>(this.sort(this._treeViewMap)); // this is a flat array of Docs + return this.childDocs; + } + + RemFromMap = (treeViewDoc: Doc, index: number[]): Doc[] => { + var indexNum = 0; + for (let i = 0; i < index.length; i++) { + indexNum += (index[i] * (10 ** (-i))); + } + console.log(String(index), treeViewDoc) + this._treeViewMap.delete(indexNum); + const overlay = (Doc.UserDoc().myOverlayDocs as Doc); + if (!DocListCast(overlay[Doc.LayoutFieldKey(overlay)]).includes(this.rootDoc)) { + this.props.Document.presentationLinearizedDocuments = new List<Doc>(this.sort(this._treeViewMap)); + } + return this.childDocs; + } + + // TODO: [AL] implement sort function for an array of numbers (e.g. arr[1,2,4] v arr[1,2,1]) + sort = (treeViewMap: Map<number, Doc>) => [...treeViewMap.entries()].sort().map(kv => kv[1]); + render() { // calling this method for keyEvents this.isPres; @@ -2415,54 +2488,74 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { return CurrentUserUtils.OverlayDocs.includes(this.rootDoc) ? <div className="miniPres"> <div className="presPanelOverlay" style={{ display: "inline-flex", height: 30, background: '#323232', top: 0, zIndex: 3000000, boxShadow: presKeyEvents ? '0 0 0px 3px ' + Colors.MEDIUM_BLUE : undefined }}> - <Tooltip title={<><div className="dash-tooltip">{"Loop"}</div></>}><div className="presPanel-button" style={{ color: this.layoutDoc.presLoop ? Colors.MEDIUM_BLUE : undefined }} onClick={() => this.layoutDoc.presLoop = !this.layoutDoc.presLoop}><FontAwesomeIcon icon={"redo-alt"} /></div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Loop"}</div></>}><div className="presPanel-button" style={{ color: this.layoutDoc.presLoop ? Colors.MEDIUM_BLUE : undefined }} + onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, () => this.layoutDoc.presLoop = !this.layoutDoc.presLoop, false, false)}><FontAwesomeIcon icon={"redo-alt"} /></div></Tooltip> <div className="presPanel-divider"></div> - <div className="presPanel-button" style={{ opacity: presStart ? 0.4 : 1 }} onClick={() => { this.back(); if (this._presTimer) { clearTimeout(this._presTimer); this.layoutDoc.presStatus = PresStatus.Manual; } }}><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}><FontAwesomeIcon icon={this.layoutDoc.presStatus === "auto" ? "pause" : "play"} /></div></Tooltip> - <div className="presPanel-button" style={{ opacity: presEnd ? 0.4 : 1 }} onClick={() => { this.next(); if (this._presTimer) { clearTimeout(this._presTimer); this.layoutDoc.presStatus = PresStatus.Manual; } }}><FontAwesomeIcon icon={"arrow-right"} /></div> + <div className="presPanel-button" style={{ opacity: presStart ? 0.4 : 1 }} + onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, this.prevClicked, false, false)}><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)}><FontAwesomeIcon icon={this.layoutDoc.presStatus === "auto" ? "pause" : "play"} /></div></Tooltip> + <div className="presPanel-button" style={{ opacity: presEnd ? 0.4 : 1 }} + onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, this.nextClicked, false, false)}><FontAwesomeIcon icon={"arrow-right"} /></div> <div className="presPanel-divider"></div> - <Tooltip title={<><div className="dash-tooltip">{"Click to return to 1st slide"}</div></>}><div className="presPanel-button" style={{ border: 'solid 1px white' }} onClick={() => this.gotoDocument(0, this.activeItem)}><b>1</b></div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Click to return to 1st slide"}</div></>}><div className="presPanel-button" style={{ border: 'solid 1px white' }} + onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, () => this.gotoDocument(0, this.activeItem), false, false)}><b>1</b></div></Tooltip> <div className="presPanel-button-text"> Slide {this.itemIndex + 1} / {this.childDocs.length} {this.playButtonFrames} </div> - <div className="presPanel-divider"></div> - <div className="presPanel-button-text" onClick={undoBatch(action(() => { this.updateMinimize(); this.layoutDoc.presStatus = PresStatus.Edit; clearTimeout(this._presTimer); }))}>EXIT</div> + <div className="presPanel-divider" /> + <div className="presPanel-button-text" onPointerDown={e => + setupMoveUpEvents(this, e, returnFalse, returnFalse, this.exitClicked, false, false)}>EXIT</div> </div> - </div> + </div > : <div className="presBox-cont" style={{ minWidth: CurrentUserUtils.OverlayDocs.includes(this.layoutDoc) ? 240 : undefined }} > {this.topPanel} {this.toolbar} {this.newDocumentToolbarDropdown} <div className="presBox-listCont"> - {mode !== CollectionViewType.Invalid ? - <CollectionView {...this.props} - ContainingCollectionDoc={this.props.Document} - PanelWidth={this.props.PanelWidth} - PanelHeight={this.panelHeight} - childIgnoreNativeSize={true} - moveDocument={returnFalse} - childFitWidth={returnTrue} - childOpacity={returnOne} - childLayoutTemplate={this.childLayoutTemplate} - filterAddDocument={this.addDocumentFilter} - removeDocument={returnFalse} - dontRegisterView={true} - focus={this.selectElement} - ScreenToLocalTransform={this.getTransform} - /> - : (null) + <div className="Slide" style={{ height: `calc(100% - 30px)` }}> + {mode !== CollectionViewType.Invalid ? + <CollectionView {...this.props} + ContainingCollectionDoc={this.props.Document} + PanelWidth={this.props.PanelWidth} + PanelHeight={this.panelHeight} + childIgnoreNativeSize={true} + moveDocument={returnFalse} + childFitWidth={returnTrue} + childOpacity={returnOne} + childLayoutTemplate={this.childLayoutTemplate} + filterAddDocument={this.addDocumentFilter} + removeDocument={returnFalse} + dontRegisterView={true} + focus={this.selectElement} + ScreenToLocalTransform={this.getTransform} + // TODO: [AL] implement AddToMap, RemFromMap functions (outside this) + AddToMap={this.AddToMap} + RemFromMap={this.RemFromMap} + hierarchyIndex={[]} + /> : (null) + } + </div> + + { // if the document type is a presentation, then the collection stacking view has a "+ new slide" button at the bottom of the stack + <Tooltip title={<div className="dash-tooltip">{"Click on document to pin to presentaiton or make a marquee selection to pin your desired view"}</div>}> + <button className="add-slide-button" onPointerDown={this.startMarqueeCreateSlide}> + + NEW SLIDE + </button> + </Tooltip> } </div> </div>; } } -ScriptingGlobals.add(function lookupPresBoxField(container: Doc, field: string, data: Doc) { - if (field === 'indexInPres') return DocListCast(container[StrCast(container.presentationFieldKey)]).indexOf(data); - if (field === 'presCollapsedHeight') return [CollectionViewType.Tree || CollectionViewType.Stacking].includes(container._viewType as any) ? 35 : 31; - if (field === 'presStatus') return container.presStatus; - if (field === '_itemIndex') return container._itemIndex; - if (field === 'presBox') return container; +// this func communicates with PresBoxElement to send information of the doc +ScriptingGlobals.add(function lookupPresBoxField(presBoxDoc: Doc, field: string, presEleDoc: Doc) { + if (field === 'indexInPres') return DocListCast(presBoxDoc._viewType === CollectionViewType.Tree ? presBoxDoc.presentationLinearizedDocuments : presBoxDoc[StrCast(presBoxDoc.presentationFieldKey)]).indexOf(presEleDoc); + if (field === 'presCollapsedHeight') return [CollectionViewType.Tree || CollectionViewType.Stacking].includes(presBoxDoc._viewType as any) ? 35 : 31; + if (field === 'presStatus') return presBoxDoc.presStatus; + if (field === '_itemIndex') return presBoxDoc._itemIndex; + if (field === 'presBox') return presBoxDoc; return undefined; });
\ No newline at end of file diff --git a/src/client/views/nodes/trails/PresElementBox.scss b/src/client/views/nodes/trails/PresElementBox.scss index a178be910..789a6d9f3 100644 --- a/src/client/views/nodes/trails/PresElementBox.scss +++ b/src/client/views/nodes/trails/PresElementBox.scss @@ -195,6 +195,47 @@ $slide-active: #5B9FDD; box-shadow: 0 0 0px 1.5px $dark-blue; } +.presItem-slide.group { + border-radius: 5px; +} + +.presItem-slide.activeGroup { + border-radius: 5px; + box-shadow: 0 0 0px 1.5px $dark-blue; +} + +.presItem-groupSlideContainer { + position: absolute; + /* grid-row: 3; */ + /* grid-column: 1/8; */ + top: 28; + display: block; + background: #92adb9; + width: 100%; + height: 10px; + border-radius: 0px 0px 5px 5px; + height: calc(100% - 28px); + display: grid; + grid-template-rows: auto auto auto; + grid-template-columns: 100%; + justify-items: left; + align-items: center; +} + +.presItem-groupSlide { + position: relative; + background-color: #d5dce2; + border-radius: 5px; + height: calc(100% - 7px); + width: calc(100% - 20px); + left: 15px; + /* height: 20px; */ + /* width: calc(100% - 19px); */ + display: flex; + grid-template-rows: 16px 10px auto; + grid-template-columns: max-content max-content max-content max-content auto; +} + .presItem-multiDrag { font-family: Roboto; font-weight: 600; @@ -230,6 +271,37 @@ $slide-active: #5B9FDD; box-shadow: 0 0 0px 1.5px $dark-blue; } -// .presItem-slide:hover { -// background: $slide-hover; -// }
\ No newline at end of file +.expandButton { + cursor: pointer; + position: absolute; + border-radius: 100px; + bottom: 0; + left: -18; + z-index: 300; + width: 15px; + height: 15px; + display: flex; + font-size: 12px; + justify-self: center; + align-self: center; + background-color: #92adb9; + color: white; + justify-content: center; + align-items: center; + transition: 0.2s; + margin-right: 3px; +} + +.expandButton:hover { + background-color: rgba(0, 0, 0, 1); + transform: scale(1.2); +} + +.presItem-groupNum { + color: #d5dce2; + position: absolute; + left: -15px; + top: 1; + font-weight: 600; + font-size: 12; +}
\ No newline at end of file diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx index f4dc9b615..bbabcb2de 100644 --- a/src/client/views/nodes/trails/PresElementBox.tsx +++ b/src/client/views/nodes/trails/PresElementBox.tsx @@ -2,9 +2,9 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Tooltip } from "@material-ui/core"; import { action, computed, IReactionDisposer, observable, reaction } from "mobx"; import { observer } from "mobx-react"; -import { DataSym, Doc, Opt } from "../../../../fields/Doc"; +import { DataSym, Doc, DocListCast, Opt } from "../../../../fields/Doc"; import { Id } from "../../../../fields/FieldSymbols"; -import { Cast, NumCast, StrCast } from "../../../../fields/Types"; +import { BoolCast, Cast, NumCast, StrCast } from "../../../../fields/Types"; import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from "../../../../Utils"; import { DocUtils } from "../../../documents/Documents"; import { DocumentType } from "../../../documents/DocumentTypes"; @@ -80,7 +80,6 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { Document={this.targetDoc} DataDoc={this.targetDoc[DataSym] !== this.targetDoc && this.targetDoc[DataSym]} styleProvider={this.styleProvider} - layerProvider={this.props.layerProvider} docViewPath={returnEmptyDoclist} rootSelected={returnTrue} addDocument={returnFalse} @@ -108,6 +107,39 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { </div>; } + @computed get renderGroupSlides() { + const childDocs = DocListCast(this.targetDoc.data); + const groupSlides = childDocs.map((doc: Doc, ind: number) => + <div className="presItem-groupSlide" onClick={(e) => { + console.log("Clicked on slide with index: ", ind); + e.stopPropagation(); + e.preventDefault(); + PresBox.Instance.modifierSelect(doc, this._itemRef.current!, this._dragRef.current!, !e.shiftKey && !e.ctrlKey && !e.metaKey, e.ctrlKey || e.metaKey, e.shiftKey); + 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} + editing={undefined} + contents={doc.title} + overflow={'ellipsis'} + GetValue={() => StrCast(doc.title)} + SetValue={(value: string) => { + doc.title = !value.trim().length ? "-untitled-" : value; + return true; + }} + /> + </div> + </div> + ); + return ( + groupSlides + ); + } @computed get duration() { let durationInS: number; if (this.rootDoc.type === DocumentType.AUDIO || this.rootDoc.type === DocumentType.VID) { durationInS = NumCast(this.rootDoc.presEndTime) - NumCast(this.rootDoc.presStartTime); durationInS = Math.round(durationInS * 10) / 10; } @@ -290,6 +322,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { const presColorBool: boolean = presBoxColor ? (presBoxColor !== Colors.WHITE && presBoxColor !== "transparent") : false; const targetDoc: Doc = this.targetDoc; const activeItem: Doc = this.rootDoc; + const isGroup: boolean = BoolCast(targetDoc._isGroup); return ( <div className={`presItem-container`} key={this.props.Document[Id] + this.indexInPres} @@ -304,8 +337,12 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { PresBox.Instance.modifierSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, !e.shiftKey && !e.ctrlKey && !e.metaKey, e.ctrlKey || e.metaKey, e.shiftKey); }} onDoubleClick={action(e => { - this.toggleProperties(); - PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, true); + if (isGroup) { + this.rootDoc.presExpandGroup = !this.rootDoc.presExpandGroup; + } else { + this.toggleProperties(); + PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, true); + } })} onPointerOver={this.onPointerOver} onPointerLeave={this.onPointerLeave} @@ -322,7 +359,69 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="presItem-number"> {`${this.indexInPres + 1}.`} </div>} - {miniView ? (null) : <div ref={miniView ? null : this._dragRef} className={`presItem-slide ${isSelected ? "active" : ""}`} + {isGroup ? + <div ref={miniView ? null : this._dragRef} className={`presItem-slide ${isSelected ? "activeGroup" : "group"}`} + style={{ + backgroundColor: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor), + boxShadow: presBoxColor && presBoxColor !== "white" && presBoxColor !== "transparent" ? isSelected ? "0 0 0px 1.5px" + presBoxColor : undefined : undefined + }}> + <div className="presItem-name" style={{ maxWidth: showMore ? (toolbarWidth - 195) : toolbarWidth - 105, cursor: isSelected ? 'text' : 'grab' }}> + <EditableView + ref={this._titleRef} + editing={!isSelected ? false : undefined} + contents={activeItem.title} + overflow={'ellipsis'} + GetValue={() => StrCast(activeItem.title)} + SetValue={this.onSetValue} + /> + </div> + <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> + {this.indexInPres === 0 ? (null) : <Tooltip 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>} + <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="group"></div> + </div> + <div className="presItem-groupSlideContainer" style={{ top: 28, height: 'calc(100% - 28px)' }}> + {this.rootDoc.presExpandGroup ? this.renderGroupSlides : (null)} + </div> + <div className="presItem-docName" style={{ maxWidth: showMore ? (toolbarWidth - 195) : toolbarWidth - 105 }}>{activeItem.presPinView ? (<><i>View of </i> {targetDoc.title}</>) : targetDoc.title}</div> + <div className="expandButton" + onClick={e => { + if (isGroup) { + this.rootDoc.presExpandGroup = !this.rootDoc.presExpandGroup; + } + }} + > + <FontAwesomeIcon icon={"caret-down"} style={{ transform: this.rootDoc.presExpandGroup ? "rotate(180deg)" : "rotate(0deg)" }} /> + </div> + </div> + : (null)} + {miniView || isGroup ? (null) : <div ref={miniView ? null : this._dragRef} className={`presItem-slide ${isSelected ? "active" : ""}`} style={{ backgroundColor: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor), boxShadow: presBoxColor && presBoxColor !== "white" && presBoxColor !== "transparent" ? isSelected ? "0 0 0px 1.5px" + presBoxColor : undefined : undefined |
