diff options
author | srichman333 <sarah_n_richman@brown.edu> | 2023-11-06 18:36:58 -0500 |
---|---|---|
committer | srichman333 <sarah_n_richman@brown.edu> | 2023-11-06 18:36:58 -0500 |
commit | 1b412d402c77a2aae82cf86b1f6a23f8a4f82caf (patch) | |
tree | 7ebd22eeade12099d1d891d9f9b264f02956ad4a /src | |
parent | 7163062edec37cef9dd9ae6c123d987e83837463 (diff) | |
parent | a4e3b645317c4589cf49f8007f6e6b57cf2c12d3 (diff) |
Merge branch 'master' into dataViz-annotations
Diffstat (limited to 'src')
61 files changed, 1465 insertions, 1186 deletions
diff --git a/src/Utils.ts b/src/Utils.ts index b5ca53a33..330ca59f9 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -555,7 +555,7 @@ export function returnAll(): 'all' { return 'all'; } -export function returnNone() { +export function returnNone(): 'none' { return 'none'; } @@ -758,7 +758,7 @@ export function DashColor(color: string) { } export function lightOrDark(color: any) { - if (color === 'transparent') return Colors.DARK_GRAY; + if (color === 'transparent' || !color) return Colors.DARK_GRAY; if (color.startsWith?.('linear')) return Colors.BLACK; const nonAlphaColor = color.startsWith('#') ? (color as string).substring(0, 7) : color.startsWith('rgba') ? color.replace(/,.[^,]*\)/, ')').replace('rgba', 'rgb') : color; const col = DashColor(nonAlphaColor).rgb(); diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 11b5f9f08..4086ede20 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -215,6 +215,7 @@ export class DocumentOptions { _lockedTransform?: BOOLt = new BoolInfo('lock the freeform_panx,freeform_pany and scale parameters of the document so that it be panned/zoomed'); layout?: string | Doc; // default layout string or template document + layout_isSvg?: BOOLt = new BoolInfo('whether document decorations and other selections should handle pointerEvents for svg content or use doc bounding box'); layout_keyValue?: STRt = new StrInfo('layout definition for showing keyValue view of document', false); layout_explainer?: STRt = new StrInfo('explanation displayed at top of a collection to describe its purpose', false); layout_headerButton?: DOCt = new DocInfo('the (button) Doc to display at the top of a collection.', false); @@ -443,6 +444,8 @@ export class DocumentOptions { userBackgroundColor?: STRt = new StrInfo('background color associated with a Dash user (seen in header fields of shared documents)'); userColor?: STRt = new StrInfo('color associated with a Dash user (seen in header fields of shared documents)'); } + +export const DocOptions = new DocumentOptions(); export namespace Docs { export let newAccount: boolean = false; @@ -569,10 +572,10 @@ export namespace Docs { childDontRegisterViews: true, onClick: FollowLinkScript(), layout_hideLinkAnchors: true, - _height: 150, + _height: 1, + _width: 1, link: '', link_description: '', - layout_showCaption: 'link_description', backgroundColor: 'lightblue', // lightblue is default color for linking dot and link documents text comment area _dropPropertiesToRemove: new List(['onClick']), }, @@ -667,7 +670,15 @@ export namespace Docs { { // NOTE: this is unused!! ink fields are filled in directly within the InkDocument() method layout: { view: InkingStroke, dataField: 'stroke' }, - options: { systemIcon: 'BsFillPencilFill' }, + options: { + systemIcon: 'BsFillPencilFill', // + nativeDimModifiable: true, + nativeHeightUnfrozen: true, + layout_hideDecorationTitle: true, // don't show title when selected + fitWidth: false, + layout_isSvg: true, + layout_forceReflow: true, + }, }, ], [ @@ -770,7 +781,7 @@ export namespace Docs { const existing = actualProtos[id] as Doc; const type = id.replace(suffix, '') as DocumentType; // get or create prototype of the specified type... - const target = existing || buildPrototype(type, id); + const target = buildPrototype(type, id, existing); // ...and set it if not undefined (can be undefined only if TemplateMap does not contain // an entry dedicated to the given DocumentType) target && PrototypeMap.set(type, target); @@ -818,7 +829,7 @@ export namespace Docs { * @param options any value specified in the DocumentOptions object likewise * becomes the default value for that key for all delegates */ - function buildPrototype(type: DocumentType, prototypeId: string): Opt<Doc> { + function buildPrototype(type: DocumentType, prototypeId: string, existing?: Doc): Opt<Doc> { // load template from type const template = TemplateMap.get(type); if (!template) { @@ -844,12 +855,14 @@ export namespace Docs { layout: layout.view?.LayoutString(layout.dataField), data: template.data, }; - Object.entries(options).map(pair => { - if (typeof pair[1] === 'string' && pair[1].startsWith('@')) { - (options as any)[pair[0]] = ComputedField.MakeFunction(pair[1].substring(1)); - } - }); - return Doc.assign(new Doc(prototypeId, true), options as any, undefined, true); + Object.entries(options) + .filter(pair => typeof pair[1] === 'string' && pair[1].startsWith('@')) + .map(pair => { + if (!existing || ScriptCast(existing[pair[0]])?.script.originalScript !== pair[1].substring(1)) { + (options as any)[pair[0]] = ComputedField.MakeFunction(pair[1].substring(1)); + } + }); + return Doc.assign(existing ?? new Doc(prototypeId, true), OmitKeys(options, Object.keys(existing ?? {})).omit, undefined, true); } } @@ -1030,12 +1043,8 @@ export namespace Docs { } export function InkDocument(color: string, strokeWidth: number, stroke_bezier: string, fillColor: string, arrowStart: string, arrowEnd: string, dash: string, points: PointData[], isInkMask: boolean, options: DocumentOptions = {}) { - const I = new Doc(); - I[Initializing] = true; - I.type = DocumentType.INK; - I.layout = InkingStroke.LayoutString('stroke'); - I.layout_fitWidth = false; - I.layout_hideDecorationTitle = true; // don't show title when selected + const ink = InstanceFromProto(Prototypes.get(DocumentType.INK), '', { title: 'ink', ...options }); + const I = Doc.GetProto(ink); // I.layout_hideOpenButton = true; // don't show open full screen button when selected I.color = color; I.fillColor = fillColor; @@ -1047,12 +1056,6 @@ export namespace Docs { I.stroke_dash = dash; I.stroke_isInkMask = isInkMask; I.text_align = 'center'; - I.title = 'ink'; - I.x = options.x as number; - I.y = options.y as number; - I._width = options._width as number; - I._height = options._height as number; - I.author = Doc.CurrentUserEmail; I.rotation = 0; I.defaultDoubleClick = 'click'; I.author_date = new DateField(); @@ -1060,7 +1063,7 @@ export namespace Docs { //I['acl-Override'] = SharingPermissions.Unset; I[Initializing] = false; - return InstanceFromProto(I, '', options); + return ink; } export function PdfDocument(url: string, options: DocumentOptions = {}, overwriteDoc?: Doc) { @@ -1402,8 +1405,10 @@ export namespace DocUtils { link_relationship: linkSettings.link_relationship, link_description: linkSettings.link_description, link_autoMoveAnchors: true, - _layout_showCaption: 'link_description', - _layout_showTitle: 'link_relationship', + _layout_showCaption: '', // removed since they conflict with showing a link with a LinkBox (ie, line, not comparison box) + _layout_showTitle: '', + // _layout_showCaption: 'link_description', + // _layout_showTitle: 'link_relationship', }, id ), diff --git a/src/client/goldenLayout.js b/src/client/goldenLayout.js index 588bf57d1..e789d8e20 100644 --- a/src/client/goldenLayout.js +++ b/src/client/goldenLayout.js @@ -3987,7 +3987,7 @@ else variableItemCount++; } - newItemSize = (1 / variableItemCount) * (100 - fixedItemSize); + newItemSize = (1 / (variableItemCount+1)) * (100 - fixedItemSize); if (_$suspendResize === true) { this.emitBubblingEvent('stateChanged'); diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index cc8f72ddf..87ee1b252 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -635,9 +635,9 @@ export class CurrentUserUtils { return [ { title: "Snap", icon: "th", toolTip: "Show Snap Lines", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"snaplines", funcs: {}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform { title: "Grid", icon: "border-all", toolTip: "Show Grid", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"grid", funcs: {}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform - { title: "View All", icon: "object-group", toolTip: "Fit all Docs to View",btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"viewAll", funcs: {}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform + { title: "View All", icon: "object-group", toolTip: "Keep all Docs in View",btnType: ButtonType.ToggleButton, ignoreClick:true, expertMode: false, toolType:"viewAll", funcs: {}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform // want the same style as toggle button, but don't want it to act as an actual toggle, so set disableToggle to true, - { title: "Fit All", icon: "arrows-left-right", toolTip: "Fit all Docs to View (persistent)", btnType: ButtonType.ClickButton, ignoreClick: false, expertMode: false, toolType:"viewAllPersist", funcs: {}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform + { title: "Fit All", icon: "arrows-left-right", toolTip: "Fit Docs to View (once)",btnType: ButtonType.ClickButton,ignoreClick:false,expertMode: false, toolType:"fitOnce", funcs: {}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform { title: "Clusters", icon: "braille", toolTip: "Show Doc Clusters", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"clusters", funcs: {}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform { title: "Cards", icon: "brain", toolTip: "Flashcards", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"flashcards", funcs: {}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform { title: "Arrange", icon:"arrow-down-short-wide",toolTip:"Toggle Auto Arrange", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"arrange", funcs: {hidden: 'IsNoviceMode()'}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform @@ -659,7 +659,7 @@ export class CurrentUserUtils { subMenu: [ { title: "Left", toolTip: "Left align (Cmd-[)", btnType: ButtonType.ToggleButton, icon: "align-left", toolType:"left", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}' }}, { title: "Center", toolTip: "Center align (Cmd-\\)",btnType: ButtonType.ToggleButton, icon: "align-center",toolType:"center",ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, - { title: "Right", toolTip: "Right align (Cmd-])", btnType: ButtonType.ToggleButton, icon: "align-right", toolType:"right", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, + { title: "Right", toolTip: "Right align (Cmd-])", btnType: ButtonType.ToggleButton, icon: "align-right", toolType:"right", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, ] }, { title: "Dictate", toolTip: "Dictate", btnType: ButtonType.ToggleButton, icon: "microphone", toolType:"dictation", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'}}, @@ -673,12 +673,12 @@ export class CurrentUserUtils { static inkTools():Button[] { return [ { title: "Pen", toolTip: "Pen (Ctrl+P)", btnType: ButtonType.ToggleButton, icon: "pen-nib", toolType: "pen", scripts: {onClick:'{ return setActiveTool(self.toolType, false, _readOnly_);}' }}, - { title: "Write", toolTip: "Write (Ctrl+Shift+P)", btnType: ButtonType.ToggleButton, icon: "pen", toolType: "write", scripts: {onClick:'{ return setActiveTool(self.toolType, false, _readOnly_);}'} }, + { title: "Write", toolTip: "Write (Ctrl+Shift+P)", btnType: ButtonType.ToggleButton, icon: "pen", toolType: "write", scripts: {onClick:'{ return setActiveTool(self.toolType, false, _readOnly_);}' }, funcs: {hidden:"IsNoviceMode()" }}, { title: "Eraser", toolTip: "Eraser (Ctrl+E)", btnType: ButtonType.ToggleButton, icon: "eraser", toolType: "eraser", scripts: {onClick:'{ return setActiveTool(self.toolType, false, _readOnly_);}' }, funcs: {hidden:"IsNoviceMode()" }}, { title: "Circle", toolTip: "Circle (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "circle", toolType:GestureUtils.Gestures.Circle, scripts: {onClick:`{ return setActiveTool(self.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(self.toolType, true, _readOnly_);}`} }, { title: "Square", toolTip: "Square (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "square", toolType:GestureUtils.Gestures.Rectangle, scripts: {onClick:`{ return setActiveTool(self.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(self.toolType, true, _readOnly_);}`} }, { title: "Line", toolTip: "Line (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "minus", toolType:GestureUtils.Gestures.Line, scripts: {onClick:`{ return setActiveTool(self.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(self.toolType, true, _readOnly_);}`} }, - { title: "Mask", toolTip: "Mask", btnType: ButtonType.ToggleButton, icon: "user-circle",toolType: "inkMask", scripts: {onClick:'{ return setInkProperty(self.toolType, value, _readOnly_);}'} }, + { title: "Mask", toolTip: "Mask", btnType: ButtonType.ToggleButton, icon: "user-circle",toolType: "inkMask", scripts: {onClick:'{ return setInkProperty(self.toolType, value, _readOnly_);}'}, funcs: {hidden:"IsNoviceMode()" } }, { title: "Width", toolTip: "Stroke width", btnType: ButtonType.NumberSliderButton, toolType: "strokeWidth", ignoreClick: true, scripts: {script: '{ return setInkProperty(self.toolType, value, _readOnly_);}'}, numBtnMin: 1}, { title: "Ink", toolTip: "Stroke color", btnType: ButtonType.ColorButton, icon: "pen", toolType: "strokeColor", ignoreClick: true, scripts: {script: '{ return setInkProperty(self.toolType, value, _readOnly_);}'} }, ]; @@ -707,8 +707,8 @@ export class CurrentUserUtils { CollectionViewType.Grid, CollectionViewType.NoteTaking]), title: "Perspective", toolTip: "View", btnType: ButtonType.DropdownList, ignoreClick: true, width: 100, scripts: { script: 'setView(value, _readOnly_)'}}, { title: "Pin", icon: "map-pin", toolTip: "Pin View to Trail", btnType: ButtonType.ClickButton, expertMode: false, width: 30, scripts: { onClick: 'pinWithView(altKey)'}, funcs: {hidden: "IsNoneSelected()"}}, - { title: "Header", icon: "heading", toolTip: "Header Color", btnType: ButtonType.ColorButton, expertMode: true, ignoreClick: true, scripts: { script: 'return setHeaderColor(value, _readOnly_)'}, funcs: {hidden: "IsNoneSelected()"}}, - { title: "Fill", icon: "fill-drip", toolTip: "Background Fill Color",btnType: ButtonType.ColorButton, expertMode: false, ignoreClick: true, width: 30, scripts: { script: 'return setBackgroundColor(value, _readOnly_)'}, funcs: {hidden: "IsNoneSelected()"}}, // Only when a document is selected + { title: "Header", icon: "heading", toolTip: "Doc Titlebar Color", btnType: ButtonType.ColorButton, expertMode: true, ignoreClick: true, scripts: { script: 'return setHeaderColor(value, _readOnly_)'} }, + { title: "Fill", icon: "fill-drip", toolTip: "Fill/Background Color",btnType: ButtonType.ColorButton, expertMode: false, ignoreClick: true, width: 30, scripts: { script: 'return setBackgroundColor(value, _readOnly_)'}, funcs: {hidden: "IsNoneSelected()"}}, // Only when a document is selected { title: "Overlay", icon: "layer-group", toolTip: "Overlay", btnType: ButtonType.ToggleButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode, true)'}, scripts: { onClick: '{ return toggleOverlay(_readOnly_); }'}}, // Only when floating document is selected in freeform { title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)'}, width: 30, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}}, { title: "Num", icon:"", toolTip: "Frame Number (click to toggle edit mode)", btnType: ButtonType.TextButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20, scripts: { onClick: '{ return curKeyFrame(_readOnly_);}'}}, @@ -832,7 +832,7 @@ export class CurrentUserUtils { // childContextMenuLabels: new List<string>(["Add to Dashboards",]), // childContextMenuIcons: new List<string>(["user-plus",]), "acl-Guest": SharingPermissions.Augment, "_acl-Guest": SharingPermissions.Augment, - childDragAction: "embed", isSystem: true, contentPointerEvents: "all", childLimitHeight: 0, _yMargin: 0, _gridGap: 15, childDontRegisterViews:true, + childDragAction: "embed", isSystem: true, contentPointerEvents: "none", childLimitHeight: 0, _yMargin: 0, _gridGap: 15, childDontRegisterViews:true, // NOTE: treeView_HideTitle & _layout_showTitle is for a TreeView's editable title, _layout_showTitle is for DocumentViews title bar _layout_showTitle: "title", treeView_HideTitle: true, ignoreClick: true, _lockedPosition: true, layout_boxShadow: "0 0", _chromeHidden: true, dontRegisterView: true, layout_explainer: "This is where documents or dashboards that other users have shared with you will appear. To share a document or dashboard right click and select 'Share'" diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index b9f6059f4..7cc8afaa6 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -146,22 +146,14 @@ export class DocumentManager { return this.getDocumentViewsById(doc[Id]); } - public getDocumentView(toFind: Doc | undefined, preferredCollection?: DocumentView): DocumentView | undefined { - const doc = - // bcz: this was temporary code used to match documents by data url instead of by id. intended only for repairing the DB - // Array.from(DocumentManager.Instance.DocumentViews).find( - // dv => - // ((dv.rootDoc.data as any)?.url?.href && (dv.rootDoc.data as any)?.url?.href === (toFind.data as any)?.url?.href) || - // ((DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href && (DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href === (DocCast(toFind.annotationOn)?.data as any)?.url?.href) - // )?.rootDoc ?? - toFind; + public getDocumentView(target: Doc | undefined, preferredCollection?: DocumentView): DocumentView | undefined { const docViewArray = DocumentManager.Instance.DocumentViews; - const passes = !doc ? [] : preferredCollection ? [preferredCollection, undefined] : [undefined]; + const passes = !target ? [] : preferredCollection ? [preferredCollection, undefined] : [undefined]; return passes.reduce( - (pass, toReturn) => + (toReturn, pass) => toReturn ?? - docViewArray.filter(view => view.rootDoc === doc).find(view => !pass || view.props.docViewPath().lastElement() === preferredCollection) ?? - docViewArray.filter(view => Doc.AreProtosEqual(view.rootDoc, doc)).find(view => !pass || view.props.docViewPath().lastElement() === preferredCollection), + docViewArray.filter(view => view.rootDoc === target).find(view => !pass || view.props.docViewPath().lastElement() === preferredCollection) ?? + docViewArray.filter(view => Doc.AreProtosEqual(view.rootDoc, target)).find(view => !pass || view.props.docViewPath().lastElement() === preferredCollection), undefined as Opt<DocumentView> ); } @@ -306,7 +298,10 @@ export class DocumentManager { let contextView: DocumentView | undefined; // view containing context that contains target let focused = false; while (true) { - docView.rootDoc.layout_fieldKey === 'layout_icon' ? await new Promise<void>(res => docView.iconify(res)) : undefined; + if (docView.rootDoc.layout_fieldKey === 'layout_icon') { + await new Promise<void>(res => docView.iconify(res)); + options.didMove = true; + } const nextFocus = docView.props.focus(docView.rootDoc, options); // focus the view within its container focused = focused || (nextFocus === undefined ? false : true); // keep track of whether focusing on a view needed to actually change anything const { childDocView, viewSpec } = await iterator(docView); @@ -351,7 +346,7 @@ export function DocFocusOrOpen(doc: Doc, options: DocFocusOptions = { willZoomCe DocumentManager.Instance.showDocumentView(dv, options).then(() => dv && Doc.linkFollowHighlight(dv.rootDoc)); } else { const container = DocCast(containingDoc ?? doc.embedContainer ?? Doc.BestEmbedding(doc)); - const showDoc = !Doc.IsSystem(container) ? container : doc; + const showDoc = !Doc.IsSystem(container) && !cv ? container : doc; options.toggleTarget = undefined; DocumentManager.Instance.showDocument(showDoc, options, () => DocumentManager.Instance.showDocument(doc, { ...options, openLocation: undefined })).then(() => { const cv = DocumentManager.Instance.getDocumentView(containingDoc); diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 4f30e92ce..8d8975763 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -4,7 +4,7 @@ import { Doc, Field, Opt, StrListCast } from '../../fields/Doc'; import { List } from '../../fields/List'; import { PrefetchProxy } from '../../fields/Proxy'; import { ScriptField } from '../../fields/ScriptField'; -import { BoolCast, ScriptCast, StrCast } from '../../fields/Types'; +import { ScriptCast, StrCast } from '../../fields/Types'; import { emptyFunction, Utils } from '../../Utils'; import { Docs, DocUtils } from '../documents/Documents'; import * as globalCssVariables from '../views/global/globalCssVariables.scss'; @@ -191,13 +191,6 @@ export namespace DragManager { // drag a document and drop it (or make an embed/copy on drop) export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions, onDropCompleted?: (e?: DragCompleteEvent) => any) { - dragData.draggedViews.forEach( - action(view => { - const ffview = view.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; - ffview && (ffview.GroupChildDrag = BoolCast(ffview.Document._isGroup)); - ffview?.setupDragLines(false); - }) - ); const addAudioTag = (dropDoc: any) => { dropDoc && !dropDoc.author_date && (dropDoc.author_date = new DateField()); dropDoc instanceof Doc && DocUtils.MakeLinkToActiveAudio(() => dropDoc); @@ -205,14 +198,7 @@ export namespace DragManager { }; const finishDrag = async (e: DragCompleteEvent) => { const docDragData = e.docDragData; - setTimeout(() => - dragData.draggedViews.forEach( - action(view => { - const ffview = view.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; - ffview && (ffview.GroupChildDrag = false); - }) - ) - ); + setTimeout(() => dragData.draggedViews.forEach(view => view.props.CollectionFreeFormDocumentView?.().dragEnding())); onDropCompleted?.(e); // glr: optional additional function to be called - in this case with presentation trails if (docDragData && !docDragData.droppedDocuments.length) { docDragData.dropAction = dragData.userDropAction || dragData.dropAction; @@ -248,6 +234,7 @@ export namespace DragManager { }; dragData.draggedDocuments.map(d => d.dragFactory); // does this help? trying to make sure the dragFactory Doc is loaded StartDrag(eles, dragData, downX, downY, options, finishDrag); + dragData.draggedViews.forEach(view => view.props.CollectionFreeFormDocumentView?.().dragStarting()); return true; } @@ -281,9 +268,6 @@ export namespace DragManager { StartDrag(ele, dragData, downX, downY, options, undefined, 'Drag Column'); } - export function SetSnapLines(horizLines: number[], vertLines: number[]) { - SnappingManager.setSnapLines(horizLines, vertLines); - } export function snapDragAspect(dragPt: number[], snapAspect: number) { let closest = Utils.SNAP_THRESHOLD; let near = dragPt; @@ -345,7 +329,7 @@ export namespace DragManager { DocDragData = dragData as DocumentDragData; const batch = UndoManager.StartBatch(dragUndoName ?? 'document drag'); eles = eles.filter(e => e); - CanEmbed = dragData.canEmbed || false; + SnappingManager.SetCanEmbed(dragData.canEmbed || false); if (!dragDiv) { dragDiv = document.createElement('div'); dragDiv.className = 'dragManager-dragDiv'; @@ -471,7 +455,7 @@ export namespace DragManager { runInAction(() => docsBeingDragged.push(...docsToDrag)); const hideDragShowOriginalElements = (hide: boolean) => { - dragLabel.style.display = hide && !CanEmbed ? '' : 'none'; + dragLabel.style.display = hide && !SnappingManager.GetCanEmbed() ? '' : 'none'; !hide && dragElements.map(dragElement => dragElement.parentNode === dragDiv && dragDiv.removeChild(dragElement)); setTimeout(() => eles.forEach(ele => (ele.hidden = hide))); }; @@ -491,13 +475,14 @@ export namespace DragManager { }; const cleanupDrag = action((undo: boolean) => { + (dragData as DocumentDragData).draggedViews?.forEach(view => view.props.CollectionFreeFormDocumentView?.().dragEnding()); hideDragShowOriginalElements(false); document.removeEventListener('pointermove', moveHandler, true); document.removeEventListener('pointerup', upHandler, true); SnappingManager.SetIsDragging(false); - SnappingManager.clearSnapLines(); if (batch.end() && undo) UndoManager.Undo(); docsBeingDragged.length = 0; + SnappingManager.SetCanEmbed(false); }); var startWindowDragTimer: any; const moveHandler = (e: PointerEvent) => { @@ -604,7 +589,7 @@ export namespace DragManager { altKey: e.altKey, metaKey: e.metaKey, ctrlKey: e.ctrlKey, - embedKey: CanEmbed, + embedKey: SnappingManager.GetCanEmbed(), }, }; target.dispatchEvent(new CustomEvent<DropEvent>('dashPreDrop', dropArgs)); diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts index dbdf580cd..2c371f28e 100644 --- a/src/client/util/DropConverter.ts +++ b/src/client/util/DropConverter.ts @@ -7,7 +7,7 @@ import { Cast, StrCast } from '../../fields/Types'; import { ImageField } from '../../fields/URLField'; import { Docs } from '../documents/Documents'; import { DocumentType } from '../documents/DocumentTypes'; -import { ButtonType } from '../views/nodes/FontIconBox/FontIconBox'; +import { ButtonType, FontIconBox } from '../views/nodes/FontIconBox/FontIconBox'; import { DragManager } from './DragManager'; import { ScriptingGlobals } from './ScriptingGlobals'; @@ -56,7 +56,7 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) { data?.draggedDocuments.map((doc, i) => { let dbox = doc; // bcz: isButtonBar is intended to allow a collection of linear buttons to be dropped and nested into another collection of buttons... it's not being used yet, and isn't very elegant - if (doc.type === DocumentType.FONTICON || StrCast(Doc.Layout(doc).layout).includes('FontIconBox')) { + if (doc.type === DocumentType.FONTICON || StrCast(Doc.Layout(doc).layout).includes(FontIconBox.name)) { if (data.dropPropertiesToRemove || dbox.dropPropertiesToRemove) { //dbox = Doc.MakeEmbedding(doc); // don't need to do anything if dropping an icon doc onto an icon bar since there should be no layout data for an icon dbox = Doc.MakeEmbedding(dbox); @@ -78,7 +78,7 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) { backgroundColor: StrCast(doc.backgroundColor), title: StrCast(layoutDoc.title), btnType: ButtonType.ClickButton, - icon: layoutDoc.isTemplateDoc ? 'font' : 'bolt', + icon: 'bolt', }); dbox.dragFactory = layoutDoc; dbox.dropPropertiesToRemove = doc.dropPropertiesToRemove instanceof ObjectField ? ObjectField.MakeCopy(doc.dropPropertiesToRemove) : undefined; diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx index 4e32ed67f..be885312d 100644 --- a/src/client/util/InteractionUtils.tsx +++ b/src/client/util/InteractionUtils.tsx @@ -94,7 +94,7 @@ export namespace InteractionUtils { width: number, strokeWidth: number, lineJoin: string, - lineCap: string, + strokeLineCap: string, bezier: string, fill: string, arrowStart: string, @@ -170,9 +170,9 @@ export namespace InteractionUtils { </defs> )} <Tag - d={bezier ? strpts : undefined} + d={bezier ? strpts + (arrowStart || arrowEnd ? ' ' : '') : undefined} points={bezier ? undefined : strpts} - filter={!dropshadow ? undefined : `drop-shadow(-1px -1px 0px ${dropshadow}) drop-shadow(2px -1px 0px ${dropshadow}) drop-shadow(2px 2px 0px ${dropshadow}) drop-shadow(-1px 2px 0px ${dropshadow})`} + //filter={!dropshadow ? undefined : `drop-shadow(-1px -1px 0px ${dropshadow}) `} style={{ // filter: drawHalo ? "url(#inkSelectionHalo)" : undefined, fill: fill && fill !== 'transparent' ? fill : 'none', @@ -181,8 +181,8 @@ export namespace InteractionUtils { // opacity: strokeWidth !== width ? 0.5 : undefined, pointerEvents: (pevents as any) === 'all' ? 'visiblepainted' : (pevents as any), stroke: color ?? 'rgb(0, 0, 0)', - strokeWidth: strokeWidth, - strokeLinecap: lineCap as any, + strokeWidth, + strokeLinecap: strokeLineCap as any, strokeDasharray: dashArray, transition: 'inherit', }} diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index a533fdd1f..ba53a760f 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -202,8 +202,8 @@ export class LinkManager { public static getOppositeAnchor(linkDoc: Doc, anchor: Doc): Doc | undefined { const a1 = Cast(linkDoc.link_anchor_1, Doc, null); const a2 = Cast(linkDoc.link_anchor_2, Doc, null); - if (Doc.AreProtosEqual(DocCast(anchor.annotationOn, anchor), DocCast(a1.annotationOn, a1))) return a2; - if (Doc.AreProtosEqual(DocCast(anchor.annotationOn, anchor), DocCast(a2.annotationOn, a2))) return a1; + if (Doc.AreProtosEqual(DocCast(anchor.annotationOn, anchor), DocCast(a1?.annotationOn, a1))) return a2; + if (Doc.AreProtosEqual(DocCast(anchor.annotationOn, anchor), DocCast(a2?.annotationOn, a2))) return a1; if (Doc.AreProtosEqual(anchor, linkDoc)) return linkDoc; } } diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index dc852596f..f75322905 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -7,7 +7,7 @@ import { BsGoogle } from 'react-icons/bs'; import { FaFillDrip, FaPalette } from 'react-icons/fa'; import { Doc } from '../../fields/Doc'; import { DashVersion } from '../../fields/DocSymbols'; -import { BoolCast, Cast, StrCast } from '../../fields/Types'; +import { BoolCast, Cast, NumCast, StrCast } from '../../fields/Types'; import { addStyleSheet, addStyleSheetRule, Utils } from '../../Utils'; import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager'; import { DocServer } from '../DocServer'; @@ -269,6 +269,19 @@ export class SettingsManager extends React.Component<{}> { size={Size.XSMALL} color={SettingsManager.userColor} /> + <Group formLabel="Title Height"> + <NumberDropdown + number={NumCast(Doc.UserDoc().headerHeight, 30)} + color={SettingsManager.userColor} + numberDropdownType={'slider'} + min={6} + max={60} + step={2} + type={Type.TERT} + unit={'px'} + setNumber={val => console.log('GOT: ' + (Doc.UserDoc().headerHeight = val))} + /> + </Group> </div> ); } diff --git a/src/client/util/SnappingManager.ts b/src/client/util/SnappingManager.ts index ed9819fc0..c0cd94067 100644 --- a/src/client/util/SnappingManager.ts +++ b/src/client/util/SnappingManager.ts @@ -1,19 +1,20 @@ import { observable, action, runInAction } from 'mobx'; -import { computedFn } from 'mobx-utils'; import { Doc } from '../../fields/Doc'; export namespace SnappingManager { class Manager { @observable IsDragging: boolean = false; + @observable IsResizing: Doc | undefined; + @observable CanEmbed: boolean = false; @observable public horizSnapLines: number[] = []; @observable public vertSnapLines: number[] = []; @action public clearSnapLines() { this.vertSnapLines = []; this.horizSnapLines = []; } - @action public setSnapLines(horizLines: number[], vertLines: number[]) { - this.horizSnapLines = horizLines; - this.vertSnapLines = vertLines; + @action public addSnapLines(horizLines: number[], vertLines: number[]) { + this.horizSnapLines.push(...horizLines); + this.vertSnapLines.push(...vertLines); } } @@ -22,8 +23,8 @@ export namespace SnappingManager { export function clearSnapLines() { manager.clearSnapLines(); } - export function setSnapLines(horizLines: number[], vertLines: number[]) { - manager.setSnapLines(horizLines, vertLines); + export function addSnapLines(horizLines: number[], vertLines: number[]) { + manager.addSnapLines(horizLines, vertLines); } export function horizSnapLines() { return manager.horizSnapLines; @@ -35,14 +36,19 @@ export namespace SnappingManager { export function SetIsDragging(dragging: boolean) { runInAction(() => (manager.IsDragging = dragging)); } + export function SetIsResizing(doc: Doc | undefined) { + runInAction(() => (manager.IsResizing = doc)); + } + export function SetCanEmbed(canEmbed: boolean) { + runInAction(() => (manager.CanEmbed = canEmbed)); + } export function GetIsDragging() { return manager.IsDragging; } - - export function SetShowSnapLines(show: boolean) { - runInAction(() => (Doc.UserDoc().freeform_snapLines = show)); + export function GetIsResizing() { + return manager.IsResizing; } - export function GetShowSnapLines() { - return Doc.UserDoc().freeform_snapLines; + export function GetCanEmbed() { + return manager.CanEmbed; } } diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss index ca3610cc0..f41cf1385 100644 --- a/src/client/views/DocumentDecorations.scss +++ b/src/client/views/DocumentDecorations.scss @@ -14,18 +14,19 @@ $resizeHandler: 8px; border-radius: 100%; height: 30; width: 30; - right: -30; - top: calc(50% - 15px); + right: -40; + bottom: -40; + //top: calc(50% - 15px); position: absolute; pointer-events: all; cursor: pointer; - background: white; + background: transparent; display: flex; justify-content: center; align-items: center; text-align: center; font-size: 30px; - opacity: 0.1; + opacity: 0.6; &:hover { opacity: 1; } @@ -227,20 +228,19 @@ $resizeHandler: 8px; &:hover { opacity: 1; } - - .checkbox{ + .checkbox { display: inline; - .checkbox-box{ + .checkbox-box { display: inline; position: relative; top: -2.5; left: 35; - zoom: .7; + zoom: 0.7; } - & .checkbox-text{ + & .checkbox-text { display: inline; position: relative; top: 1.5; @@ -248,7 +248,7 @@ $resizeHandler: 8px; } } - .documentDecorations-shareNone{ + .documentDecorations-shareNone { width: calc(100% + 10px); background: grey; color: rgb(71, 71, 71); @@ -256,29 +256,28 @@ $resizeHandler: 8px; border: 2px solid rgb(71, 71, 71); } .documentDecorations-shareEdit, - .documentDecorations-shareAdmin{ + .documentDecorations-shareAdmin { width: calc(100% + 10px); background: rgb(254, 254, 199); color: rgb(75, 75, 5); border-radius: 8px; border: 2px solid rgb(75, 75, 5); } - .documentDecorations-shareAugment{ + .documentDecorations-shareAugment { width: calc(100% + 10px); background: rgb(208, 255, 208); - color:rgb(19, 80, 19); + color: rgb(19, 80, 19); border-radius: 8px; border: 2px solid rgb(19, 80, 19); - } - .documentDecorations-shareView{ + .documentDecorations-shareView { width: calc(100% + 10px); background: rgb(213, 213, 255); color: rgb(25, 25, 101); border-radius: 8px; border: 2px solid rgb(25, 25, 101); } - .documentDecorations-shareNot-Shared{ + .documentDecorations-shareNot-Shared { width: calc(100% + 10px); background: rgb(255, 207, 207); color: rgb(146, 58, 58); @@ -320,8 +319,8 @@ $resizeHandler: 8px; .documentDecorations-bottomResizer, .documentDecorations-rightResizer { pointer-events: auto; - background: $medium-gray; - opacity: 0.2; + background: $medium-gray-dim; + //opacity: 0.2; &:hover { opacity: 1; } @@ -354,7 +353,7 @@ $resizeHandler: 8px; position: absolute; border-radius: 100%; left: 7px; - top: 27px; + top: 7px; background: $medium-gray; height: 10; width: 10; @@ -366,15 +365,19 @@ $resizeHandler: 8px; position: relative; background: black; color: rgb(145, 144, 144); - height: 14; - width: 14; + height: 20; + width: 20; pointer-events: all; margin: auto; display: flex; align-items: center; - flex-direction: column; - border-radius: 15%; + border-radius: 100%; cursor: default; + svg { + width: 10; + height: 10; + margin: auto; + } } .documentDecorations-rotationPath { @@ -447,9 +450,9 @@ $resizeHandler: 8px; opacity: 1; } - .documentDecorations-bottomRightResizer { - grid-row: 4; - } + // .documentDecorations-bottomRightResizer { + // grid-row: 4; + // } .documentDecorations-topRightResizer, .documentDecorations-bottomLeftResizer { @@ -488,6 +491,10 @@ $resizeHandler: 8px; } } +.documentDecorations-container.showNothing { + margin-top: $headerHeight; + grid-template-rows: $resizeHandler 1fr $resizeHandler; +} .documentDecorations-background { background: lightblue; position: absolute; diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 40eb1fe2b..5a145e94a 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -20,6 +20,7 @@ import { DocumentManager } from '../util/DocumentManager'; import { DragManager } from '../util/DragManager'; import { LinkFollower } from '../util/LinkFollower'; import { SelectionManager } from '../util/SelectionManager'; +import { SettingsManager } from '../util/SettingsManager'; import { SnappingManager } from '../util/SnappingManager'; import { UndoManager } from '../util/UndoManager'; import { CollectionDockingView } from './collections/CollectionDockingView'; @@ -46,9 +47,9 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P private _titleHeight = 20; private _resizeUndo?: UndoManager.Batch; private _offX = 0; - _offY = 0; // offset from click pt to inner edge of resize border + private _offY = 0; // offset from click pt to inner edge of resize border private _snapX = 0; - _snapY = 0; // last snapped location of resize border + private _snapY = 0; // last snapped location of resize border private _dragHeights = new Map<Doc, { start: number; lowest: number }>(); private _inkDragDocs: { doc: Doc; x: number; y: number; width: number; height: number }[] = []; @@ -62,7 +63,6 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P @observable public pullColor: string = 'white'; @observable private _isRotating: boolean = false; @observable private _isRounding: boolean = false; - @observable private _isResizing: boolean = false; @observable private showLayoutAcl: boolean = false; constructor(props: any) { @@ -70,8 +70,8 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P DocumentDecorations.Instance = this; reaction( () => SelectionManager.Views().slice(), - action(docs => { - this._showNothing = !DocumentView.LongPress && docs.length === 1; // show decorations if multiple docs are selected or we're long pressing + action(views => { + this._showNothing = !DocumentView.LongPress && views.length === 1; // show decorations if multiple docs are selected or we're long pressing this._editingTitle = false; }) ); @@ -80,8 +80,10 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P 'pointermove', action(e => { if (this.Bounds.x || this.Bounds.y || this.Bounds.r || this.Bounds.b) { - if (this.Bounds.x !== Number.MAX_VALUE && (this.Bounds.x > e.clientX || this.Bounds.r < e.clientX || this.Bounds.y > e.clientY || this.Bounds.b < e.clientY)) { + if (this.Bounds.x !== Number.MAX_VALUE && (this.Bounds.x > e.clientX + 10 || this.Bounds.r < e.clientX - 10 || this.Bounds.y > e.clientY + 10 || this.Bounds.b < e.clientY - 10)) { this._showNothing = false; + } else { + this._showNothing = true; } } }) @@ -225,10 +227,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P e.x, e.y, { - dragComplete: action(e => { - dragData.canEmbed && SelectionManager.DeselectAll(); - this._hidden = false; - }), + dragComplete: action(e => (this._hidden = false)), hideSource: true, } ); @@ -255,7 +254,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P iconView.props.removeDocument?.(iconView.props.Document); } }); - SelectionManager.DeselectAll(); + views.forEach(v => SelectionManager.DeselectView()); } this._iconifyBatch?.end(); this._iconifyBatch = undefined; @@ -403,10 +402,8 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P }), // moveEvent action(action(() => (this._isRotating = false))), // upEvent action((e, doubleTap) => { - if (doubleTap) { - seldocview.rootDoc.rotation_centerX = 0.5; - seldocview.rootDoc.rotation_centerY = 0.5; - } + seldocview.rootDoc.rotation_centerX = 0; + seldocview.rootDoc.rotation_centerY = 0; }) ); }; @@ -477,6 +474,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P @action onPointerDown = (e: React.PointerEvent): void => { + SnappingManager.SetIsResizing(SelectionManager.Docs().lastElement()); setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, emptyFunction); DocumentView.Interacting = true; // turns off pointer events on things like youtube videos and web pages so that dragging doesn't get "stuck" when cursor moves over them this._resizeHdlId = e.currentTarget.className; @@ -492,7 +490,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P ffview && ffviewSet.add(ffview); this._dragHeights.set(docView.layoutDoc, { start: NumCast(docView.rootDoc._height), lowest: NumCast(docView.rootDoc._height) }); }); - Array.from(ffviewSet).map(ffview => ffview.setupDragLines(false)); + Array.from(ffviewSet).map(ffview => ffview.dragStarting(false, false)); }; onPointerMove = (e: PointerEvent, down: number[], move: number[]): boolean => { @@ -687,6 +685,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P @action onPointerUp = (e: PointerEvent): void => { + SnappingManager.SetIsResizing(undefined); this._resizeHdlId = ''; DocumentView.Interacting = false; this._resizeUndo?.end(); @@ -774,13 +773,14 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P var shareSymbolIcon = ReverseHierarchyMap.get(shareMode)?.image; // hide the decorations if the parent chooses to hide it or if the document itself hides it - const hideDecorations = seldocview.props.hideDecorations || seldocview.rootDoc.hideDecorations; + const hideDecorations = SnappingManager.GetIsResizing() || seldocview.props.hideDecorations || seldocview.rootDoc.layout_hideDecorations; const hideResizers = ![AclAdmin, AclEdit, AclAugment].includes(GetEffectiveAcl(seldocview.rootDoc)) || hideDecorations || seldocview.props.hideResizeHandles || seldocview.rootDoc.layout_hideResizeHandles || this._isRounding || this._isRotating; - const hideTitle = hideDecorations || seldocview.props.hideDecorationTitle || seldocview.rootDoc.layout_hideDecorationTitle || this._isRounding || this._isRotating; + const hideTitle = this._showNothing || hideDecorations || seldocview.props.hideDecorationTitle || seldocview.rootDoc.layout_hideDecorationTitle || this._isRounding || this._isRotating; const hideDocumentButtonBar = hideDecorations || seldocview.props.hideDocumentButtonBar || seldocview.rootDoc.layout_hideDocumentButtonBar || this._isRounding || this._isRotating; // if multiple documents have been opened at the same time, then don't show open button const hideOpenButton = + this._showNothing || hideDecorations || seldocview.props.hideOpenButton || seldocview.rootDoc.layout_hideOpenButton || @@ -788,6 +788,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P this._isRounding || this._isRotating; const hideDeleteButton = + this._showNothing || hideDecorations || this._isRounding || this._isRotating || @@ -821,7 +822,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P // Radius constants const useRounding = seldocview.ComponentView instanceof ImageBox || seldocview.ComponentView instanceof FormattedTextBox || seldocview.ComponentView instanceof CollectionFreeFormView; - const borderRadius = numberValue(StrCast(seldocview.rootDoc.layout_borderRounding)); + const borderRadius = numberValue(Cast(seldocview.rootDoc.layout_borderRounding, 'string', null)); const docMax = Math.min(NumCast(seldocview.rootDoc.width) / 2, NumCast(seldocview.rootDoc.height) / 2); const maxDist = Math.min((this.Bounds.r - this.Bounds.x) / 2, (this.Bounds.b - this.Bounds.y) / 2); const radiusHandle = (borderRadius / docMax) * maxDist; @@ -845,9 +846,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P */} </div> </div> - ) : ( - <div /> - ); + ) : null; const titleArea = this._editingTitle ? ( <input @@ -863,12 +862,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P onPointerDown={e => e.stopPropagation()} /> ) : ( - <div - className="documentDecorations-title" - key="title" - onPointerDown={e => { - e.stopPropagation; - }}> + <div className="documentDecorations-title" key="title" onPointerDown={e => e.stopPropagation}> {hideTitle ? null : ( <span className="documentDecorations-titleSpan" onPointerDown={this.onTitleDown}> {this.selectionTitle} @@ -884,9 +878,9 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P )} </div> ); - + const freeformDoc = SelectionManager.Views().some(v => v.props.CollectionFreeFormDocumentView?.()); return ( - <div className="documentDecorations" style={{ display: this._showNothing ? 'none' : undefined }}> + <div className="documentDecorations" style={{ display: this._showNothing && !freeformDoc ? 'none' : undefined }}> <div className="documentDecorations-background" style={{ @@ -906,13 +900,13 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P {bounds.r - bounds.x < 15 && bounds.b - bounds.y < 15 ? null : ( <div> <div - className="documentDecorations-container" + className={`documentDecorations-container ${this._showNothing ? 'showNothing' : ''}`} key="container" style={{ transform: `translate(${bounds.x - this._resizeBorderWidth / 2}px, ${bounds.y - this._resizeBorderWidth / 2 - this._titleHeight}px) rotate(${rotation}deg)`, transformOrigin: `50% calc(50% + 10px)`, width: bounds.r - bounds.x + this._resizeBorderWidth + 'px', - height: bounds.b - bounds.y + this._resizeBorderWidth + this._titleHeight + 'px', + height: bounds.b - bounds.y + this._resizeBorderWidth + (this._showNothing ? 0 : this._titleHeight) + 'px', }}> <div className="documentDecorations-topbar" @@ -944,17 +938,18 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P {useRounding && ( <div key="rad" + className="documentDecorations-borderRadius" style={{ - background: `${this._isRounding ? Colors.MEDIUM_BLUE : undefined}`, - transform: `translate(${radiusHandleLocation}px, ${radiusHandleLocation}px)`, + opacity: 0.5, + background: `${this._isRounding ? Colors.MEDIUM_BLUE : SettingsManager.userColor}`, + transform: `translate(${radiusHandleLocation ?? 0}px, ${(radiusHandleLocation ?? 0) + (this._showNothing ? 0 : this._titleHeight)}px)`, }} - className={`documentDecorations-borderRadius`} onPointerDown={this.onRadiusDown} onContextMenu={e => e.preventDefault()} /> )} - {hideDocumentButtonBar ? null : ( + {hideDocumentButtonBar || this._showNothing ? null : ( <div className="link-button-container" key="links" @@ -980,8 +975,8 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P }}> {this._isRotating ? null : ( <Tooltip enterDelay={750} title={<div className="dash-tooltip">tap to set rotate center, drag to rotate</div>}> - <div className="documentDecorations-rotation" style={{ pointerEvents: 'all', color: 'blue' }} onPointerDown={this.onRotateDown} onContextMenu={e => e.preventDefault()}> - <IconButton icon={<FaUndo />} color={Colors.LIGHT_GRAY} /> + <div className="documentDecorations-rotation" onPointerDown={this.onRotateDown} onContextMenu={e => e.preventDefault()}> + <IconButton icon={<FaUndo />} color={SettingsManager.userColor} /> </div> </Tooltip> )} diff --git a/src/client/views/EditableView.scss b/src/client/views/EditableView.scss index f7c03caf9..27b260450 100644 --- a/src/client/views/EditableView.scss +++ b/src/client/views/EditableView.scss @@ -3,7 +3,7 @@ overflow-wrap: break-word; word-wrap: break-word; hyphens: auto; - overflow: auto; + overflow: hidden; height: 100%; min-width: 20; text-overflow: ellipsis; diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index ca4ffaf3a..abb7ed7ee 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -283,11 +283,26 @@ export class EditableView extends React.Component<EditableProps> { <div className={`editableView-container-editing${this.props.oneLine ? '-oneLine' : ''}`} ref={this._ref} - style={{ display: this.props.display, textOverflow: this.props.overflow, minHeight: '10px', whiteSpace: this.props.oneLine ? 'nowrap' : 'pre-line', height: this.props.height, maxHeight: this.props.maxHeight }} + style={{ + display: this.props.display, // + textOverflow: this.props.overflow, + minHeight: '10px', + whiteSpace: this.props.oneLine ? 'nowrap' : 'pre-line', + height: this.props.height, + maxHeight: this.props.maxHeight, + fontStyle: this.props.fontStyle, + fontSize: this.props.fontSize, + }} //onPointerDown={this.stopPropagation} onClick={this.onClick} placeholder={this.props.placeholder}> - <span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize }}>{this.props.contents ? this.props.contents?.valueOf() : this.props.placeholder?.valueOf()}</span> + <span + style={{ + fontStyle: this.props.fontStyle, + fontSize: this.props.fontSize, + }}> + {this.props.contents ? this.props.contents?.valueOf() : this.props.placeholder?.valueOf()} + </span> </div> ); } diff --git a/src/client/views/FilterPanel.tsx b/src/client/views/FilterPanel.tsx index 0df88f970..cb5c9b085 100644 --- a/src/client/views/FilterPanel.tsx +++ b/src/client/views/FilterPanel.tsx @@ -7,7 +7,7 @@ import { CiCircleRemove } from 'react-icons/ci'; import Select from 'react-select'; import { Doc, DocListCast, Field, LinkedTo, StrListCast } from '../../fields/Doc'; import { RichTextField } from '../../fields/RichTextField'; -import { DocumentOptions, FInfo } from '../documents/Documents'; +import { DocOptions, DocumentOptions, FInfo } from '../documents/Documents'; import { DocumentManager } from '../util/DocumentManager'; import { UserOptions } from '../util/GroupManager'; import { SearchUtil } from '../util/SearchUtil'; @@ -18,6 +18,7 @@ import { Handle, Tick, TooltipRail, Track } from './nodes/SliderBox-components'; import { SettingsManager } from '../util/SettingsManager'; import { Id } from '../../fields/FieldSymbols'; import { List } from '../../fields/List'; +import { emptyFunction } from '../../Utils'; interface filterProps { rootDoc: Doc; @@ -25,8 +26,6 @@ interface filterProps { @observer export class FilterPanel extends React.Component<filterProps> { - private _documentOptions: DocumentOptions = new DocumentOptions(); - public static LayoutString(fieldKey: string) { return FieldView.LayoutString(FilterPanel, fieldKey); } @@ -51,12 +50,10 @@ export class FilterPanel extends React.Component<filterProps> { if (targetDoc) { SearchUtil.foreachRecursiveDoc([this.targetDoc], (depth, doc) => allDocs.add(doc)); } - console.log('this is all Docs' + Array.from(allDocs)); return Array.from(allDocs); } @computed get _allFacets() { - // trace(); const noviceReqFields = ['author', 'tags', 'text', 'type', LinkedTo]; const noviceLayoutFields: string[] = []; //["_layout_curPage"]; const noviceFields = [...noviceReqFields, ...noviceLayoutFields]; @@ -68,11 +65,8 @@ export class FilterPanel extends React.Component<filterProps> { .filter(key => key.indexOf('modificationDate') !== -1 || (key[0] === key[0].toUpperCase() && !key.startsWith('_')) || noviceFields.includes(key) || !Doc.noviceMode) .sort(); - // console.log('THIS IS HERE ' + Doc.UserDoc().color + 'space ' + Doc.UserDoc().color); noviceFields.forEach(key => sortedKeys.splice(sortedKeys.indexOf(key), 1)); - console.log('this is novice fields ' + noviceFields + 'and this is sorted Keys ' + sortedKeys); - return [...noviceFields, ...sortedKeys]; } @@ -206,13 +200,7 @@ export class FilterPanel extends React.Component<filterProps> { */ @action - facetClick = (facetHeader: string) => { - // just when someone chooses a facet - - this._selectedFacetHeaders.add(facetHeader); - - return; - }; + facetClick = (facetHeader: string) => this._selectedFacetHeaders.add(facetHeader); @action sortingCurrentFacetValues = (facetHeader: string) => { @@ -260,57 +248,59 @@ export class FilterPanel extends React.Component<filterProps> { return nonNumbers / facetValues.length > 0.1 ? facetValues.sort() : facetValues.sort((n1: string, n2: string) => Number(n1) - Number(n2)); }; - render() { - let filteredOptions: string[] = ['author', 'tags', 'text', 'acl-Guest', ...this._allFacets.filter(facet => facet[0] === facet.charAt(0).toUpperCase())]; + @computed get fieldsDropdown() { + const filteredOptions = ['author', 'tags', 'text', 'acl-Guest', ...this._allFacets.filter(facet => facet[0] === facet.charAt(0).toUpperCase())]; - Object.entries(this._documentOptions).forEach((pair: [string, FInfo]) => { - if (pair[1].filterable) { - filteredOptions.push(pair[0]); - } - }); + Object.entries(DocOptions) + .filter(opts => opts[1].filterable) + .forEach((pair: [string, FInfo]) => filteredOptions.push(pair[0])); + const options = filteredOptions.map(facet => ({ value: facet, label: facet })); - let options = filteredOptions.map(facet => ({ value: facet, label: facet })); + return ( + <Select + styles={{ + control: (baseStyles, state) => ({ + ...baseStyles, + color: SettingsManager.userColor, + background: SettingsManager.userBackgroundColor, + }), + placeholder: (baseStyles, state) => ({ + ...baseStyles, + color: SettingsManager.userColor, + background: SettingsManager.userBackgroundColor, + }), + input: (baseStyles, state) => ({ + ...baseStyles, + color: SettingsManager.userColor, + background: SettingsManager.userBackgroundColor, + }), + option: (baseStyles, state) => ({ + ...baseStyles, + color: SettingsManager.userColor, + background: !state.isFocused ? SettingsManager.userBackgroundColor : SettingsManager.userVariantColor, + }), + menuList: (baseStyles, state) => ({ + ...baseStyles, + backgroundColor: SettingsManager.userBackgroundColor, + }), + }} + placeholder={'add a filter'} + options={options} + isMulti={false} + onChange={val => this.facetClick((val as UserOptions).value)} + onKeyDown={e => e.stopPropagation()} + //onMenuClose={onClose} + value={null} + closeMenuOnSelect={true} + /> + ); + } + render() { return ( <div className="filterBox-treeView"> <div className="filterBox-select"> - <div style={{ width: '100%' }}> - <Select - styles={{ - control: (baseStyles, state) => ({ - ...baseStyles, - color: SettingsManager.userColor, - background: SettingsManager.userBackgroundColor, - }), - placeholder: (baseStyles, state) => ({ - ...baseStyles, - color: SettingsManager.userColor, - background: SettingsManager.userBackgroundColor, - }), - input: (baseStyles, state) => ({ - ...baseStyles, - color: SettingsManager.userColor, - background: SettingsManager.userBackgroundColor, - }), - option: (baseStyles, state) => ({ - ...baseStyles, - color: SettingsManager.userColor, - background: !state.isFocused ? SettingsManager.userBackgroundColor : SettingsManager.userVariantColor, - }), - menuList: (baseStyles, state) => ({ - ...baseStyles, - backgroundColor: SettingsManager.userBackgroundColor, - }), - }} - placeholder="Add a filter..." - options={options} - isMulti={false} - onChange={val => this.facetClick((val as UserOptions).value)} - onKeyDown={e => e.stopPropagation()} - value={null} - closeMenuOnSelect={true} - /> - </div> + <div style={{ width: '100%' }}>{this.fieldsDropdown}</div> {/* THE FOLLOWING CODE SHOULD BE DEVELOPER FOR BOOLEAN EXPRESSION (AND / OR) */} {/* <div className="filterBox-select-bool"> <select className="filterBox-selection" onChange={action(e => this.targetDoc && (this.targetDoc._childFilters_boolean = (e.target as any).value))} defaultValue={StrCast(this.targetDoc?.childFilters_boolean)}> diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx index 07e3270b1..0d7f7ebd8 100644 --- a/src/client/views/InkControlPtHandles.tsx +++ b/src/client/views/InkControlPtHandles.tsx @@ -2,18 +2,16 @@ import React = require('react'); import { action, observable } from 'mobx'; import { observer } from 'mobx-react'; import { Doc } from '../../fields/Doc'; -import { ControlPoint, InkData, PointData, InkField } from '../../fields/InkField'; +import { ControlPoint, InkData, PointData } from '../../fields/InkField'; import { List } from '../../fields/List'; import { listSpec } from '../../fields/Schema'; -import { Cast, NumCast } from '../../fields/Types'; -import { setupMoveUpEvents, returnFalse } from '../../Utils'; -import { Transform } from '../util/Transform'; +import { Cast } from '../../fields/Types'; +import { returnFalse, setupMoveUpEvents } from '../../Utils'; +import { SelectionManager } from '../util/SelectionManager'; import { UndoManager } from '../util/UndoManager'; import { Colors } from './global/globalEnums'; import { InkingStroke } from './InkingStroke'; import { InkStrokeProperties } from './InkStrokeProperties'; -import { DocumentView } from './nodes/DocumentView'; -import { SelectionManager } from '../util/SelectionManager'; export interface InkControlProps { inkDoc: Doc; @@ -155,7 +153,7 @@ export class InkControlPtHandles extends React.Component<InkControlProps> { cx={control.X} cy={control.Y} r={this.props.screenSpaceLineWidth * 2 * scale} - opacity={this.props.inkView.controlUndo ? 0.15 : 1} + opacity={this.props.inkView.controlUndo ? 0.35 : 1} height={this.props.screenSpaceLineWidth * 4 * scale} width={this.props.screenSpaceLineWidth * 4 * scale} strokeWidth={this.props.screenSpaceLineWidth / 2} diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts index abc4381a6..736ca8d90 100644 --- a/src/client/views/InkStrokeProperties.ts +++ b/src/client/views/InkStrokeProperties.ts @@ -12,6 +12,7 @@ import { DocumentManager } from '../util/DocumentManager'; import { undoBatch } from '../util/UndoManager'; import { InkingStroke } from './InkingStroke'; import { DocumentView } from './nodes/DocumentView'; +import _ = require('lodash'); export class InkStrokeProperties { static _Instance: InkStrokeProperties | undefined; @@ -64,7 +65,7 @@ export class InkStrokeProperties { doc._height = (newYrange.max - newYrange.min) * ptsYscale + NumCast(doc.stroke_width); doc.x = oldXrange.coord + (newXrange.min - oldXrange.min) * ptsXscale; doc.y = oldYrange.coord + (newYrange.min - oldYrange.min) * ptsYscale; - Doc.GetProto(doc).stroke = new InkField(newPoints); + Doc.SetInPlace(doc, 'stroke', new InkField(newPoints), true); appliedFunc = true; } } @@ -262,7 +263,7 @@ export class InkStrokeProperties { var endDir = { x: 0, y: 0 }; for (var i = 0; i < nearestSeg / 4 + 1; i++) { const bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y }))); - if (i === 0) startDir = bez.derivative(0); + if (i === 0) startDir = bez.derivative(_.isEqual(bez.derivative(0), { x: 0, y: 0, t: 0 }) ? 1e-8 : 0); if (i === nearestSeg / 4) endDir = bez.derivative(nearestT); for (var t = 0; t < (i === nearestSeg / 4 ? nearestT + 0.05 : 1); t += 0.05) { const pt = bez.compute(i !== nearestSeg / 4 ? t : Math.min(nearestT, t)); @@ -273,7 +274,7 @@ export class InkStrokeProperties { for (var i = nearestSeg / 4; i < splicedPoints.length / 4; i++) { const bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y }))); if (i === nearestSeg / 4) startDir = bez.derivative(nearestT); - if (i === splicedPoints.length / 4 - 1) endDir = bez.derivative(1); + if (i === splicedPoints.length / 4 - 1) endDir = bez.derivative(_.isEqual(bez.derivative(1), { x: 0, y: 0, t: 1 }) ? 1 - 1e-8 : 1); for (var t = i === nearestSeg / 4 ? nearestT : 0; t < (i === nearestSeg / 4 ? 1 + 0.05 + 1e-7 : 1 + 1e-7); t += 0.05) { const pt = bez.compute(Math.min(1, t)); samplesRight.push(new Point(pt.x, pt.y)); diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 9b52f5870..d26c7761e 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -246,7 +246,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { * factor for converting between ink and screen space. */ inkScaledData = () => { - const inkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? []; + const inkData = Cast(this.rootDoc[this.fieldKey], InkField)?.inkData ?? []; const inkStrokeWidth = NumCast(this.rootDoc.stroke_width, 1); const inkTop = Math.min(...inkData.map(p => p.Y)) - inkStrokeWidth / 2; const inkBottom = Math.max(...inkData.map(p => p.Y)) + inkStrokeWidth / 2; @@ -390,7 +390,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { } const highlight = !this.controlUndo && this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Highlighting); const highlightIndex = highlight?.highlightIndex; - const highlightColor = (!this.props.isSelected() || !isInkMask) && highlight?.highlightIndex ? highlight?.highlightColor : undefined; + const highlightColor = !this.props.isSelected() && !isInkMask && highlight?.highlightIndex ? highlight?.highlightColor : undefined; const color = StrCast(this.layoutDoc.stroke_outlineColor, !closed && fillColor && fillColor !== 'transparent' ? StrCast(this.layoutDoc.color, 'transparent') : 'transparent'); // Visually renders the polygonal line made by the user. @@ -419,15 +419,16 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { undefined, color === 'transparent' ? highlightColor : undefined ); + const higlightMargin = Math.min(12, Math.max(2, 0.3 * inkStrokeWidth)); // Invisible polygonal line that enables the ink to be selected by the user. const clickableLine = (downHdlr?: (e: React.PointerEvent) => void, mask: boolean = false) => InteractionUtils.CreatePolyline( inkData, inkLeft, inkTop, - mask && color === 'transparent' ? this.strokeColor : color, + mask && color === 'transparent' ? this.strokeColor : highlightColor ?? color, inkStrokeWidth, - inkStrokeWidth + NumCast(this.layoutDoc.stroke_borderWidth) + (fillColor ? (closed ? 2 : (highlightIndex ?? 0) + 2) : 2), + inkStrokeWidth + NumCast(this.layoutDoc.stroke_borderWidth) + (fillColor ? (closed ? higlightMargin : (highlightIndex ?? 0) + higlightMargin) : higlightMargin), StrCast(this.layoutDoc.stroke_lineJoin), StrCast(this.layoutDoc.stroke_lineCap), StrCast(this.layoutDoc.stroke_bezier), diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 0a3389fc2..da5e4f966 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -918,7 +918,8 @@ export class MainView extends React.Component { } @computed get snapLines() { SnappingManager.GetIsDragging(); - const dragged = DragManager.docsBeingDragged.lastElement(); + SnappingManager.GetIsResizing(); + const dragged = DragManager.docsBeingDragged.lastElement() ?? SelectionManager.Docs().lastElement(); const dragPar = dragged ? DocumentManager.Instance.getDocumentView(dragged)?.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView : undefined; return !dragPar?.rootDoc.freeform_snapLines ? null : ( <div className="mainView-snapLines"> diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx index d1561fd67..18f53b8e7 100644 --- a/src/client/views/PropertiesButtons.tsx +++ b/src/client/views/PropertiesButtons.tsx @@ -1,38 +1,33 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Icon, Tooltip } from '@material-ui/core'; +import { Dropdown, DropdownType, IListItemProps, Toggle, ToggleType, Type } from 'browndash-components'; import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; +import { AiOutlineColumnWidth } from 'react-icons/ai'; +import { BiHide, BiShow } from 'react-icons/bi'; +import { BsGrid3X3GapFill } from 'react-icons/bs'; +import { CiGrid31 } from 'react-icons/ci'; +import { FaBraille, FaLock, FaLockOpen } from 'react-icons/fa'; +import { MdClosedCaption, MdClosedCaptionDisabled, MdGridOff, MdGridOn, MdSubtitles, MdSubtitlesOff, MdTouchApp } from 'react-icons/md'; +import { RxWidth } from 'react-icons/rx'; +import { TbEditCircle, TbEditCircleOff, TbHandOff, TbHandStop, TbHighlight, TbHighlightOff } from 'react-icons/tb'; +import { TfiBarChart } from 'react-icons/tfi'; import { Doc, DocListCast, Opt } from '../../fields/Doc'; -import { Id } from '../../fields/FieldSymbols'; import { InkField } from '../../fields/InkField'; import { RichTextField } from '../../fields/RichTextField'; -import { BoolCast, ScriptCast, StrCast } from '../../fields/Types'; +import { BoolCast, ScriptCast } from '../../fields/Types'; import { ImageField } from '../../fields/URLField'; -import { Utils } from '../../Utils'; import { DocUtils } from '../documents/Documents'; import { CollectionViewType, DocumentType } from '../documents/DocumentTypes'; import { IsFollowLinkScript } from '../util/LinkFollower'; import { LinkManager } from '../util/LinkManager'; import { SelectionManager } from '../util/SelectionManager'; +import { SettingsManager } from '../util/SettingsManager'; import { undoable, undoBatch } from '../util/UndoManager'; import { Colors } from './global/globalEnums'; import { InkingStroke } from './InkingStroke'; import { DocumentView, OpenWhere } from './nodes/DocumentView'; -import { pasteImageBitmap } from './nodes/WebBoxRenderer'; import './PropertiesButtons.scss'; import React = require('react'); -import { JsxElement } from 'typescript'; -import { FaBraille, FaHighlighter, FaLock, FaLockOpen, FaThumbtack } from 'react-icons/fa'; -import { AiOutlineApple, AiOutlineColumnWidth, AiOutlinePicture } from 'react-icons/ai'; -import { MdClosedCaption, MdClosedCaptionDisabled, MdGridOff, MdGridOn, MdSubtitles, MdSubtitlesOff, MdTouchApp } from 'react-icons/md'; -import { TbEditCircle, TbEditCircleOff, TbHandOff, TbHandStop, TbHighlight, TbHighlightOff } from 'react-icons/tb'; -import { BiHide, BiShow } from 'react-icons/bi'; -import { BsGrid3X3GapFill } from 'react-icons/bs'; -import { TfiBarChart } from 'react-icons/tfi'; -import { CiGrid31 } from 'react-icons/ci'; -import { RxWidth } from 'react-icons/rx'; -import { Dropdown, DropdownType, IListItemProps, Toggle, ToggleType, Type } from 'browndash-components'; -import { SettingsManager } from '../util/SettingsManager'; enum UtilityButtonState { Default, @@ -61,8 +56,8 @@ export class PropertiesButtons extends React.Component<{}, {}> { text={label(targetDoc?.[property])} color={SettingsManager.userColor} icon={icon(targetDoc?.[property] as any)} - iconPlacement={'left'} - align={'flex-start'} + iconPlacement="left" + align="flex-start" fillWidth={true} toggleType={ToggleType.BUTTON} onClick={undoable(() => { @@ -173,6 +168,16 @@ export class PropertiesButtons extends React.Component<{}, {}> { ); } + @computed get verticalAlignButton() { + //select text + return this.propertyToggleBtn( + on => (on ? 'ALIGN TOP' : 'ALIGN CENTER'), + '_layout_centered', + on => `${on ? 'Text is aligned with top of document' : 'Text is aligned with center of document'} `, + on => <MdTouchApp /> // 'eye' + ); + } + @computed get fitContentButton() { return this.propertyToggleBtn( on => (on ? 'PREVIOUS VIEW' : 'VIEW ALL'), //'View All', @@ -487,7 +492,7 @@ export class PropertiesButtons extends React.Component<{}, {}> { render() { const layoutField = this.selectedDoc?.[Doc.LayoutFieldKey(this.selectedDoc)]; const isText = layoutField instanceof RichTextField; - const isInk = layoutField instanceof InkField; + const isInk = this.selectedDoc?.layout_isSvg; const isImage = layoutField instanceof ImageField; const isMap = this.selectedDoc?.type === DocumentType.MAP; const isCollection = this.selectedDoc?.type === DocumentType.COL; @@ -512,10 +517,11 @@ export class PropertiesButtons extends React.Component<{}, {}> { {toggle(this.layout_fitWidthButton)} {/* {toggle(this.freezeThumb)} */} {toggle(this.forceActiveButton)} + {toggle(this.verticalAlignButton, { display: !isText ? 'none' : '' })} {toggle(this.fitContentButton, { display: !isFreeForm && !isMap ? 'none' : '' })} {/* {toggle(this.isLightboxButton, { display: !isFreeForm && !isMap ? 'none' : '' })} */} {toggle(this.layout_autoHeightButton, { display: !isText && !isStacking && !isTree ? 'none' : '' })} - {toggle(this.maskButton, { display: !isInk ? 'none' : '' })} + {toggle(this.maskButton, { display: isNovice || !isInk ? 'none' : '' })} {toggle(this.hideImageButton, { display: !isImage ? 'none' : '' })} {toggle(this.chromeButton, { display: isNovice ? 'none' : '' })} {toggle(this.gridButton, { display: !isCollection ? 'none' : '' })} diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index 04a948a27..d37971517 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -73,9 +73,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @computed get isPres(): boolean { return this.selectedDoc?.type === DocumentType.PRES; } - @computed get isLink(): boolean { - return this.selectedDoc?.type === DocumentType.LINK; - } @computed get dataDoc() { return this.selectedDoc?.[DocData]; } @@ -1174,12 +1171,10 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { } @computed get inkSubMenu() { - let isDouble = false; - return ( <> <PropertiesSection title="Appearance" isOpen={this.openAppearance} setIsOpen={bool => (this.openAppearance = bool)} onDoubleClick={() => this.CloseAll()}> - {this.isInk ? this.appearanceEditor : null} + {this.selectedDoc?.layout_isSvg ? this.appearanceEditor : null} </PropertiesSection> <PropertiesSection title="Transform" isOpen={this.openTransform} setIsOpen={bool => (this.openTransform = bool)} onDoubleClick={() => this.CloseAll()}> {this.transformEditor} diff --git a/src/client/views/StyleProvider.scss b/src/client/views/StyleProvider.scss index f069e7e1b..4d3096f71 100644 --- a/src/client/views/StyleProvider.scss +++ b/src/client/views/StyleProvider.scss @@ -12,7 +12,7 @@ opacity: 0.3; display: flex; flex-direction: column; - color: gold; + color: red; border-radius: 3px; justify-content: center; cursor: default; diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index 6ee96de5b..c6d3efd0c 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -1,10 +1,11 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; -import { Dropdown, DropdownType, IconButton, IListItemProps, ListBox, ListItem, Popup, Shadows, Size, Type } from 'browndash-components'; +import { Dropdown, DropdownType, IconButton, IListItemProps, Shadows, Size, Type } from 'browndash-components'; import { action, runInAction, untracked } from 'mobx'; import { extname } from 'path'; import { BsArrowDown, BsArrowDownUp, BsArrowUp } from 'react-icons/bs'; +import { FaFilter } from 'react-icons/fa'; import { Doc, Opt, StrListCast } from '../../fields/Doc'; import { BoolCast, Cast, DocCast, ImageCast, NumCast, StrCast } from '../../fields/Types'; import { DashColor, lightOrDark, Utils } from '../../Utils'; @@ -13,18 +14,16 @@ import { DocFocusOrOpen, DocumentManager } from '../util/DocumentManager'; import { IsFollowLinkScript } from '../util/LinkFollower'; import { LinkManager } from '../util/LinkManager'; import { SelectionManager } from '../util/SelectionManager'; -import { ColorScheme, SettingsManager } from '../util/SettingsManager'; +import { SettingsManager } from '../util/SettingsManager'; import { undoBatch, UndoManager } from '../util/UndoManager'; import { TreeSort } from './collections/TreeSort'; import { Colors } from './global/globalEnums'; -import { InkingStroke } from './InkingStroke'; import { DocumentView, DocumentViewProps } from './nodes/DocumentView'; import { FieldViewProps } from './nodes/FieldView'; import { KeyValueBox } from './nodes/KeyValueBox'; +import { PropertiesView } from './PropertiesView'; import './StyleProvider.scss'; import React = require('react'); -import { PropertiesView } from './PropertiesView'; -import { FaFilter } from 'react-icons/fa'; export enum StyleProp { TreeViewIcon = 'treeView_Icon', @@ -84,13 +83,15 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps const selected = property.includes(':selected'); const isCaption = property.includes(':caption'); const isAnchor = property.includes(':anchor'); + const isContent = property.includes(':content'); const isAnnotated = property.includes(':annotated'); - const isInk = () => doc && StrCast(Doc.Layout(doc).layout).includes(InkingStroke.name) && !props?.LayoutTemplateString; + const isInk = () => doc?._layout_isSvg && !props?.LayoutTemplateString; const isOpen = property.includes(':open'); const isEmpty = property.includes(':empty'); const boxBackground = property.includes(':box'); const fieldKey = props?.fieldKey ? props.fieldKey + '_' : isCaption ? 'caption_' : ''; const lockedPosition = () => doc && BoolCast(doc._lockedPosition); + const titleHeight = () => props?.styleProvider?.(doc, props, StyleProp.TitleHeight); const backgroundCol = () => props?.styleProvider?.(doc, props, StyleProp.BackgroundColor); const color = () => props?.styleProvider?.(doc, props, StyleProp.Color); const opacity = () => props?.styleProvider?.(doc, props, StyleProp.Opacity); @@ -121,14 +122,14 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps const highlightStyle = ['solid', 'dashed', 'solid', 'solid', 'solid'][highlightIndex]; if (highlightIndex) { return { - highlightStyle, + highlightStyle: doc._isGroup ? "dotted": highlightStyle, highlightColor, highlightIndex, - highlightStroke: doc.type === DocumentType.INK, + highlightStroke: doc.layout_isSvg, }; } } - return undefined; + return undefined; case StyleProp.DocContents:return undefined; case StyleProp.WidgetColor:return isAnnotated ? Colors.LIGHT_BLUE : 'dimgrey'; case StyleProp.Opacity: return props?.LayoutTemplateString?.includes(KeyValueBox.name) ? 1 : doc?.text_inlineAnnotations ? 0 : Cast(doc?._opacity, "number", Cast(doc?.opacity, 'number', null)); @@ -138,7 +139,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps case StyleProp.FontWeight: return StrCast(doc?.[fieldKey + 'fontWeight'], StrCast(doc?._text_fontWeight, StrCast(Doc.UserDoc().fontWeight))); case StyleProp.FillColor: return StrCast(doc?._fillColor, StrCast(doc?.fillColor, 'transparent')); case StyleProp.ShowCaption:return doc?._type_collection === CollectionViewType.Carousel || props?.hideCaptions ? undefined : StrCast(doc?._layout_showCaption); - case StyleProp.TitleHeight:return 15; + case StyleProp.TitleHeight:return (props?.ScreenToLocalTransform().Scale ?? 1)*(props?.NativeDimScaling?.()??1) * NumCast(Doc.UserDoc().headerHeight,30) case StyleProp.ShowTitle: return ( (doc && @@ -171,7 +172,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps case StyleProp.BorderPath: const borderPath = Doc.IsComicStyle(doc) && props?.renderDepth && - doc?.type !== DocumentType.INK && { path: wavyBorderPath(props?.PanelWidth?.() || 0, props?.PanelHeight?.() || 0), fill: wavyBorderPath(props?.PanelWidth?.() || 0, props?.PanelHeight?.() || 0, 0.08), width: 3 }; + !doc?.layout_isSvg && { path: wavyBorderPath(props?.PanelWidth?.() || 0, props?.PanelHeight?.() || 0), fill: wavyBorderPath(props?.PanelWidth?.() || 0, props?.PanelHeight?.() || 0, 0.08), width: 3 }; return !borderPath ? null : { @@ -190,7 +191,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps doc?.type === DocumentType.LABEL) && layout_showTitle() && !StrCast(doc?.layout_showTitle).includes(':hover') - ? 15 + ? titleHeight() : 0; case StyleProp.BackgroundColor: { if (DocumentView.LastPressedSidebarBtn === doc) return SettingsManager.userColor; // hack to indicate active menu panel item @@ -201,11 +202,11 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps case DocumentType.PRES: docColor = docColor || 'transparent'; break; case DocumentType.FONTICON: docColor = boxBackground ? undefined : docColor || Colors.DARK_GRAY; break; case DocumentType.RTF: docColor = docColor || Colors.LIGHT_GRAY; break; + case DocumentType.LINK: docColor = (isAnchor ? docColor : undefined); break; case DocumentType.INK: docColor = doc?.stroke_isInkMask ? 'rgba(0,0,0,0.7)' : undefined; break; case DocumentType.EQUATION: docColor = docColor || 'transparent'; break; case DocumentType.LABEL: docColor = docColor || Colors.LIGHT_GRAY; break; case DocumentType.BUTTON: docColor = docColor || Colors.LIGHT_GRAY; break; - case DocumentType.LINK: docColor = (isAnchor ? docColor : '') || 'transparent'; break; case DocumentType.IMG: case DocumentType.WEB: case DocumentType.PDF: @@ -231,7 +232,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps case StyleProp.BoxShadow: { if (!doc || opacity() === 0 || doc.noShadow) return undefined; // if it's not visible, then no shadow) if (doc.layout_boxShadow === 'standard') return Shadows.STANDARD_SHADOW; - if (IsFollowLinkScript(doc?.onClick) && LinkManager.Links(doc).length && ![DocumentType.LINK, DocumentType.INK].includes(doc.type as any)) return StrCast(doc?._linkButtonShadow, 'lightblue 0em 0em 1em'); + if (IsFollowLinkScript(doc?.onClick) && LinkManager.Links(doc).length && !doc.layout_isSvg) return StrCast(doc?._linkButtonShadow, 'lightblue 0em 0em 1em'); switch (doc?.type) { case DocumentType.COL: return StrCast( @@ -240,7 +241,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps ? '4px 4px 10px 2px' : lockedPosition() || doc?._isGroup || docProps?.LayoutTemplateString ? undefined // groups have no drop shadow -- they're supposed to be "invisible". LayoutString's imply collection is being rendered as something else (e.g., title of a Slide) - : `${Colors.MEDIUM_GRAY} ${StrCast(doc.layout_boxShadow, '0.2vw 0.2vw 0.8vw')}` + : `${Colors.DARK_GRAY} ${StrCast(doc.layout_boxShadow, '0.2vw 0.2vw 0.8vw')}` ); case DocumentType.LABEL: @@ -261,17 +262,17 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps if (StrCast(doc?.pointerEvents) && !props?.LayoutTemplateString?.includes(KeyValueBox.name)) return StrCast(doc!.pointerEvents); // honor pointerEvents field (set by lock button usually) if it's not a keyValue view of the Doc if (docProps?.DocumentView?.().ComponentView?.overridePointerEvents?.() !== undefined) return docProps?.DocumentView?.().ComponentView?.overridePointerEvents?.(); if (DocumentView.ExploreMode || doc?.layout_unrendered) return isInk() ? 'visiblePainted' : 'all'; - if (props?.contentPointerEvents) return StrCast(props.contentPointerEvents); if (props?.pointerEvents?.() === 'none') return 'none'; if (opacity() === 0) return 'none'; - if (props?.isDocumentActive?.()) return isInk() ? 'visiblePainted' : 'all'; + if (props?.isGroupActive?.() ) return isInk() ? 'visiblePainted': (doc?._isGroup )? undefined: 'all' + if (props?.isDocumentActive?.() && !props.treeViewDoc) return isInk() ? 'visiblePainted' : 'all'; return undefined; // fixes problem with tree view elements getting pointer events when the tree view is not active case StyleProp.Decorations: const lock = () => { if (props?.docViewPath().lastElement()?.rootDoc?._type_collection === CollectionViewType.Freeform) { return doc?.pointerEvents !== 'none' ? null : ( <div className="styleProvider-lock" onClick={() => toggleLockedPosition(doc)}> - <FontAwesomeIcon icon='lock' style={{ color: 'red' }} size="lg" /> + <FontAwesomeIcon icon='lock' size="lg" /> </div> ); } diff --git a/src/client/views/animationtimeline/Keyframe.scss b/src/client/views/animationtimeline/Region.scss index 38eb103c6..f7476ab55 100644 --- a/src/client/views/animationtimeline/Keyframe.scss +++ b/src/client/views/animationtimeline/Region.scss @@ -1,5 +1,4 @@ -@import "./../global/globalCssVariables.scss"; - +@import './../global/globalCssVariables.scss'; $timelineColor: #9acedf; $timelineDark: #77a1aa; @@ -9,7 +8,7 @@ $timelineDark: #77a1aa; width: 5px; position: absolute; - // pointer-events: none; + // pointer-events: none; .menubox { width: 200px; height: 200px; @@ -27,11 +26,15 @@ $timelineDark: #77a1aa; .leftResize { left: -10px; border: 3px solid black; + transform: rotate(45deg) scale(0.25) !important; + background-color: black !important; } .rightResize { right: -10px; border: 3px solid black; + transform: rotate(45deg) scale(0.25) !important; + background-color: black !important; } .keyframe-indicator { @@ -100,6 +103,4 @@ $timelineDark: #77a1aa; background-color: rgba(0, 0, 0, 0.5); opacity: 0; } - - -}
\ No newline at end of file +} diff --git a/src/client/views/animationtimeline/Keyframe.tsx b/src/client/views/animationtimeline/Region.tsx index addc00c85..53c5c4718 100644 --- a/src/client/views/animationtimeline/Keyframe.tsx +++ b/src/client/views/animationtimeline/Region.tsx @@ -5,19 +5,16 @@ import { Doc, DocListCast, Opt } from '../../../fields/Doc'; import { List } from '../../../fields/List'; import { createSchema, defaultSpec, listSpec, makeInterface } from '../../../fields/Schema'; import { Cast, NumCast } from '../../../fields/Types'; -import { Docs } from '../../documents/Documents'; import { Transform } from '../../util/Transform'; -import { CollectionDockingView } from '../collections/CollectionDockingView'; import '../global/globalCssVariables.scss'; -import { OpenWhereMod } from '../nodes/DocumentView'; -import './Keyframe.scss'; +import './Region.scss'; import './Timeline.scss'; import { TimelineMenu } from './TimelineMenu'; /** * Useful static functions that you can use. Mostly for logic, but you can also add UI logic here also */ -export namespace KeyframeFunc { +export namespace RegionHelpers { export enum KeyframeType { end = 'end', fade = 'fade', @@ -29,7 +26,7 @@ export namespace KeyframeFunc { right = 'right', } - export const findAdjacentRegion = (dir: KeyframeFunc.Direction, currentRegion: Doc, regions: Doc[]): RegionData | undefined => { + export const findAdjacentRegion = (dir: RegionHelpers.Direction, currentRegion: Doc, regions: Doc[]): RegionData | undefined => { let leftMost: RegionData | undefined = undefined; let rightMost: RegionData | undefined = undefined; regions.forEach(region => { @@ -128,10 +125,11 @@ interface IProps { collection: Doc; tickSpacing: number; tickIncrement: number; + saveStateKf: Doc | undefined; time: number; changeCurrentBarX: (x: number) => void; transform: Transform; - makeKeyData: (region: RegionData, pos: number, kftype: KeyframeFunc.KeyframeType) => Doc; + makeKeyData: (region: RegionData, pos: number, kftype: RegionHelpers.KeyframeType) => Doc; } /** @@ -158,7 +156,7 @@ interface IProps { * @author Andrew Kim */ @observer -export class Keyframe extends React.Component<IProps> { +export class Region extends React.Component<IProps> { @observable private _bar = React.createRef<HTMLDivElement>(); @observable private _mouseToggled = false; @observable private _doubleClickEnabled = false; @@ -173,16 +171,16 @@ export class Keyframe extends React.Component<IProps> { return DocListCast(this.regiondata.keyframes); } @computed private get pixelPosition() { - return KeyframeFunc.convertPixelTime(this.regiondata.position, 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement); + return RegionHelpers.convertPixelTime(this.regiondata.position, 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement); } @computed private get pixelDuration() { - return KeyframeFunc.convertPixelTime(this.regiondata.duration, 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement); + return RegionHelpers.convertPixelTime(this.regiondata.duration, 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement); } @computed private get pixelFadeIn() { - return KeyframeFunc.convertPixelTime(this.regiondata.fadeIn, 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement); + return RegionHelpers.convertPixelTime(this.regiondata.fadeIn, 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement); } @computed private get pixelFadeOut() { - return KeyframeFunc.convertPixelTime(this.regiondata.fadeOut, 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement); + return RegionHelpers.convertPixelTime(this.regiondata.fadeOut, 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement); } constructor(props: any) { @@ -192,10 +190,10 @@ export class Keyframe extends React.Component<IProps> { setTimeout(() => { //giving it a temporary 1sec delay... if (!this.regiondata.keyframes) this.regiondata.keyframes = new List<Doc>(); - const start = this.props.makeKeyData(this.regiondata, this.regiondata.position, KeyframeFunc.KeyframeType.end); - const fadeIn = this.props.makeKeyData(this.regiondata, this.regiondata.position + this.regiondata.fadeIn, KeyframeFunc.KeyframeType.fade); - const fadeOut = this.props.makeKeyData(this.regiondata, this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut, KeyframeFunc.KeyframeType.fade); - const finish = this.props.makeKeyData(this.regiondata, this.regiondata.position + this.regiondata.duration, KeyframeFunc.KeyframeType.end); + const start = this.props.makeKeyData(this.regiondata, this.regiondata.position, RegionHelpers.KeyframeType.end); + const fadeIn = this.props.makeKeyData(this.regiondata, this.regiondata.position + this.regiondata.fadeIn, RegionHelpers.KeyframeType.fade); + const fadeOut = this.props.makeKeyData(this.regiondata, this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut, RegionHelpers.KeyframeType.fade); + const finish = this.props.makeKeyData(this.regiondata, this.regiondata.position + this.regiondata.duration, RegionHelpers.KeyframeType.end); fadeIn.opacity = 1; fadeOut.opacity = 1; start.opacity = 0.1; @@ -233,10 +231,10 @@ export class Keyframe extends React.Component<IProps> { if (e.movementX !== 0) { this._mouseToggled = true; } - const left = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.left, this.regiondata, this.regions)!; - const right = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.right, this.regiondata, this.regions)!; + const left = RegionHelpers.findAdjacentRegion(RegionHelpers.Direction.left, this.regiondata, this.regions)!; + const right = RegionHelpers.findAdjacentRegion(RegionHelpers.Direction.right, this.regiondata, this.regions)!; const prevX = this.regiondata.position; - const futureX = this.regiondata.position + KeyframeFunc.convertPixelTime(e.movementX, 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement); + const futureX = this.regiondata.position + RegionHelpers.convertPixelTime(e.movementX, 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement); if (futureX <= 0) { this.regiondata.position = 0; } else if (left && left.position + left.duration >= futureX) { @@ -275,8 +273,8 @@ export class Keyframe extends React.Component<IProps> { e.preventDefault(); e.stopPropagation(); const bar = this._bar.current!; - const offset = KeyframeFunc.convertPixelTime(Math.round((e.clientX - bar.getBoundingClientRect().left) * this.props.transform.Scale), 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement); - const leftRegion = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.left, this.regiondata, this.regions); + const offset = RegionHelpers.convertPixelTime(Math.round((e.clientX - bar.getBoundingClientRect().left) * this.props.transform.Scale), 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement); + const leftRegion = RegionHelpers.findAdjacentRegion(RegionHelpers.Direction.left, this.regiondata, this.regions); if (leftRegion && this.regiondata.position + offset <= leftRegion.position + leftRegion.duration) { this.regiondata.position = leftRegion.position + leftRegion.duration; this.regiondata.duration = NumCast(this.keyframes[this.keyframes.length - 1].time) - (leftRegion.position + leftRegion.duration); @@ -299,8 +297,8 @@ export class Keyframe extends React.Component<IProps> { e.preventDefault(); e.stopPropagation(); const bar = this._bar.current!; - const offset = KeyframeFunc.convertPixelTime(Math.round((e.clientX - bar.getBoundingClientRect().right) * this.props.transform.Scale), 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement); - const rightRegion = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.right, this.regiondata, this.regions); + const offset = RegionHelpers.convertPixelTime(Math.round((e.clientX - bar.getBoundingClientRect().right) * this.props.transform.Scale), 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement); + const rightRegion = RegionHelpers.findAdjacentRegion(RegionHelpers.Direction.right, this.regiondata, this.regions); const fadeOutKeyframeTime = NumCast(this.keyframes[this.keyframes.length - 3].time); if (this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut + offset <= fadeOutKeyframeTime) { //case 1: when third to last keyframe is in the way @@ -315,24 +313,24 @@ export class Keyframe extends React.Component<IProps> { }; @action - createKeyframe = async (clientX: number) => { + createKeyframe = (clientX: number) => { this._mouseToggled = true; const bar = this._bar.current!; - const offset = KeyframeFunc.convertPixelTime(Math.round((clientX - bar.getBoundingClientRect().left) * this.props.transform.Scale), 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement); + const offset = RegionHelpers.convertPixelTime(Math.round((clientX - bar.getBoundingClientRect().left) * this.props.transform.Scale), 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement); if (offset > this.regiondata.fadeIn && offset < this.regiondata.duration - this.regiondata.fadeOut) { //make sure keyframe is not created inbetween fades and ends const position = this.regiondata.position; - this.props.makeKeyData(this.regiondata, Math.round(position + offset), KeyframeFunc.KeyframeType.default); + this.props.makeKeyData(this.regiondata, Math.round(position + offset), RegionHelpers.KeyframeType.default); this.regiondata.hasData = true; - this.props.changeCurrentBarX(KeyframeFunc.convertPixelTime(Math.round(position + offset), 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement)); //first move the keyframe to the correct location and make a copy so the correct file gets coppied + this.props.changeCurrentBarX(RegionHelpers.convertPixelTime(Math.round(position + offset), 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement)); //first move the keyframe to the correct location and make a copy so the correct file gets coppied } }; @action - moveKeyframe = async (e: React.MouseEvent, kf: Doc) => { + moveKeyframe = (e: React.MouseEvent, kf: Doc) => { e.preventDefault(); e.stopPropagation(); - this.props.changeCurrentBarX(KeyframeFunc.convertPixelTime(NumCast(kf.time!), 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement)); + this.props.changeCurrentBarX(RegionHelpers.convertPixelTime(NumCast(kf.time!), 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement)); }; /** @@ -340,17 +338,14 @@ export class Keyframe extends React.Component<IProps> { */ @action makeKeyframeMenu = (kf: Doc, e: MouseEvent) => { - TimelineMenu.Instance.addItem('button', 'Toggle Fade Only', () => { - kf.type = kf.type === KeyframeFunc.KeyframeType.fade ? KeyframeFunc.KeyframeType.default : KeyframeFunc.KeyframeType.fade; - }), - TimelineMenu.Instance.addItem( - 'button', - 'Delete', - action(() => { - (this.regiondata.keyframes as List<Doc>).splice(this.keyframes.indexOf(kf), 1); - this.forceUpdate(); - }) - ), + TimelineMenu.Instance.addItem( + 'button', + 'Delete', + action(() => { + (this.regiondata.keyframes as List<Doc>).splice(this.keyframes.indexOf(kf), 1); + this.forceUpdate(); + }) + ), TimelineMenu.Instance.addItem( 'input', 'Move', @@ -362,7 +357,10 @@ export class Keyframe extends React.Component<IProps> { } if (!cannotMove) { this.keyframes[kfIndex].time = parseInt(val, 10); - this.keyframes[1].time = this.regiondata.position + this.regiondata.fadeIn; + if (kfIndex === 1) { + this.regiondata.fadeIn = parseInt(val, 10) - this.regiondata.position; + } + // this.keyframes[1].time = this.regiondata.position + this.regiondata.fadeIn; } }) ); @@ -485,38 +483,34 @@ export class Keyframe extends React.Component<IProps> { drawKeyframes = () => { const keyframeDivs: JSX.Element[] = []; return DocListCast(this.regiondata.keyframes).map(kf => { - if ((kf.type as KeyframeFunc.KeyframeType) !== KeyframeFunc.KeyframeType.end) { - return ( - <> - <div className="keyframe" style={{ left: `${KeyframeFunc.convertPixelTime(NumCast(kf.time), 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement) - this.pixelPosition}px` }}> - <div className="divider"></div> - <div - className="keyframeCircle keyframe-indicator" - onPointerDown={e => { - e.preventDefault(); - e.stopPropagation(); - this.moveKeyframe(e, kf); - }} - onContextMenu={(e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - this.makeKeyframeMenu(kf, e.nativeEvent); - }} - onDoubleClick={e => { - e.preventDefault(); - e.stopPropagation(); - }}></div> - </div> - <div className="keyframe-information" /> - </> - ); - } else { - return ( - <div className="keyframe" style={{ left: `${KeyframeFunc.convertPixelTime(NumCast(kf.time), 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement) - this.pixelPosition}px` }}> - <div className="divider" /> + return ( + <> + <div className="keyframe" style={{ left: `${RegionHelpers.convertPixelTime(NumCast(kf.time), 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement) - this.pixelPosition}px` }}> + <div className="divider"></div> + <div + className="keyframeCircle keyframe-indicator" + style={{ + borderColor: this.props.saveStateKf === kf ? 'red' : undefined, + }} + onPointerDown={e => { + e.preventDefault(); + e.stopPropagation(); + this.moveKeyframe(e, kf); + }} + onContextMenu={(e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + this.makeKeyframeMenu(kf, e.nativeEvent); + }} + onDoubleClick={e => { + e.preventDefault(); + e.stopPropagation(); + }} + /> </div> - ); - } + <div className="keyframe-information" /> + </> + ); }); }; @@ -531,8 +525,8 @@ export class Keyframe extends React.Component<IProps> { if (index !== this.keyframes.length - 1) { const right = this.keyframes[index + 1]; const bodyRef = React.createRef<HTMLDivElement>(); - const kfPos = KeyframeFunc.convertPixelTime(NumCast(kf.time), 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement); - const rightPos = KeyframeFunc.convertPixelTime(NumCast(right.time), 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement); + const kfPos = RegionHelpers.convertPixelTime(NumCast(kf.time), 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement); + const rightPos = RegionHelpers.convertPixelTime(NumCast(right.time), 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement); keyframeDividers.push( <div ref={bodyRef} @@ -579,12 +573,12 @@ export class Keyframe extends React.Component<IProps> { }%, rgba(154, 206, 223, 0) 100% )`, }} onPointerDown={this.onBarPointerDown}> + {this.drawKeyframes()} + {this.drawKeyframeDividers()} <div className="leftResize keyframe-indicator" onPointerDown={this.onResizeLeft}></div> {/* <div className="keyframe-information"></div> */} <div className="rightResize keyframe-indicator" onPointerDown={this.onResizeRight}></div> {/* <div className="keyframe-information"></div> */} - {this.drawKeyframes()} - {this.drawKeyframeDividers()} </div> ); } diff --git a/src/client/views/animationtimeline/Timeline.tsx b/src/client/views/animationtimeline/Timeline.tsx index 7ca13756a..4be3b05ab 100644 --- a/src/client/views/animationtimeline/Timeline.tsx +++ b/src/client/views/animationtimeline/Timeline.tsx @@ -1,34 +1,41 @@ +import { IconLookup } from '@fortawesome/fontawesome-svg-core'; import { faBackward, faForward, faGripLines, faPauseCircle, faPlayCircle } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable, runInAction, trace } from 'mobx'; +import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast } from '../../../fields/Doc'; -import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types'; -import { Utils, setupMoveUpEvents, emptyFunction, returnFalse } from '../../../Utils'; +import { BoolCast, NumCast, StrCast } from '../../../fields/Types'; +import { emptyFunction, setupMoveUpEvents, Utils } from '../../../Utils'; +import { DocumentType } from '../../documents/DocumentTypes'; +import clamp from '../../util/clamp'; import { FieldViewProps } from '../nodes/FieldView'; -import { KeyframeFunc } from './Keyframe'; +import { RegionHelpers } from './Region'; import './Timeline.scss'; import { TimelineOverview } from './TimelineOverview'; import { Track } from './Track'; -import clamp from '../../util/clamp'; -import { DocumentType } from '../../documents/DocumentTypes'; -import { IconLookup } from '@fortawesome/fontawesome-svg-core'; /** - * Timeline class controls most of timeline functions besides individual keyframe and track mechanism. Main functions are + * Timeline class controls most of timeline functions besides individual region and track mechanism. Main functions are * zooming, panning, currentBarX (scrubber movement). Most of the UI stuff is also handled here. You shouldn't really make * any logical changes here. Most work is needed on UI. * * The hierarchy works this way: * - * Timeline.tsx --> Track.tsx --> Keyframe.tsx + * Timeline.tsx --> Track.tsx --> Region.tsx | | | TimelineMenu.tsx (timeline's custom contextmenu) | | TimelineOverview.tsx (youtube like dragging thing is play mode, complex dragging thing in editing mode) + + Timeline (Track[]) + Track(Region[],animatedDoc) -> Region1(K[]) Region2 ... + F1 K1 K2...FL K1 K2 K... + K(x,y,_width,opacity) + ... + Track Most style changes are in SCSS file. If you have any questions, email me or text me. @@ -58,7 +65,6 @@ export class Timeline extends React.Component<FieldViewProps> { //boolean vars and instance vars @observable private _currentBarX: number = 0; @observable private _windSpeed: number = 1; - @observable private _isPlaying: boolean = false; //scrubber playing @observable private _totalLength: number = 0; @observable private _visibleLength: number = 0; @observable private _visibleStart: number = 0; @@ -69,6 +75,8 @@ export class Timeline extends React.Component<FieldViewProps> { @observable private _playButton = faPlayCircle; @observable private _titleHeight = 0; + @observable public IsPlaying: boolean = false; //scrubber playing + /** * collection get method. Basically defines what defines collection's children. These will be tracked in the timeline. Do not edit. */ @@ -144,14 +152,17 @@ export class Timeline extends React.Component<FieldViewProps> { @action play = () => { const playTimeline = () => { - if (this._isPlaying) { + if (this.IsPlaying) { this.changeCurrentBarX(this._currentBarX >= this._totalLength ? 0 : this._currentBarX + this._windSpeed); setTimeout(playTimeline, 15); } }; - this._isPlaying = !this._isPlaying; - this._playButton = this._isPlaying ? faPauseCircle : faPlayCircle; - this._isPlaying && playTimeline(); + Array.from(this.mapOfTracks.values()) + .filter(key => key) + .forEach(key => key!.saveKeyframe()); + this.IsPlaying = !this.IsPlaying; + this._playButton = this.IsPlaying ? faPauseCircle : faPlayCircle; + this.IsPlaying && playTimeline(); }; /** @@ -221,7 +232,7 @@ export class Timeline extends React.Component<FieldViewProps> { if (this._visibleStart + this._visibleLength + 20 >= this._totalLength) { this._visibleStart -= e.movementX; this._totalLength -= e.movementX; - this._time -= KeyframeFunc.convertPixelTime(e.movementX, 'mili', 'time', this._tickSpacing, this._tickIncrement); + this._time -= RegionHelpers.convertPixelTime(e.movementX, 'mili', 'time', this._tickSpacing, this._tickIncrement); this.props.Document.AnimationLength = this._time; } return false; @@ -278,11 +289,11 @@ export class Timeline extends React.Component<FieldViewProps> { e.preventDefault(); e.stopPropagation(); const offset = e.clientX - this._infoContainer.current!.getBoundingClientRect().left; - const prevTime = KeyframeFunc.convertPixelTime(this._visibleStart + offset, 'mili', 'time', this._tickSpacing, this._tickIncrement); - const prevCurrent = KeyframeFunc.convertPixelTime(this._currentBarX, 'mili', 'time', this._tickSpacing, this._tickIncrement); + const prevTime = RegionHelpers.convertPixelTime(this._visibleStart + offset, 'mili', 'time', this._tickSpacing, this._tickIncrement); + const prevCurrent = RegionHelpers.convertPixelTime(this._currentBarX, 'mili', 'time', this._tickSpacing, this._tickIncrement); this.zoom(e.deltaY < 0); - const currPixel = KeyframeFunc.convertPixelTime(prevTime, 'mili', 'pixel', this._tickSpacing, this._tickIncrement); - const currCurrent = KeyframeFunc.convertPixelTime(prevCurrent, 'mili', 'pixel', this._tickSpacing, this._tickIncrement); + const currPixel = RegionHelpers.convertPixelTime(prevTime, 'mili', 'pixel', this._tickSpacing, this._tickIncrement); + const currCurrent = RegionHelpers.convertPixelTime(prevCurrent, 'mili', 'pixel', this._tickSpacing, this._tickIncrement); this._infoContainer.current!.scrollLeft = currPixel - offset; this._visibleStart = currPixel - offset > 0 ? currPixel - offset : 0; this._visibleStart += this._visibleLength + this._visibleStart > this._totalLength ? this._totalLength - (this._visibleStart + this._visibleLength) : 0; @@ -478,7 +489,7 @@ export class Timeline extends React.Component<FieldViewProps> { // @computed getCurrentTime = () => { - const current = KeyframeFunc.convertPixelTime(this._currentBarX, 'mili', 'time', this._tickSpacing, this._tickIncrement); + const current = RegionHelpers.convertPixelTime(this._currentBarX, 'mili', 'time', this._tickSpacing, this._tickIncrement); return this.toReadTime(current > this._time ? this._time : current); }; @@ -505,13 +516,13 @@ export class Timeline extends React.Component<FieldViewProps> { @action toAuthoring = () => { this._time = Math.ceil((this.findLongestTime() ?? 1) / 100000) * 100000; - this._totalLength = KeyframeFunc.convertPixelTime(this._time, 'mili', 'pixel', this._tickSpacing, this._tickIncrement); + this._totalLength = RegionHelpers.convertPixelTime(this._time, 'mili', 'pixel', this._tickSpacing, this._tickIncrement); }; @action toPlay = () => { this._time = this.findLongestTime(); - this._totalLength = KeyframeFunc.convertPixelTime(this._time, 'mili', 'pixel', this._tickSpacing, this._tickIncrement); + this._totalLength = RegionHelpers.convertPixelTime(this._time, 'mili', 'pixel', this._tickSpacing, this._tickIncrement); }; /** @@ -532,9 +543,10 @@ export class Timeline extends React.Component<FieldViewProps> { <div key="timeline_scrubberhead" className="scrubberhead" onPointerDown={this.onScrubberDown}></div> </div> <div key="timeline_trackbox" className="trackbox" ref={this._trackbox} style={{ width: `${this._totalLength}px` }}> - {this.children.map(doc => ( + {[...this.children, this.props.Document].map(doc => ( <Track ref={ref => this.mapOfTracks.push(ref)} + timeline={this} animatedDoc={doc} currentBarX={this._currentBarX} changeCurrentBarX={this.changeCurrentBarX} @@ -550,16 +562,8 @@ export class Timeline extends React.Component<FieldViewProps> { </div> <div className="currentTime">Current: {this.getCurrentTime()}</div> <div key="timeline_title" className="title-container" ref={this._titleContainer}> - {this.children.map(doc => ( - <div - style={{ height: `${this._titleHeight}px` }} - className="datapane" - onPointerOver={() => { - Doc.BrushDoc(doc); - }} - onPointerOut={() => { - Doc.UnBrushDoc(doc); - }}> + {[...this.children, this.props.Document].map(doc => ( + <div style={{ height: `${this._titleHeight}px` }} className="datapane" onPointerOver={() => Doc.BrushDoc(doc)} onPointerOut={() => Doc.UnBrushDoc(doc)}> <p>{StrCast(doc.title)}</p> </div> ))} diff --git a/src/client/views/animationtimeline/TimelineMenu.tsx b/src/client/views/animationtimeline/TimelineMenu.tsx index aa422c092..1769c41bd 100644 --- a/src/client/views/animationtimeline/TimelineMenu.tsx +++ b/src/client/views/animationtimeline/TimelineMenu.tsx @@ -1,12 +1,11 @@ -import * as React from "react"; -import { observable, action, runInAction } from "mobx"; -import { observer } from "mobx-react"; -import "./TimelineMenu.scss"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faChartLine, faRoad, faClipboard, faPen, faTrash, faTable } from "@fortawesome/free-solid-svg-icons"; -import { Utils } from "../../../Utils"; -import { IconLookup } from "@fortawesome/fontawesome-svg-core"; - +import { IconLookup } from '@fortawesome/fontawesome-svg-core'; +import { faChartLine, faClipboard } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { Utils } from '../../../Utils'; +import './TimelineMenu.scss'; @observer export class TimelineMenu extends React.Component { @@ -25,9 +24,9 @@ export class TimelineMenu extends React.Component { @action openMenu = (x?: number, y?: number) => { this._opacity = 1; - x ? this._x = x : this._x = 0; - y ? this._y = y : this._y = 0; - } + x ? (this._x = x) : (this._x = 0); + y ? (this._y = y) : (this._y = 0); + }; @action closeMenu = () => { @@ -35,44 +34,67 @@ export class TimelineMenu extends React.Component { this._currentMenu = []; this._x = -1000000; this._y = -1000000; - } + }; @action - addItem = (type: "input" | "button", title: string, event: (e: any, ...args: any[]) => void) => { - if (type === "input") { + addItem = (type: 'input' | 'button', title: string, event: (e: any, ...args: any[]) => void) => { + if (type === 'input') { const inputRef = React.createRef<HTMLInputElement>(); - let text = ""; - this._currentMenu.push(<div key={Utils.GenerateGuid()} className="timeline-menu-item"><FontAwesomeIcon icon={faClipboard as IconLookup} size="lg" /><input className="timeline-menu-input" ref={inputRef} placeholder={title} onChange={(e) => { - e.stopPropagation(); - text = e.target.value; - }} onKeyDown={(e) => { - if (e.keyCode === 13) { - event(text); - this.closeMenu(); - e.stopPropagation(); - } - }} /></div>); - } else if (type === "button") { - this._currentMenu.push(<div key={Utils.GenerateGuid()} className="timeline-menu-item"><FontAwesomeIcon icon={faChartLine as IconLookup} size="lg" /><p className="timeline-menu-desc" onClick={(e) => { - e.preventDefault(); - e.stopPropagation(); - event(e); - this.closeMenu(); - }}>{title}</p></div>); + let text = ''; + this._currentMenu.push( + <div key={Utils.GenerateGuid()} className="timeline-menu-item"> + <FontAwesomeIcon icon={faClipboard as IconLookup} size="lg" /> + <input + className="timeline-menu-input" + ref={inputRef} + placeholder={title} + onChange={e => { + e.stopPropagation(); + text = e.target.value; + }} + onKeyDown={e => { + if (e.keyCode === 13) { + event(Number(text)); + this.closeMenu(); + e.stopPropagation(); + } + }} + /> + </div> + ); + } else if (type === 'button') { + this._currentMenu.push( + <div key={Utils.GenerateGuid()} className="timeline-menu-item"> + <FontAwesomeIcon icon={faChartLine as IconLookup} size="lg" /> + <p + className="timeline-menu-desc" + onClick={e => { + e.preventDefault(); + e.stopPropagation(); + event(e); + this.closeMenu(); + }}> + {title} + </p> + </div> + ); } - } + }; @action addMenu = (title: string) => { - this._currentMenu.unshift(<div key={Utils.GenerateGuid()} className="timeline-menu-header"><p className="timeline-menu-header-desc">{title}</p></div>); - } + this._currentMenu.unshift( + <div key={Utils.GenerateGuid()} className="timeline-menu-header"> + <p className="timeline-menu-header-desc">{title}</p> + </div> + ); + }; render() { return ( - <div key={Utils.GenerateGuid()} className="timeline-menu-container" style={{ opacity: this._opacity, left: this._x, top: this._y }} > + <div key={Utils.GenerateGuid()} className="timeline-menu-container" style={{ opacity: this._opacity, left: this._x, top: this._y }}> {this._currentMenu} </div> ); } - -}
\ No newline at end of file +} diff --git a/src/client/views/animationtimeline/TimelineOverview.tsx b/src/client/views/animationtimeline/TimelineOverview.tsx index 81a5587e4..82ac69a3b 100644 --- a/src/client/views/animationtimeline/TimelineOverview.tsx +++ b/src/client/views/animationtimeline/TimelineOverview.tsx @@ -1,11 +1,9 @@ -import * as React from "react"; -import { observable, action, computed, runInAction, reaction, IReactionDisposer } from "mobx"; -import { observer } from "mobx-react"; -import "./TimelineOverview.scss"; -import * as $ from 'jquery'; -import { Timeline } from "./Timeline"; -import { Keyframe, KeyframeFunc } from "./Keyframe"; - +import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { RegionHelpers } from './Region'; +import { Timeline } from './Timeline'; +import './TimelineOverview.scss'; interface TimelineOverviewProps { totalLength: number; @@ -21,9 +19,8 @@ interface TimelineOverviewProps { tickIncrement: number; } - @observer -export class TimelineOverview extends React.Component<TimelineOverviewProps>{ +export class TimelineOverview extends React.Component<TimelineOverviewProps> { @observable private _visibleRef = React.createRef<HTMLDivElement>(); @observable private _scrubberRef = React.createRef<HTMLDivElement>(); @observable private authoringContainer = React.createRef<HTMLDivElement>(); @@ -49,13 +46,13 @@ export class TimelineOverview extends React.Component<TimelineOverviewProps>{ this.setOverviewWidth(); }); } - }, + } ); - } + }; componentWillUnmount = () => { this._authoringReaction && this._authoringReaction(); - } + }; @action setOverviewWidth() { @@ -66,8 +63,7 @@ export class TimelineOverview extends React.Component<TimelineOverviewProps>{ if (this.props.isAuthoring) { this.activeOverviewWidth = this.overviewBarWidth; - } - else { + } else { this.activeOverviewWidth = this.playbarWidth; } } @@ -76,37 +72,37 @@ export class TimelineOverview extends React.Component<TimelineOverviewProps>{ onPointerDown = (e: React.PointerEvent) => { e.stopPropagation(); e.preventDefault(); - document.removeEventListener("pointermove", this.onPanX); - document.removeEventListener("pointerup", this.onPointerUp); - document.addEventListener("pointermove", this.onPanX); - document.addEventListener("pointerup", this.onPointerUp); - } + document.removeEventListener('pointermove', this.onPanX); + document.removeEventListener('pointerup', this.onPointerUp); + document.addEventListener('pointermove', this.onPanX); + document.addEventListener('pointerup', this.onPointerUp); + }; @action onPanX = (e: PointerEvent) => { e.stopPropagation(); e.preventDefault(); - const movX = (this.props.visibleStart / this.props.totalLength) * (this.DEFAULT_WIDTH) + e.movementX; - this.props.movePanX((movX / (this.DEFAULT_WIDTH)) * this.props.totalLength); - } + const movX = (this.props.visibleStart / this.props.totalLength) * this.DEFAULT_WIDTH + e.movementX; + this.props.movePanX((movX / this.DEFAULT_WIDTH) * this.props.totalLength); + }; @action onPointerUp = (e: PointerEvent) => { e.stopPropagation(); e.preventDefault(); - document.removeEventListener("pointermove", this.onPanX); - document.removeEventListener("pointerup", this.onPointerUp); - } + document.removeEventListener('pointermove', this.onPanX); + document.removeEventListener('pointerup', this.onPointerUp); + }; @action onScrubberDown = (e: React.PointerEvent) => { e.preventDefault(); e.stopPropagation(); - document.removeEventListener("pointermove", this.onScrubberMove); - document.removeEventListener("pointerup", this.onScrubberUp); - document.addEventListener("pointermove", this.onScrubberMove); - document.addEventListener("pointerup", this.onScrubberUp); - } + document.removeEventListener('pointermove', this.onScrubberMove); + document.removeEventListener('pointerup', this.onScrubberUp); + document.addEventListener('pointermove', this.onScrubberMove); + document.addEventListener('pointerup', this.onScrubberUp); + }; @action onScrubberMove = (e: PointerEvent) => { @@ -115,22 +111,22 @@ export class TimelineOverview extends React.Component<TimelineOverviewProps>{ const scrubberRef = this._scrubberRef.current!; const left = scrubberRef.getBoundingClientRect().left; const offsetX = Math.round(e.clientX - left); - this.props.changeCurrentBarX((((offsetX) / this.activeOverviewWidth) * this.props.totalLength) + this.props.currentBarX); - } + this.props.changeCurrentBarX((offsetX / this.activeOverviewWidth) * this.props.totalLength + this.props.currentBarX); + }; @action onScrubberUp = (e: PointerEvent) => { e.preventDefault(); e.stopPropagation(); - document.removeEventListener("pointermove", this.onScrubberMove); - document.removeEventListener("pointerup", this.onScrubberUp); - } + document.removeEventListener('pointermove', this.onScrubberMove); + document.removeEventListener('pointerup', this.onScrubberUp); + }; @action getTimes() { - const vis = KeyframeFunc.convertPixelTime(this.props.visibleLength, "mili", "time", this.props.tickSpacing, this.props.tickIncrement); - const x = KeyframeFunc.convertPixelTime(this.props.currentBarX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement); - const start = KeyframeFunc.convertPixelTime(this.props.visibleStart, "mili", "time", this.props.tickSpacing, this.props.tickIncrement); + const vis = RegionHelpers.convertPixelTime(this.props.visibleLength, 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement); + const x = RegionHelpers.convertPixelTime(this.props.currentBarX, 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement); + const start = RegionHelpers.convertPixelTime(this.props.visibleStart, 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement); this.visibleTime = vis; this.currentX = x; this.visibleStart = start; @@ -144,7 +140,7 @@ export class TimelineOverview extends React.Component<TimelineOverviewProps>{ const visibleBarWidth = percentVisible * this.activeOverviewWidth; const percentScrubberStart = this.currentX / this.props.time; - let scrubberStart = this.props.currentBarX / this.props.totalLength * this.activeOverviewWidth; + let scrubberStart = (this.props.currentBarX / this.props.totalLength) * this.activeOverviewWidth; if (scrubberStart > this.activeOverviewWidth) scrubberStart = this.activeOverviewWidth; const percentBarStart = this.visibleStart / this.props.time; @@ -153,29 +149,25 @@ export class TimelineOverview extends React.Component<TimelineOverviewProps>{ let playWidth = (this.props.currentBarX / this.props.totalLength) * this.activeOverviewWidth; if (playWidth > this.activeOverviewWidth) playWidth = this.activeOverviewWidth; - const timeline = this.props.isAuthoring ? [ - - <div key="timeline-overview-container" className="timeline-overview-container overviewBar" id="timelineOverview" ref={this.authoringContainer}> - <div ref={this._visibleRef} key="1" className="timeline-overview-visible" style={{ left: `${barStart}px`, width: `${visibleBarWidth}px` }} onPointerDown={this.onPointerDown}></div>, - <div ref={this._scrubberRef} key="2" className="timeline-overview-scrubber-container" style={{ left: `${scrubberStart}px` }} onPointerDown={this.onScrubberDown}> - <div key="timeline-overview-scrubber-head" className="timeline-overview-scrubber-head"></div> - </div> - </div> - ] : [ - <div key="1" className="timeline-play-bar overviewBar" id="timelinePlay" ref={this.playbackContainer}> - <div ref={this._scrubberRef} className="timeline-play-head" style={{ left: `${scrubberStart}px` }} onPointerDown={this.onScrubberDown}></div> - </div>, - <div key="2" className="timeline-play-tail" style={{ width: `${playWidth}px` }}></div> - ]; + const timeline = this.props.isAuthoring + ? [ + <div key="timeline-overview-container" className="timeline-overview-container overviewBar" id="timelineOverview" ref={this.authoringContainer}> + <div ref={this._visibleRef} key="1" className="timeline-overview-visible" style={{ left: `${barStart}px`, width: `${visibleBarWidth}px` }} onPointerDown={this.onPointerDown}></div>, + <div ref={this._scrubberRef} key="2" className="timeline-overview-scrubber-container" style={{ left: `${scrubberStart}px` }} onPointerDown={this.onScrubberDown}> + <div key="timeline-overview-scrubber-head" className="timeline-overview-scrubber-head"></div> + </div> + </div>, + ] + : [ + <div key="1" className="timeline-play-bar overviewBar" id="timelinePlay" ref={this.playbackContainer}> + <div ref={this._scrubberRef} className="timeline-play-head" style={{ left: `${scrubberStart}px` }} onPointerDown={this.onScrubberDown}></div> + </div>, + <div key="2" className="timeline-play-tail" style={{ width: `${playWidth}px` }}></div>, + ]; return ( <div className="timeline-flex"> - <div className="timelineOverview-bounding"> - {timeline} - </div> + <div className="timelineOverview-bounding">{timeline}</div> </div> ); } - } - - diff --git a/src/client/views/animationtimeline/Track.tsx b/src/client/views/animationtimeline/Track.tsx index 1010332f5..f36b5ade8 100644 --- a/src/client/views/animationtimeline/Track.tsx +++ b/src/client/views/animationtimeline/Track.tsx @@ -1,17 +1,19 @@ import { action, computed, intercept, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, DocListCast, Opt, DocListCastAsync } from '../../../fields/Doc'; +import { Doc, DocListCast, DocListCastAsync, Opt } from '../../../fields/Doc'; import { Copy } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { ObjectField } from '../../../fields/ObjectField'; import { listSpec } from '../../../fields/Schema'; import { Cast, NumCast } from '../../../fields/Types'; import { Transform } from '../../util/Transform'; -import { Keyframe, KeyframeFunc, RegionData } from './Keyframe'; +import { Region, RegionData, RegionHelpers } from './Region'; +import { Timeline } from './Timeline'; import './Track.scss'; interface IProps { + timeline: Timeline; animatedDoc: Doc; currentBarX: number; transform: Transform; @@ -32,28 +34,29 @@ export class Track extends React.Component<IProps> { @observable private _newKeyframe: boolean = false; private readonly MAX_TITLE_HEIGHT = 75; @observable private _trackHeight = 0; - private primitiveWhitelist = ['x', 'y', '_width', '_height', 'opacity', '_layout_scrollTop']; + private primitiveWhitelist = ['x', 'y', '_freeform_panX', '_freeform_panY', '_width', '_height', '_rotation', 'opacity', '_layout_scrollTop']; private objectWhitelist = ['data']; @computed private get regions() { return DocListCast(this.props.animatedDoc.regions); } @computed private get time() { - return NumCast(KeyframeFunc.convertPixelTime(this.props.currentBarX, 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement)); + return NumCast(RegionHelpers.convertPixelTime(this.props.currentBarX, 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement)); } - async componentDidMount() { - const regions = await DocListCastAsync(this.props.animatedDoc.regions); - if (!regions) this.props.animatedDoc.regions = new List<Doc>(); //if there is no region, then create new doc to store stuff - //these two lines are exactly same from timeline.tsx - const relativeHeight = window.innerHeight / 20; - runInAction(() => (this._trackHeight = relativeHeight < this.MAX_TITLE_HEIGHT ? relativeHeight : this.MAX_TITLE_HEIGHT)); //for responsiveness - this._timelineVisibleReaction = this.timelineVisibleReaction(); - this._currentBarXReaction = this.currentBarXReaction(); - if (DocListCast(this.props.animatedDoc.regions).length === 0) this.createRegion(this.time); - this.props.animatedDoc.hidden = false; - this.props.animatedDoc.opacity = 1; - // this.autoCreateKeyframe(); + componentDidMount() { + DocListCastAsync(this.props.animatedDoc.regions).then(regions => { + if (!regions) this.props.animatedDoc.regions = new List<Doc>(); //if there is no region, then create new doc to store stuff + //these two lines are exactly same from timeline.tsx + const relativeHeight = window.innerHeight / 20; + runInAction(() => (this._trackHeight = relativeHeight < this.MAX_TITLE_HEIGHT ? relativeHeight : this.MAX_TITLE_HEIGHT)); //for responsiveness + this._timelineVisibleReaction = this.timelineVisibleReaction(); + this._currentBarXReaction = this.currentBarXReaction(); + if (DocListCast(this.props.animatedDoc.regions).length === 0) this.createRegion(this.time); + this.props.animatedDoc.hidden = false; + this.props.animatedDoc.opacity = 1; + // this.autoCreateKeyframe(); + }); } /** @@ -84,37 +87,37 @@ export class Track extends React.Component<IProps> { * */ @action - saveKeyframe = async () => { - const keyframes = Cast(this.saveStateRegion?.keyframes, listSpec(Doc)) as List<Doc>; - const kfIndex = keyframes.indexOf(this.saveStateKf!); + saveKeyframe = () => { + if (this.props.timeline.IsPlaying || !this.saveStateRegion || !this.saveStateKf) { + this.saveStateKf = undefined; + this.saveStateRegion = undefined; + return; + } + const keyframes = Cast(this.saveStateRegion.keyframes, listSpec(Doc)) as List<Doc>; + const kfIndex = keyframes.indexOf(this.saveStateKf); const kf = keyframes[kfIndex] as Doc; //index in the keyframe if (this._newKeyframe) { - DocListCast(this.saveStateRegion?.keyframes).forEach((kf, index) => { + DocListCast(this.saveStateRegion.keyframes).forEach((kf, index) => { this.copyDocDataToKeyFrame(kf); kf.opacity = index === 0 || index === 3 ? 0.1 : 1; }); this._newKeyframe = false; } if (!kf) return; - if (kf.type === KeyframeFunc.KeyframeType.default) { - // only save for non-fades - this.copyDocDataToKeyFrame(kf); - const leftkf = KeyframeFunc.calcMinLeft(this.saveStateRegion!, this.time, kf); // lef keyframe, if it exists - const rightkf = KeyframeFunc.calcMinRight(this.saveStateRegion!, this.time, kf); //right keyframe, if it exists - if (leftkf?.type === KeyframeFunc.KeyframeType.fade) { + // only save for non-fades + if (this.copyDocDataToKeyFrame(kf)) { + const leftkf = RegionHelpers.calcMinLeft(this.saveStateRegion, this.time, kf); // lef keyframe, if it exists + const rightkf = RegionHelpers.calcMinRight(this.saveStateRegion, this.time, kf); //right keyframe, if it exists + if (leftkf?.type === RegionHelpers.KeyframeType.end) { //replicating this keyframe to fades - const edge = KeyframeFunc.calcMinLeft(this.saveStateRegion!, this.time, leftkf); + const edge = RegionHelpers.calcMinLeft(this.saveStateRegion, this.time, leftkf); edge && this.copyDocDataToKeyFrame(edge); leftkf && this.copyDocDataToKeyFrame(leftkf); - edge && (edge.opacity = 0.1); - leftkf && (leftkf.opacity = 1); } - if (rightkf?.type === KeyframeFunc.KeyframeType.fade) { - const edge = KeyframeFunc.calcMinRight(this.saveStateRegion!, this.time, rightkf); + if (rightkf?.type === RegionHelpers.KeyframeType.end) { + const edge = RegionHelpers.calcMinRight(this.saveStateRegion, this.time, rightkf); edge && this.copyDocDataToKeyFrame(edge); rightkf && this.copyDocDataToKeyFrame(rightkf); - edge && (edge.opacity = 0.1); - rightkf && (rightkf.opacity = 1); } } keyframes[kfIndex] = kf; @@ -143,7 +146,7 @@ export class Track extends React.Component<IProps> { const r = region as RegionData; //for some region is returning undefined... which is not the case if (DocListCast(r.keyframes).find(kf => kf.time === this.time) === undefined) { //basically when there is no additional keyframe at that timespot - this.makeKeyData(r, this.time, KeyframeFunc.KeyframeType.default); + this.makeKeyData(r, this.time, RegionHelpers.KeyframeType.default); } } }, @@ -178,7 +181,7 @@ export class Track extends React.Component<IProps> { this.timeChange(); } else { this.props.animatedDoc.hidden = true; - this.props.animatedDoc.opacity = 0; + this.props.animatedDoc !== this.props.collection && (this.props.animatedDoc.opacity = 0); //if (this._autoKfReaction) this._autoKfReaction(); } } @@ -221,23 +224,23 @@ export class Track extends React.Component<IProps> { * when scrubber position changes. Need to edit the logic */ @action - timeChange = async () => { - if (this.saveStateKf !== undefined) { - await this.saveKeyframe(); - } else if (this._newKeyframe) { - await this.saveKeyframe(); + timeChange = () => { + if (this.saveStateKf !== undefined || this._newKeyframe) { + this.saveKeyframe(); } - const regiondata = await this.findRegion(Math.round(this.time)); //finds a region that the scrubber is on + const regiondata = this.findRegion(Math.round(this.time)); //finds a region that the scrubber is on if (regiondata) { - const leftkf: Doc | undefined = await KeyframeFunc.calcMinLeft(regiondata, this.time); // lef keyframe, if it exists - const rightkf: Doc | undefined = await KeyframeFunc.calcMinRight(regiondata, this.time); //right keyframe, if it exists - const currentkf: Doc | undefined = await this.calcCurrent(regiondata); //if the scrubber is on top of the keyframe + const leftkf: Doc | undefined = RegionHelpers.calcMinLeft(regiondata, this.time); // lef keyframe, if it exists + const rightkf: Doc | undefined = RegionHelpers.calcMinRight(regiondata, this.time); //right keyframe, if it exists + const currentkf: Doc | undefined = this.calcCurrent(regiondata); //if the scrubber is on top of the keyframe if (currentkf) { - await this.applyKeys(currentkf); - this.saveStateKf = currentkf; - this.saveStateRegion = regiondata; + this.applyKeys(currentkf); + runInAction(() => { + this.saveStateKf = currentkf; + this.saveStateRegion = regiondata; + }); } else if (leftkf && rightkf) { - await this.interpolate(leftkf, rightkf); + this.interpolate(leftkf, rightkf); } } }; @@ -247,7 +250,7 @@ export class Track extends React.Component<IProps> { * need to change the logic here */ @action - private applyKeys = async (kf: Doc) => { + private applyKeys = (kf: Doc) => { this.primitiveWhitelist.forEach(key => { if (!kf[key]) { this.props.animatedDoc[key] = undefined; @@ -275,9 +278,12 @@ export class Track extends React.Component<IProps> { * basic linear interpolation function */ @action - interpolate = async (left: Doc, right: Doc) => { + interpolate = (left: Doc, right: Doc) => { this.primitiveWhitelist.forEach(key => { - if (left[key] && right[key] && typeof left[key] === 'number' && typeof right[key] === 'number') { + if (key === 'opacity' && this.props.animatedDoc === this.props.collection) { + return; + } + if (typeof left[key] === 'number' && typeof right[key] === 'number') { //if it is number, interpolate const dif = NumCast(right[key]) - NumCast(left[key]); const deltaLeft = this.time - NumCast(left.time); @@ -306,7 +312,7 @@ export class Track extends React.Component<IProps> { onInnerDoubleClick = (e: React.MouseEvent) => { const inner = this._inner.current!; const offsetX = Math.round((e.clientX - inner.getBoundingClientRect().left) * this.props.transform.Scale); - this.createRegion(KeyframeFunc.convertPixelTime(offsetX, 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement)); + this.createRegion(RegionHelpers.convertPixelTime(offsetX, 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement)); }; /** @@ -316,10 +322,10 @@ export class Track extends React.Component<IProps> { createRegion = (time: number) => { if (this.findRegion(time) === undefined) { //check if there is a region where double clicking (prevents phantom regions) - const regiondata = KeyframeFunc.defaultKeyframe(); //create keyframe data + const regiondata = RegionHelpers.defaultKeyframe(); //create keyframe data regiondata.position = time; //set position - const rightRegion = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.right, regiondata, this.regions); + const rightRegion = RegionHelpers.findAdjacentRegion(RegionHelpers.Direction.right, regiondata, this.regions); if (rightRegion && rightRegion.position - regiondata.position <= 4000) { //edge case when there is less than default 4000 duration space between this and right region @@ -335,7 +341,7 @@ export class Track extends React.Component<IProps> { }; @action - makeKeyData = (regiondata: RegionData, time: number, type: KeyframeFunc.KeyframeType = KeyframeFunc.KeyframeType.default) => { + makeKeyData = (regiondata: RegionData, time: number, type: RegionHelpers.KeyframeType = RegionHelpers.KeyframeType.default) => { //Kfpos is mouse offsetX, representing time const trackKeyFrames = DocListCast(regiondata.keyframes); const existingkf = trackKeyFrames.find(TK => TK.time === time); @@ -359,16 +365,21 @@ export class Track extends React.Component<IProps> { @action copyDocDataToKeyFrame = (doc: Doc) => { + var somethingChanged = false; this.primitiveWhitelist.map(key => { const originalVal = this.props.animatedDoc[key]; - doc[key] = originalVal instanceof ObjectField ? originalVal[Copy]() : originalVal; + somethingChanged = somethingChanged || originalVal !== doc[key]; + if (doc.type === RegionHelpers.KeyframeType.end && key === 'opacity') doc.opacity = 0; + else doc[key] = originalVal instanceof ObjectField ? originalVal[Copy]() : originalVal; }); + return somethingChanged; }; /** * UI sstuff here. Not really much to change */ render() { + const saveStateKf = this.saveStateKf; return ( <div className="track-container"> <div className="track"> @@ -380,7 +391,7 @@ export class Track extends React.Component<IProps> { onPointerOver={() => Doc.BrushDoc(this.props.animatedDoc)} onPointerOut={() => Doc.UnBrushDoc(this.props.animatedDoc)}> {this.regions?.map((region, i) => { - return <Keyframe key={`${i}`} {...this.props} RegionData={region} makeKeyData={this.makeKeyData} />; + return <Region key={`${i}`} {...this.props} saveStateKf={saveStateKf} RegionData={region} makeKeyData={this.makeKeyData} />; })} </div> </div> diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx index ec9d86c1a..52cf40635 100644 --- a/src/client/views/collections/CollectionMenu.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -3,11 +3,11 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon, FontAwesomeIconProps } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; import { Toggle, ToggleType, Type } from 'browndash-components'; -import { Lambda, action, computed, observable, reaction, runInAction } from 'mobx'; +import { action, computed, Lambda, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { ColorState } from 'react-color'; -import { Utils, emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents } from '../../../Utils'; import { Doc, DocListCast, Opt } from '../../../fields/Doc'; +import { Document } from '../../../fields/documentSchemas'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; @@ -15,10 +15,10 @@ import { ObjectField } from '../../../fields/ObjectField'; import { RichTextField } from '../../../fields/RichTextField'; import { listSpec } from '../../../fields/Schema'; import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../fields/Types'; -import { Document } from '../../../fields/documentSchemas'; import { GestureUtils } from '../../../pen-gestures/GestureUtils'; -import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; +import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents, Utils } from '../../../Utils'; import { Docs } from '../../documents/Documents'; +import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; import { DragManager } from '../../util/DragManager'; import { ScriptingGlobals } from '../../util/ScriptingGlobals'; import { SelectionManager } from '../../util/SelectionManager'; @@ -31,17 +31,17 @@ import { GestureOverlay } from '../GestureOverlay'; import { ActiveFillColor, ActiveInkColor, SetActiveArrowEnd, SetActiveArrowStart, SetActiveBezierApprox, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth } from '../InkingStroke'; import { LightboxView } from '../LightboxView'; import { MainView } from '../MainView'; -import { DefaultStyleProvider } from '../StyleProvider'; +import { media_state } from '../nodes/AudioBox'; import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView'; import { DocumentView, DocumentViewInternal, OpenWhereMod } from '../nodes/DocumentView'; import { RichTextMenu } from '../nodes/formattedText/RichTextMenu'; +import { DefaultStyleProvider } from '../StyleProvider'; import { CollectionDockingView } from './CollectionDockingView'; +import { CollectionFreeFormView } from './collectionFreeForm'; +import { CollectionLinearView } from './collectionLinear'; import './CollectionMenu.scss'; import { COLLECTION_BORDER_WIDTH } from './CollectionView'; import { TabDocView } from './TabDocView'; -import { CollectionFreeFormView } from './collectionFreeForm'; -import { CollectionLinearView } from './collectionLinear'; -import { media_state } from '../nodes/AudioBox'; interface CollectionMenuProps { panelHeight: () => number; diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx index c7ad80f11..afeef5a8f 100644 --- a/src/client/views/collections/CollectionNoteTakingView.tsx +++ b/src/client/views/collections/CollectionNoteTakingView.tsx @@ -265,7 +265,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { addDocument={this.props.addDocument} moveDocument={this.props.moveDocument} removeDocument={this.props.removeDocument} - contentPointerEvents={StrCast(this.layoutDoc.contentPointerEvents)} + contentPointerEvents={StrCast(this.layoutDoc.contentPointerEvents) as any} whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged} addDocTab={this.props.addDocTab} bringToFront={returnFalse} diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx index c4650647c..7c61bc4da 100644 --- a/src/client/views/collections/CollectionStackedTimeline.tsx +++ b/src/client/views/collections/CollectionStackedTimeline.tsx @@ -203,7 +203,6 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack this._trimStart = this.clipStart; this._trimStart = this.clipEnd; this._trimming = TrimScope.None; - e.stopPropagation(); break; case 'ArrowLeft': this.props.setTime(Math.min(Math.max(this.clipStart, this.currentTime - jump), this.clipEnd)); diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index c43a9d2b8..0b29e7286 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -357,7 +357,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection addDocument={this.props.addDocument} moveDocument={this.props.moveDocument} removeDocument={this.props.removeDocument} - contentPointerEvents={StrCast(this.layoutDoc.contentPointerEvents)} + contentPointerEvents={StrCast(this.layoutDoc.contentPointerEvents) as any} whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged} addDocTab={this.props.addDocTab} bringToFront={returnFalse} diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 011bc1de5..8a1ba0df1 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -8,14 +8,22 @@ import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; import { Cast, ScriptCast, StrCast } from '../../../fields/Types'; import { WebField } from '../../../fields/URLField'; +import { GetEffectiveAcl, TraceMobx } from '../../../fields/util'; import { GestureUtils } from '../../../pen-gestures/GestureUtils'; import { returnFalse, Utils } from '../../../Utils'; import { DocServer } from '../../DocServer'; +import { Docs, DocumentOptions, DocUtils } from '../../documents/Documents'; +import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; import { Networking } from '../../Network'; +import { DragManager, dropActionType } from '../../util/DragManager'; import { ImageUtils } from '../../util/Import & Export/ImageUtils'; import { InteractionUtils } from '../../util/InteractionUtils'; +import { SelectionManager } from '../../util/SelectionManager'; +import { SnappingManager } from '../../util/SnappingManager'; import { undoBatch, UndoManager } from '../../util/UndoManager'; import { DocComponent } from '../DocComponent'; +import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; +import { CollectionView, CollectionViewProps } from './CollectionView'; import React = require('react'); export interface SubCollectionViewProps extends CollectionViewProps { @@ -118,7 +126,7 @@ export function CollectionSubView<X>(moreProps?: X) { childDocs.forEach(d => { // dragging facets const dragged = this.props.childFilters?.().some(f => f.includes(Utils.noDragsDocFilter)); - if (dragged && DragManager.docsBeingDragged.includes(d)) return false; + if (dragged && SnappingManager.GetCanEmbed() && DragManager.docsBeingDragged.includes(d)) return false; let notFiltered = d.z || Doc.IsSystem(d) || DocUtils.FilterDocs([d], this.unrecursiveDocFilters(), childFiltersByRanges, this.props.Document).length > 0; if (notFiltered) { notFiltered = (!searchDocs.length || searchDocs.includes(d)) && DocUtils.FilterDocs([d], childDocFilters, childFiltersByRanges, this.props.Document).length > 0; @@ -217,7 +225,10 @@ export function CollectionSubView<X>(moreProps?: X) { (de.embedKey || dropAction || Doc.AreProtosEqual(Cast(movedDocs[0].annotationOn, Doc, null), this.rootDoc)) && (dropAction !== 'inSame' || docDragData.draggedDocuments.every(d => d.embedContainer === this.rootDoc)); const moved = docDragData.moveDocument(movedDocs, this.rootDoc, canAdd ? this.addDocument : returnFalse); added = canAdd || moved ? moved : undefined; - } else { + } else if (addedDocs.length) { + added = this.addDocument(addedDocs); + } + if (!added && ScriptCast(this.rootDoc.dropConverter)) { ScriptCast(this.rootDoc.dropConverter)?.script.run({ dragData: docDragData }); added = addedDocs.length ? this.addDocument(addedDocs) : true; } @@ -483,11 +494,3 @@ export function CollectionSubView<X>(moreProps?: X) { return CollectionSubView; } - -import { GetEffectiveAcl, TraceMobx } from '../../../fields/util'; -import { Docs, DocumentOptions, DocUtils } from '../../documents/Documents'; -import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; -import { DragManager, dropActionType } from '../../util/DragManager'; -import { SelectionManager } from '../../util/SelectionManager'; -import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; -import { CollectionView, CollectionViewProps } from './CollectionView'; diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 9e5ac77d9..e408c193a 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -98,8 +98,6 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree Object.values(this._disposers).forEach(disposer => disposer?.()); } - shrinkWrap = () => {}; // placeholder to allow setContentView to work - componentDidMount() { //this.props.setContentView?.(this); this._disposers.autoheight = reaction( diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index ce19b3f9b..c2062e8ab 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -1,4 +1,4 @@ -import { computed, observable, runInAction } from 'mobx'; +import { IReactionDisposer, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast, Opt } from '../../../fields/Doc'; @@ -41,7 +41,6 @@ interface CollectionViewProps_ extends FieldViewProps { isAnnotationOverlayScrollable?: boolean; // whether the annotation overlay can be vertically scrolled (just for tree views, currently) layoutEngine?: () => string; setPreviewCursor?: (func: (x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void) => void; - setBrushViewer?: (func?: (view: { width: number; height: number; panX: number; panY: number }, transTime: number) => void) => void; ignoreUnrendered?: boolean; // property overrides for child documents @@ -79,6 +78,8 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab public static SetSafeMode(safeMode: boolean) { this._safeMode = safeMode; } + private reactionDisposer: IReactionDisposer | undefined; + @observable _isContentActive: boolean | undefined; protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; @@ -87,6 +88,21 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab runInAction(() => (this._annotationKeySuffix = returnEmptyString)); } + componentDidMount() { + // we use a reaction/observable instead of a computed value to reduce invalidations. + // There are many variables that aggregate into this boolean output - a change in any of them + // will cause downstream invalidations even if the computed value doesn't change. By making + // this a reaction, downstream invalidations only occur when the reaction value actually changes. + this.reactionDisposer = reaction( + () => (this.isAnyChildContentActive() ? true : this.props.isContentActive()), + active => (this._isContentActive = active), + { fireImmediately: true } + ); + } + componentWillUnmount() { + this.reactionDisposer?.(); + } + get collectionViewType(): CollectionViewType | undefined { const viewField = StrCast(this.layoutDoc._type_collection); if (CollectionView._safeMode) { @@ -221,7 +237,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab childHideResizeHandles = () => this.props.childHideResizeHandles?.() ?? BoolCast(this.Document.childHideResizeHandles); childHideDecorationTitle = () => this.props.childHideDecorationTitle?.() ?? BoolCast(this.Document.childHideDecorationTitle); childLayoutTemplate = () => this.props.childLayoutTemplate?.() || Cast(this.rootDoc.childLayoutTemplate, Doc, null); - isContentActive = (outsideReaction?: boolean) => (this.isAnyChildContentActive() ? true : this.props.isContentActive()); + isContentActive = (outsideReaction?: boolean) => this._isContentActive; render() { TraceMobx(); diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index f89aa065b..193c70add 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -201,12 +201,10 @@ export class TreeView extends React.Component<TreeViewProps> { if (!docView) { this._editTitle = false; } else if (docView.isSelected()) { - const doc = docView.Document; - SelectionManager.SelectSchemaViewDoc(doc); this._editTitle = true; this._disposers.selection = reaction( - () => SelectionManager.SelectedSchemaDoc(), - seldoc => seldoc !== doc && this.setEditTitle(undefined) + () => docView.isSelected(), + isSel => !isSel && this.setEditTitle(undefined) ); } else { docView.select(false); @@ -753,7 +751,7 @@ export class TreeView extends React.Component<TreeViewProps> { : { pointerEvents: this.props.isContentActive() ? 'all' : undefined, opacity: checked === 'unchecked' || typeof iconType !== 'string' ? undefined : 0.4, - color: StrCast(this.doc.color, checked === 'unchecked' ? 'white' : 'inherit'), + color: checked === 'unchecked' ? SettingsManager.userColor : 'inherit', } }> {this.props.treeView.outlineMode ? ( @@ -883,7 +881,7 @@ export class TreeView extends React.Component<TreeViewProps> { // just render a title for a tree view label (identified by treeViewDoc being set in 'props') maxWidth: props?.PanelWidth() || undefined, background: props?.styleProvider?.(doc, props, StyleProp.BackgroundColor), - outline: `solid ${highlightColor} ${highlightIndex}px`, + outline: SnappingManager.GetIsDragging() ? undefined: `solid ${highlightColor} ${highlightIndex}px`, paddingLeft: NumCast(treeView.rootDoc.childXPadding, NumCast(treeView.props.childXPadding, Doc.IsComicStyle(doc)?20:0)), paddingRight: NumCast(treeView.rootDoc.childXPadding, NumCast(treeView.props.childXPadding, Doc.IsComicStyle(doc)?20:0)), paddingTop: treeView.props.childYPadding, diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx index c1c01eacb..d93e44ab7 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx @@ -42,6 +42,7 @@ export interface PoolData { transition?: string; highlight?: boolean; replica: string; + pointerEvents?: string; // without this, toggling lockPosition of a group/collection in a freeform view won't update until something else invalidates the freeform view's documents forcing -- this is a problem with doLayoutComputation which makes a performance test to insure somethingChanged pair: { layout: Doc; data?: Doc }; } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index c90fdf013..250760bd5 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -54,10 +54,14 @@ } } + .presPathLabels { + pointer-events: none; + } svg.presPaths { position: absolute; z-index: 100000; overflow: visible; + pointer-events: none; } svg.presPaths-hidden { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index bfc61f601..da0f7c893 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -16,17 +16,18 @@ import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } fro import { ImageField } from '../../../../fields/URLField'; import { TraceMobx } from '../../../../fields/util'; import { GestureUtils } from '../../../../pen-gestures/GestureUtils'; -import { aggregateBounds, emptyFunction, intersectRect, lightOrDark, returnFalse, returnNone, returnTrue, returnZero, setupMoveUpEvents, Utils } from '../../../../Utils'; +import { aggregateBounds, DashColor, emptyFunction, intersectRect, lightOrDark, returnFalse, returnZero, setupMoveUpEvents, Utils } from '../../../../Utils'; import { CognitiveServices } from '../../../cognitive_services/CognitiveServices'; import { Docs, DocUtils } from '../../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; import { DocumentManager } from '../../../util/DocumentManager'; import { DragManager, dropActionType } from '../../../util/DragManager'; import { InteractionUtils } from '../../../util/InteractionUtils'; +import { FollowLinkScript } from '../../../util/LinkFollower'; import { ReplayMovements } from '../../../util/ReplayMovements'; import { ScriptingGlobals } from '../../../util/ScriptingGlobals'; import { SelectionManager } from '../../../util/SelectionManager'; -import { ColorScheme, freeformScrollMode } from '../../../util/SettingsManager'; +import { freeformScrollMode } from '../../../util/SettingsManager'; import { SnappingManager } from '../../../util/SnappingManager'; import { Transform } from '../../../util/Transform'; import { undoBatch, UndoManager } from '../../../util/UndoManager'; @@ -51,7 +52,6 @@ import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCurso import './CollectionFreeFormView.scss'; import { MarqueeView } from './MarqueeView'; import React = require('react'); -import { FollowLinkScript } from '../../../util/LinkFollower'; export type collectionFreeformViewProps = { NativeWidth?: () => number; @@ -64,8 +64,6 @@ export type collectionFreeformViewProps = { noOverlay?: boolean; // used to suppress docs in the overlay (z) layer (ie, for minimap since overlay doesn't scale) engineProps?: any; getScrollHeight?: () => number | undefined; - dontRenderDocuments?: boolean; // used for annotation overlays which need to distribute documents into different freeformviews with different mixBlendModes depending on whether they are transparent or not. - // However, this screws up interactions since only the top layer gets events. so we render the freeformview a 3rd time with all documents in order to get interaction events (eg., marquee) but we don't actually want to display the documents. }; @observer @@ -82,6 +80,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection private _lastY: number = 0; private _downX: number = 0; private _downY: number = 0; + private _downTime = 0; private _inkToTextStartX: number | undefined; private _inkToTextStartY: number | undefined; private _wordPalette: Map<string, string> = new Map<string, string>(); @@ -92,7 +91,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection private _layoutPoolData = observable.map<string, PoolData>(); private _layoutSizeData = observable.map<string, { width?: number; height?: number }>(); private _cachedPool: Map<string, PoolData> = new Map(); - private _lastTap = 0; private _batch: UndoManager.Batch | undefined = undefined; private _brushtimer: any; private _brushtimer1: any; @@ -118,21 +116,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @observable.shallow _layoutElements: ViewDefResult[] = []; // shallow because some layout items (eg pivot labels) are just generated 'divs' and can't be frozen as observables @observable _panZoomTransition: number = 0; // sets the pan/zoom transform ease time- used by nudge(), focus() etc to smoothly zoom/pan. set to 0 to use document's transition time or default of 0 - @observable _hLines: number[] | undefined; - @observable _vLines: number[] | undefined; @observable _firstRender = false; // this turns off rendering of the collection's content so that there's instant feedback when a tab is switched of what content will be shown. could be used for performance improvement @observable _showAnimTimeline = false; @observable _clusterSets: Doc[][] = []; @observable _deleteList: DocumentView[] = []; @observable _timelineRef = React.createRef<Timeline>(); - @observable _marqueeRef: HTMLDivElement | null = null; @observable _marqueeViewRef = React.createRef<MarqueeView>(); @observable GroupChildDrag: boolean = false; // child document view being dragged. needed to update drop areas of groups when a group item is dragged. - @observable _brushedView = { width: 0, height: 0, panX: 0, panY: 0, opacity: 0 }; // highlighted region of freeform canvas used by presentations to indicate a region - - constructor(props: any) { - super(props); - } + @observable _brushedView: { width: number; height: number; panX: number; panY: number } | undefined; // highlighted region of freeform canvas used by presentations to indicate a region @computed get views() { const viewsMask = this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z && ele.inkMask !== -1 && ele.inkMask !== undefined).map(ele => ele.ele); @@ -156,12 +147,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const cb = Cast(this.rootDoc.contentBounds, listSpec('number')); return cb ? { x: cb[0], y: cb[1], r: cb[2], b: cb[3] } - : this.props.contentBounds?.() ?? - aggregateBounds( - this._layoutElements.filter(e => e.bounds?.width && !e.bounds.z).map(e => e.bounds!), - NumCast(this.layoutDoc._xPadding, 10), - NumCast(this.layoutDoc._yPadding, 10) - ); + : aggregateBounds( + this._layoutElements.filter(e => e.bounds?.width && !e.bounds.z).map(e => e.bounds!), + NumCast(this.layoutDoc._xPadding, 10), + NumCast(this.layoutDoc._yPadding, 10) + ); } @computed get nativeWidth() { return this.props.NativeWidth?.() || (this.fitContentsToBox ? 0 : Doc.NativeWidth(this.Document, Cast(this.Document.resolvedDataDoc, Doc, null))); @@ -251,7 +241,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this.layoutDoc._freeform_scale = vals.scale; }; freeformData = (force?: boolean) => (!this._firstRender && (this.fitContentsToBox || force) ? this.fitToContentVals : undefined); - reverseNativeScaling = () => (this.fitContentsToBox ? true : false); + ignoreNativeDimScaling = () => (this.fitContentsToBox ? true : false); // freeform_panx, freeform_pany, freeform_scale all attempt to get values first from the layout controller, then from the layout/dataDoc (or template layout doc), and finally from the resolved template data document. // this search order, for example, allows icons of cropped images to find the panx/pany/zoom on the cropped image's data doc instead of the usual layout doc because the zoom/panX/panY define the cropped image panX = () => this.freeformData()?.bounds.cx ?? NumCast(this.Document[this.panXFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.freeform_panX, 1)); @@ -317,6 +307,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection focus = (anchor: Doc, options: DocFocusOptions) => { if (this._lightboxDoc) return; + if (anchor === this.rootDoc) { + if (options.willZoomCentered && options.zoomScale) { + this.fitContentOnce(); + options.didMove = true; + } + } if (anchor.type !== DocumentType.CONFIG && !DocListCast(this.Document[this.fieldKey ?? Doc.LayoutFieldKey(this.Document)]).includes(anchor)) return; const xfToCollection = options?.docTransform ?? Transform.Identity(); const savedState = { panX: NumCast(this.Document[this.panXFieldKey]), panY: NumCast(this.Document[this.panYFieldKey]), scale: options?.willZoomCentered ? this.Document[this.scaleFieldKey] : undefined }; @@ -462,9 +458,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return false; }; - onExternalDrop = (e: React.DragEvent) => { - return (pt => super.onExternalDrop(e, { x: pt[0], y: pt[1] }))(this.getTransform().transformPoint(e.pageX, e.pageY)); - }; + onExternalDrop = (e: React.DragEvent) => (([x, y]) => super.onExternalDrop(e, { x, y }))(this.getTransform().transformPoint(e.pageX, e.pageY)); pickCluster(probe: number[]) { return this.childLayoutPairs @@ -641,6 +635,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection onPointerDown = (e: React.PointerEvent): void => { this._downX = this._lastX = e.pageX; this._downY = this._lastY = e.pageY; + this._downTime = Date.now(); if (e.button === 0 && !e.altKey && !e.ctrlKey && this.props.isContentActive(true)) { if ( !this.props.Document._isGroup && // group freeforms don't pan when dragged -- instead let the event go through to allow the group itself to drag @@ -715,7 +710,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection points, ActiveIsInkMask(), { - title: 'ink stroke', + title: ge.gesture.toString(), x: B.x - (ActiveInkWidth() * this.props.ScreenToLocalTransform().Scale) / 2, y: B.y - (ActiveInkWidth() * this.props.ScreenToLocalTransform().Scale) / 2, _width: B.width + ActiveInkWidth() * this.props.ScreenToLocalTransform().Scale, @@ -733,9 +728,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection if (this._inkToTextStartX && this._inkToTextStartY) { const end = this.getTransform().transformPoint(Math.max(...ge.points.map(p => p.X)), Math.max(...ge.points.map(p => p.Y))); const setDocs = this.getActiveDocuments().filter(s => DocCast(s.proto)?.type === DocumentType.RTF && s.color); - const sets = setDocs.map(sd => { - return Cast(sd.text, RichTextField)?.Text as string; - }); + const sets = setDocs.map(sd => Cast(sd.text, RichTextField)?.Text as string); if (sets.length && sets[0]) { this._wordPalette.clear(); const colors = setDocs.map(sd => FieldValue(sd.color) as string); @@ -810,31 +803,24 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @action onClick = (e: React.MouseEvent) => { if (this._lightboxDoc) this._lightboxDoc = undefined; - if (this.onBrowseClickHandler()) { - if (this.props.DocumentView?.()) { - this.onBrowseClickHandler().script.run({ documentView: this.props.DocumentView(), clientX: e.clientX, clientY: e.clientY }); - } - e.stopPropagation(); - e.preventDefault(); - } else if (Math.abs(e.pageX - this._downX) < 3 && Math.abs(e.pageY - this._downY) < 3) { - if (e.shiftKey && (this.props.renderDepth === 0 || this.isContentActive())) { - if (Date.now() - this._lastTap < 300) { - // reset zoom of freeform view to 1-to-1 on a shift + double click - this.zoomSmoothlyAboutPt(this.getTransform().transformPoint(e.clientX, e.clientY), 1); - } + if (Utils.isClick(e.pageX, e.pageY, this._downX, this._downY, this._downTime)) { + if (this.onBrowseClickHandler()) { + this.onBrowseClickHandler().script.run({ documentView: this.props.DocumentView?.(), clientX: e.clientX, clientY: e.clientY }); + e.stopPropagation(); + e.preventDefault(); + } else if (this.isContentActive() && e.shiftKey) { + // reset zoom of freeform view to 1-to-1 on a shift + double click + this.zoomSmoothlyAboutPt(this.getTransform().transformPoint(e.clientX, e.clientY), 1); e.stopPropagation(); e.preventDefault(); } - this._lastTap = Date.now(); } }; @action scrollPan = (e: WheelEvent | { deltaX: number; deltaY: number }): void => { PresBox.Instance?.pauseAutoPres(); - const dx = e.deltaX; - const dy = e.deltaY; - this.setPan(NumCast(this.Document[this.panXFieldKey]) - dx, NumCast(this.Document[this.panYFieldKey]) - dy, 0, true); + this.setPan(NumCast(this.Document[this.panXFieldKey]) - e.deltaX, NumCast(this.Document[this.panYFieldKey]) - e.deltaY, 0, true); }; @action @@ -1183,23 +1169,24 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @action bringToFront = (doc: Doc, sendToBack?: boolean) => { - if (sendToBack) { - const docs = this.childLayoutPairs.map(pair => pair.layout).slice(); - docs.sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex)); - let zfirst = docs.length ? NumCast(docs[0].zIndex) : 0; - doc.zIndex = zfirst - 1; - } else if (doc.stroke_isInkMask) { + if (doc.stroke_isInkMask) { doc.zIndex = 5000; } else { - const docs = this.childLayoutPairs.map(pair => pair.layout).slice(); - docs.sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex)); - let zlast = docs.length ? Math.max(docs.length, NumCast(docs.lastElement().zIndex)) : 1; - if (docs.lastElement() !== doc) { - if (zlast - docs.length > 100) { - for (let i = 0; i < docs.length; i++) doc.zIndex = i + 1; - zlast = docs.length + 1; + // prettier-ignore + const docs = this.childLayoutPairs.map(pair => pair.layout) + .sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex)); + if (sendToBack) { + const zfirst = docs.length ? NumCast(docs[0].zIndex) : 0; + doc.zIndex = zfirst - 1; + } else { + let zlast = docs.length ? Math.max(docs.length, NumCast(docs.lastElement().zIndex)) : 1; + if (docs.lastElement() !== doc) { + if (zlast - docs.length > 100) { + for (let i = 0; i < docs.length; i++) doc.zIndex = i + 1; + zlast = docs.length + 1; + } + doc.zIndex = zlast + 1; } - doc.zIndex = zlast + 1; } } }; @@ -1291,14 +1278,22 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return this.addDocument?.(newDoc); } }; - @computed get _pointerEvents() { + @computed get childPointerEvents() { const engine = this.props.layoutEngine?.() || StrCast(this.props.Document._layoutEngine); - const pointerEvents = DocumentView.Interacting + const pointerevents = DocumentView.Interacting ? 'none' - : this.props.childPointerEvents?.() ?? (this.props.viewDefDivClick || (engine === computePassLayout.name && !this.props.isSelected(true)) || this.isContentActive() === false ? 'none' : this.props.pointerEvents?.()); - return pointerEvents; + : this.props.childPointerEvents?.() ?? + (this.props.viewDefDivClick || // + (engine === computePassLayout.name && !this.props.isSelected(true)) || + this.isContentActive() === false + ? 'none' + : this.props.pointerEvents?.()); + console.log(`${this.rootDoc.title} pe = ` + pointerevents); + return pointerevents; } - pointerEvents = () => this._pointerEvents; + + @observable _childPointerEvents: 'none' | 'all' | 'visiblepainted' | undefined; + childPointerEventsFunc = () => this._childPointerEvents; childContentsActive = () => (this.props.childContentsActive ?? this.isContentActive() === false ? returnFalse : emptyFunction)(); getChildDocView(entry: PoolData) { const childLayout = entry.pair.layout; @@ -1308,8 +1303,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection key={childLayout[Id] + (entry.replica || '')} DataDoc={childData} Document={childLayout} + isGroupActive={this.props.isGroupActive} renderDepth={this.props.renderDepth + 1} replica={entry.replica} + hideDecorations={BoolCast(childLayout._layout_isSvg && childLayout.type === DocumentType.LINK)} suppressSetHeight={this.layoutEngine ? true : false} renderCutoffProvider={this.renderCutoffProvider} CollectionFreeFormView={this} @@ -1327,7 +1324,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection childFilters={this.childDocFilters} childFiltersByRanges={this.childDocRangeFilters} searchFilterDocs={this.searchFilterDocs} - isDocumentActive={this.props.childDocumentsActive?.() || this.rootDoc._isGroup ? this.props.isDocumentActive : this.isContentActive} + isDocumentActive={childLayout.pointerEvents === 'none' ? returnFalse : this.props.childDocumentsActive?.() ? this.props.isDocumentActive : this.isContentActive} isContentActive={this.childContentsActive} focus={this.Document._isGroup ? this.groupFocus : this.isAnnotationOverlay ? this.props.focus : this.focus} addDocTab={this.addDocTab} @@ -1343,8 +1340,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection sizeProvider={this.childSizeProvider} bringToFront={this.bringToFront} layout_showTitle={this.props.childlayout_showTitle} - dontRegisterView={this.props.dontRenderDocuments || this.props.dontRegisterView} - pointerEvents={this.pointerEvents} + dontRegisterView={this.props.dontRegisterView} + pointerEvents={this.childPointerEventsFunc} //fitContentsToBox={this.props.fitContentsToBox || BoolCast(this.props.treeView_FreezeChildDimensions)} // bcz: check this /> ); @@ -1383,9 +1380,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }); @observable _lightboxDoc: Opt<Doc>; - getCalculatedPositions(params: { pair: { layout: Doc; data?: Doc }; index: number; collection: Doc }): PoolData { + getCalculatedPositions(pair: { layout: Doc; data?: Doc }): PoolData { const random = (min: number, max: number, x: number, y: number) => /* min should not be equal to max */ min + (((Math.abs(x * y) * 9301 + 49297) % 233280) / 233280) * (max - min); - const childDoc = params.pair.layout; + const childDoc = pair.layout; const childDocLayout = Doc.Layout(childDoc); const layoutFrameNumber = Cast(this.Document._currentFrame, 'number'); // frame number that container is at which determines layout frame values const contentFrameNumber = Cast(childDocLayout._currentFrame, 'number', layoutFrameNumber ?? null); // frame number that content is at which determines what content is displayed @@ -1411,7 +1408,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection width: _width, height: _height, transition: StrCast(childDocLayout.dataTransition), - pair: params.pair, + pair, + pointerEvents: Cast(childDoc.pointerEvents, 'string', null), replica: '', }; } @@ -1490,7 +1488,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } doFreeformLayout(poolData: Map<string, PoolData>) { - this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map((pair, i) => poolData.set(pair.layout[Id], this.getCalculatedPositions({ pair, index: i, collection: this.Document }))); + this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map((pair, i) => poolData.set(pair.layout[Id], this.getCalculatedPositions(pair))); return [] as ViewDefResult[]; } @@ -1512,37 +1510,39 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @observable _numLoaded = 1; _lastPoolSize = 0; - get doLayoutComputation() { - const { newPool, computedElementData } = this.doInternalLayoutComputation; + @action + doLayoutComputation = (newPool: Map<string, PoolData>, computedElementData: ViewDefResult[]) => { const array = Array.from(newPool.entries()); - let somethingChanged = array.length !== this._lastPoolSize; this._lastPoolSize = array.length; - runInAction(() => { - for (const entry of array) { - const lastPos = this._cachedPool.get(entry[0]); // last computed pos - const newPos = entry[1]; - if ( - !lastPos || - newPos.color !== lastPos.color || - newPos.backgroundColor !== lastPos.backgroundColor || - newPos.opacity !== lastPos.opacity || - newPos.x !== lastPos.x || - newPos.y !== lastPos.y || - newPos.z !== lastPos.z || - newPos.rotation !== lastPos.rotation || - newPos.zIndex !== lastPos.zIndex || - newPos.transition !== lastPos.transition - ) { - this._layoutPoolData.set(entry[0], newPos); - somethingChanged = true; - } - if (!lastPos || newPos.height !== lastPos.height || newPos.width !== lastPos.width) { - this._layoutSizeData.set(entry[0], { width: newPos.width, height: newPos.height }); - somethingChanged = true; - } + for (const entry of array) { + const lastPos = this._cachedPool.get(entry[0]); // last computed pos + const newPos = entry[1]; + if ( + !lastPos || + newPos.color !== lastPos.color || + newPos.backgroundColor !== lastPos.backgroundColor || + newPos.opacity !== lastPos.opacity || + newPos.x !== lastPos.x || + newPos.y !== lastPos.y || + newPos.z !== lastPos.z || + newPos.rotation !== lastPos.rotation || + newPos.zIndex !== lastPos.zIndex || + newPos.transition !== lastPos.transition || + newPos.pointerEvents !== lastPos.pointerEvents + ) { + this._layoutPoolData.set(entry[0], newPos); } - }); - if (!somethingChanged) return undefined; + if (!lastPos || newPos.height !== lastPos.height || newPos.width !== lastPos.width) { + this._layoutSizeData.set(entry[0], { width: newPos.width, height: newPos.height }); + } + } + // by returning undefined, we prevent an edit being made to layoutElements when nothing has happened + // this short circuit, prevents lots of downstream mobx invalidations which would have no effect but cause + // a distinct lag at the start of dragging. + // The reason we're here in the first place without a change is that when dragging a document, + // filters are changed on the annotation layers (eg. WebBox) which invalidate the childDoc list + // for the overlay views -- however, in many cases, this filter change doesn't actually affect anything + // (e.g, no annotations, or only opaque annotations). this._cachedPool.clear(); Array.from(newPool.entries()).forEach(k => this._cachedPool.set(k[0], k[1])); const elements = computedElementData.slice(); @@ -1560,7 +1560,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this.Document._freeform_useClusters && !this._clusterSets.length && this.childDocs.length && this.updateClusters(true); return elements; - } + }; getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { // create an anchor that saves information about the current state of the freeform view (pan, zoom, view type) @@ -1577,11 +1577,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return anchor; }; - @action componentDidMount() { this.props.setContentView?.(this); super.componentDidMount?.(); - this.props.setBrushViewer?.(this.brushView); setTimeout( action(() => { this._firstRender = false; @@ -1616,26 +1614,31 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection { fireImmediately: true } ); - this._disposers.layoutComputation = reaction( - () => this.doLayoutComputation, - elements => { - if (elements !== undefined) this._layoutElements = elements || []; + this._disposers.pointerevents = reaction( + () => { + const engine = this.props.layoutEngine?.() || StrCast(this.props.Document._layoutEngine); + return DocumentView.Interacting + ? 'none' + : this.props.childPointerEvents?.() ?? + (this.props.viewDefDivClick || // + (engine === computePassLayout.name && !this.props.isSelected(true)) || + this.isContentActive() === false + ? 'none' + : this.props.pointerEvents?.()); }, - { fireImmediately: true, name: 'doLayout' } + pointerevents => (this._childPointerEvents = pointerevents as any), + { fireImmediately: true } ); - this._disposers.active = reaction( - () => this.isContentActive(), - active => this.rootDoc[this.autoResetFieldKey] && !active && this.resetView() + this._disposers.layoutComputation = reaction( + () => this.doInternalLayoutComputation, + ({ newPool, computedElementData }) => (this._layoutElements = this.doLayoutComputation(newPool, computedElementData)), + { fireImmediately: true, name: 'layoutComputationReaction' } ); - this._disposers.fitContent = reaction( - () => this.rootDoc.fitContentOnce, - fitContentOnce => { - if (fitContentOnce) this.fitContentOnce(); - this.rootDoc.fitContentOnce = undefined; - }, - { fireImmediately: true, name: 'fitContent' } + this._disposers.active = reaction( + () => this.isContentActive(), // if autoreset is on, then whenever the view is selected, it will be restored to it default pan/zoom positions + active => !SnappingManager.GetIsDragging() && this.rootDoc[this.autoResetFieldKey] && active && this.resetView() ); }) ); @@ -1724,7 +1727,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection componentWillUnmount() { Object.values(this._disposers).forEach(disposer => disposer?.()); - this._marqueeRef?.removeEventListener('dashDragAutoScroll', this.onDragAutoScroll as any); } @action @@ -1732,26 +1734,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection // super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY)); }; - @action - onDragAutoScroll = (e: CustomEvent<React.DragEvent>) => { - if ((e as any).handlePan || this.props.isAnnotationOverlay) return; - (e as any).handlePan = true; - - if (!this.layoutDoc._freeform_noAutoPan && !this.props.renderDepth && this._marqueeRef) { - const dragX = e.detail.clientX; - const dragY = e.detail.clientY; - const bounds = this._marqueeRef?.getBoundingClientRect(); - - const deltaX = dragX - bounds.left < 25 ? -(25 + (bounds.left - dragX)) : bounds.right - dragX < 25 ? 25 - (bounds.right - dragX) : 0; - const deltaY = dragY - bounds.top < 25 ? -(25 + (bounds.top - dragY)) : bounds.bottom - dragY < 25 ? 25 - (bounds.bottom - dragY) : 0; - if (deltaX !== 0 || deltaY !== 0) { - this.Document[this.panYFieldKey] = NumCast(this.Document[this.panYFieldKey]) + deltaY / 2; - this.Document[this.panXFieldKey] = NumCast(this.Document[this.panXFieldKey]) + deltaX / 2; - } - } - e.stopPropagation(); - }; - @undoBatch promoteCollection = () => { const childDocs = this.childDocs.slice(); @@ -1826,16 +1808,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this.props.Document._isGroup && this.Document.transcription && appearanceItems.push({ description: 'Ink to text', event: () => this.transcribeStrokes(false), icon: 'font' }); - // this.props.Document._isGroup && this.childDocs.filter(s => s.type === DocumentType.INK).length > 0 && appearanceItems.push({ description: "Ink to math", event: () => this.transcribeStrokes(true), icon: "square-root-alt" }); - !Doc.noviceMode ? appearanceItems.push({ description: 'Arrange contents in grid', event: this.layoutDocsInGrid, icon: 'table' }) : null; !appearance && ContextMenu.Instance.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'eye' }); const viewctrls = ContextMenu.Instance.findByDescription('UI Controls...'); const viewCtrlItems = viewctrls && 'subitems' in viewctrls ? viewctrls.subitems : []; - !Doc.noviceMode - ? viewCtrlItems.push({ description: (SnappingManager.GetShowSnapLines() ? 'Hide' : 'Show') + ' Snap Lines', event: () => SnappingManager.SetShowSnapLines(!SnappingManager.GetShowSnapLines()), icon: 'compress-arrows-alt' }) - : null; !Doc.noviceMode ? viewCtrlItems.push({ description: (this.Document._freeform_useClusters ? 'Hide' : 'Show') + ' Clusters', event: () => this.updateClusters(!this.Document._freeform_useClusters), icon: 'braille' }) : null; !viewctrls && ContextMenu.Instance.addItem({ description: 'UI Controls...', subitems: viewCtrlItems, icon: 'eye' }); @@ -1871,23 +1848,38 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; @action - setupDragLines = (snapToDraggedDoc: boolean = false) => { + dragEnding = () => { + this.GroupChildDrag = false; + SnappingManager.clearSnapLines(); + }; + @action + dragStarting = (snapToDraggedDoc: boolean = false, showGroupDragTarget: boolean, visited = new Set<Doc>()) => { + if (visited.has(this.rootDoc)) return; + visited.add(this.rootDoc); + showGroupDragTarget && (this.GroupChildDrag = BoolCast(this.Document._isGroup)); + if (this.rootDoc._isGroup && this.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView) { + this.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView.dragStarting(snapToDraggedDoc, false, visited); + } const activeDocs = this.getActiveDocuments(); const size = this.getTransform().transformDirection(this.props.PanelWidth(), this.props.PanelHeight()); const selRect = { left: this.panX() - size[0] / 2, top: this.panY() - size[1] / 2, width: size[0], height: size[1] }; const docDims = (doc: Doc) => ({ left: NumCast(doc.x), top: NumCast(doc.y), width: NumCast(doc._width), height: NumCast(doc._height) }); const isDocInView = (doc: Doc, rect: { left: number; top: number; width: number; height: number }) => intersectRect(docDims(doc), rect); - const otherBounds = { left: this.panX(), top: this.panY(), width: Math.abs(size[0]), height: Math.abs(size[1]) }; - let snappableDocs = activeDocs.filter(doc => doc.z === undefined && isDocInView(doc, selRect)); // first see if there are any foreground docs to snap to - !snappableDocs.length && (snappableDocs = activeDocs.filter(doc => doc.z === undefined && isDocInView(doc, selRect))); // if not, see if there are background docs to snap to - !snappableDocs.length && (snappableDocs = activeDocs.filter(doc => doc.z !== undefined && isDocInView(doc, otherBounds))); // if not, then why not snap to floating docs + const snappableDocs = activeDocs.filter(doc => doc.z === undefined && isDocInView(doc, selRect)); // first see if there are any foreground docs to snap to + activeDocs.forEach( + doc => + doc._isGroup && + SnappingManager.GetIsResizing() !== doc && + !DragManager.docsBeingDragged.includes(doc) && + (DocumentManager.Instance.getDocumentView(doc)?.ComponentView as CollectionFreeFormView)?.dragStarting(snapToDraggedDoc, false, visited) + ); const horizLines: number[] = []; const vertLines: number[] = []; const invXf = this.getTransform().inverse(); snappableDocs - .filter(doc => snapToDraggedDoc || !DragManager.docsBeingDragged.includes(Cast(doc.rootDocument, Doc, null) || doc)) + .filter(doc => !doc._isGroup && (snapToDraggedDoc || (SnappingManager.GetIsResizing() !== doc && !DragManager.docsBeingDragged.includes(doc)))) .forEach(doc => { const { left, top, width, height } = docDims(doc); const topLeftInScreen = invXf.transformPoint(left, top); @@ -1896,7 +1888,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection horizLines.push(topLeftInScreen[1], topLeftInScreen[1] + docSize[1] / 2, topLeftInScreen[1] + docSize[1]); // horiz center line vertLines.push(topLeftInScreen[0], topLeftInScreen[0] + docSize[0] / 2, topLeftInScreen[0] + docSize[0]); // right line }); - DragManager.SetSnapLines(horizLines, vertLines); + SnappingManager.addSnapLines(horizLines, vertLines); }; incrementalRendering = () => this.childDocs.filter(doc => !this._renderCutoffData.get(doc[Id])).length !== 0; @@ -1926,13 +1918,43 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection ); } - showPresPaths = () => (CollectionFreeFormView.ShowPresPaths ? PresBox.Instance.getPaths(this.rootDoc) : null); - brushedView = () => this._brushedView; - gridColor = () => { - const backColor = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor + ':box'); - return lightOrDark(backColor); - }; + gridColor = () => + DashColor(lightOrDark(this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor))) + .fade(0.6) + .toString(); + @computed get backgroundGrid() { + return ( + <div> + <CollectionFreeFormBackgroundGrid // bcz : UGHH don't know why, but if we don't wrap in a div, then PDF's don't render when taking snapshot of a dashboard and the background grid is on!!? + PanelWidth={this.props.PanelWidth} + PanelHeight={this.props.PanelHeight} + panX={this.panX} + panY={this.panY} + color={this.gridColor} + nativeDimScaling={this.nativeDim} + zoomScaling={this.zoomScaling} + layoutDoc={this.layoutDoc} + isAnnotationOverlay={this.isAnnotationOverlay} + cachedCenteringShiftX={this.cachedCenteringShiftX} + cachedCenteringShiftY={this.cachedCenteringShiftY} + /> + </div> + ); + } + @computed get pannableContents() { + return ( + <CollectionFreeFormViewPannableContents + rootDoc={this.rootDoc} + brushedView={this.brushedView} + isAnnotationOverlay={this.isAnnotationOverlay} + transform={this.contentTransform} + transition={this._panZoomTransition ? `transform ${this._panZoomTransition}ms` : Cast(this.layoutDoc._viewTransition, 'string', Cast(this.props.DocumentView?.()?.rootDoc._viewTransition, 'string', null))} + viewDefDivClick={this.props.viewDefDivClick}> + {this.children} + </CollectionFreeFormViewPannableContents> + ); + } @computed get marqueeView() { TraceMobx(); return ( @@ -1950,44 +1972,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection addLiveTextDocument={this.addLiveTextBox} getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} + panXFieldKey={this.panXFieldKey} + panYFieldKey={this.panYFieldKey} isAnnotationOverlay={this.isAnnotationOverlay}> - <div - className="marqueeView-div" - ref={r => { - this._marqueeRef = r; - r?.addEventListener('dashDragAutoScroll', this.onDragAutoScroll as any); - }} - style={{ opacity: this.props.dontRenderDocuments ? 0.7 : undefined }}> - {this.layoutDoc._freeform_backgroundGrid ? ( - <div> - <CollectionFreeFormBackgroundGrid // bcz : UGHH don't know why, but if we don't wrap in a div, then PDF's don't render whenn taking snapshot of a dashboard and the background grid is on!!? - PanelWidth={this.props.PanelWidth} - PanelHeight={this.props.PanelHeight} - panX={this.panX} - panY={this.panY} - color={this.gridColor} - nativeDimScaling={this.nativeDim} - zoomScaling={this.zoomScaling} - layoutDoc={this.layoutDoc} - isAnnotationOverlay={this.isAnnotationOverlay} - cachedCenteringShiftX={this.cachedCenteringShiftX} - cachedCenteringShiftY={this.cachedCenteringShiftY} - /> - </div> - ) : null} - <CollectionFreeFormViewPannableContents - brushedView={this.brushedView} - isAnnotationOverlay={this.isAnnotationOverlay} - isAnnotationOverlayScrollable={this.props.isAnnotationOverlayScrollable} - transform={this.contentTransform} - zoomScaling={this.zoomScaling} - presPaths={this.showPresPaths} - presPinView={BoolCast(this.Document.config_pinView)} - transition={this._panZoomTransition ? `transform ${this._panZoomTransition}ms` : Cast(this.layoutDoc._viewTransition, 'string', Cast(this.props.DocumentView?.()?.rootDoc._viewTransition, 'string', null))} - viewDefDivClick={this.props.viewDefDivClick}> - {this.children} - </CollectionFreeFormViewPannableContents> - </div> + {this.layoutDoc._freeform_backgroundGrid ? this.backgroundGrid : null} + {this.pannableContents} {this._showAnimTimeline ? <Timeline ref={this._timelineRef} {...this.props} /> : null} </MarqueeView> ); @@ -2007,12 +1996,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection brushView = (viewport: { width: number; height: number; panX: number; panY: number }, transTime: number) => { this._brushtimer1 && clearTimeout(this._brushtimer1); this._brushtimer && clearTimeout(this._brushtimer); - this._brushedView = { width: 0, height: 0, panX: 0, panY: 0, opacity: 0 }; + this._brushedView = undefined; this._brushtimer1 = setTimeout( action(() => { - this._brushedView = { ...viewport, panX: viewport.panX - viewport.width / 2, panY: viewport.panY - viewport.height / 2, opacity: 1 }; + this._brushedView = { ...viewport, panX: viewport.panX - viewport.width / 2, panY: viewport.panY - viewport.height / 2 }; this._brushtimer = setTimeout( - action(() => (this._brushedView.opacity = 0)), + action(() => (this._brushedView = undefined)), 2500 ); }), @@ -2044,7 +2033,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection onClick={this.onClick} onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} - onDrop={this.onExternalDrop.bind(this)} + onDrop={this.onExternalDrop} onDragOver={e => e.preventDefault()} onContextMenu={this.onContextMenu} style={{ @@ -2083,17 +2072,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection <> {this._firstRender ? this.placeholder : this.marqueeView} {this.props.noOverlay ? null : <CollectionFreeFormOverlayView elements={this.elementFunc} />} - - {/* // uncomment to show snap lines */} - <div className="snapLines" style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', pointerEvents: 'none' }}> - <svg style={{ width: '100%', height: '100%' }}> - {(this._hLines ?? []) - .map(l => <line x1="0" y1={l} x2="1000" y2={l} stroke="black" />) // - .concat((this._vLines ?? []).map(l => <line y1="0" x1={l} y2="1000" x2={l} stroke="black" />)) ?? []} - </svg> - </div> - - {this.GroupChildDrag ? <div className="collectionFreeForm-groupDropper" /> : null} + {!this.GroupChildDrag ? null : <div className="collectionFreeForm-groupDropper" />} </> )} </div> @@ -2116,119 +2095,52 @@ class CollectionFreeFormOverlayView extends React.Component<CollectionFreeFormOv } interface CollectionFreeFormViewPannableContentsProps { - transform: () => string; - zoomScaling: () => number; + rootDoc: Doc; viewDefDivClick?: ScriptField; children?: React.ReactNode | undefined; - //children: () => JSX.Element[]; transition?: string; - presPaths: () => JSX.Element | null; - presPinView?: boolean; isAnnotationOverlay: boolean | undefined; - isAnnotationOverlayScrollable: boolean | undefined; - brushedView: () => { panX: number; panY: number; width: number; height: number; opacity: number }; + transform: () => string; + brushedView: () => { panX: number; panY: number; width: number; height: number } | undefined; } @observer class CollectionFreeFormViewPannableContents extends React.Component<CollectionFreeFormViewPannableContentsProps> { - @observable _drag: string = ''; - - //Adds event listener so knows pointer is down and moving - onPointerDown = (e: React.PointerEvent): void => { - e.stopPropagation(); - e.preventDefault(); - this._drag = (e.target as any)?.id ?? ''; - document.getElementById(this._drag) && setupMoveUpEvents(e.target, e, this.onPointerMove, emptyFunction, emptyFunction); - }; - - //Adjusts the value in NodeStore - @action - onPointerMove = (e: PointerEvent) => { - const doc = document.getElementById('resizable'); - const toNumber = (original: number, delta: number) => original + delta * this.props.zoomScaling(); - if (doc) { - switch (this._drag) { - case 'resizer-br': - doc.style.width = toNumber(doc.offsetWidth, e.movementX) + 'px'; - doc.style.height = toNumber(doc.offsetHeight, e.movementY) + 'px'; - break; - case 'resizer-bl': - doc.style.width = toNumber(doc.offsetWidth, -e.movementX) + 'px'; - doc.style.height = toNumber(doc.offsetHeight, e.movementY) + 'px'; - doc.style.left = toNumber(doc.offsetLeft, e.movementX) + 'px'; - break; - case 'resizer-tr': - doc.style.width = toNumber(doc.offsetWidth, -e.movementX) + 'px'; - doc.style.height = toNumber(doc.offsetHeight, -e.movementY) + 'px'; - doc.style.top = toNumber(doc.offsetTop, e.movementY) + 'px'; - case 'resizer-tl': - doc.style.width = toNumber(doc.offsetWidth, -e.movementX) + 'px'; - doc.style.height = toNumber(doc.offsetHeight, -e.movementY) + 'px'; - doc.style.top = toNumber(doc.offsetTop, e.movementY) + 'px'; - doc.style.left = toNumber(doc.offsetLeft, e.movementX) + 'px'; - case 'resizable': - doc.style.top = toNumber(doc.offsetTop, e.movementY) + 'px'; - doc.style.left = toNumber(doc.offsetLeft, e.movementX) + 'px'; - } - return false; - } - return true; - }; - @computed get presPaths() { - return !this.props.presPaths() ? null : ( - <> - <div key="presorder">{PresBox.Instance?.order}</div> - <svg key="svg" className="presPaths"> - <defs> - <marker id="markerSquare" markerWidth="3" markerHeight="3" refX="1.5" refY="1.5" orient="auto" overflow="visible"> - <rect x="0" y="0" width="3" height="3" stroke="#69a6db" strokeWidth="1" fill="white" fillOpacity="0.8" /> - </marker> - <marker id="markerSquareFilled" markerWidth="3" markerHeight="3" refX="1.5" refY="1.5" orient="auto" overflow="visible"> - <rect x="0" y="0" width="3" height="3" stroke="#69a6db" strokeWidth="1" fill="#69a6db" /> - </marker> - <marker id="markerArrow" markerWidth="3" markerHeight="3" refX="2" refY="4" orient="auto" overflow="visible"> - <path d="M2,2 L2,6 L6,4 L2,2 Z" stroke="#69a6db" strokeLinejoin="round" strokeWidth="1" fill="white" fillOpacity="0.8" /> - </marker> - </defs> - {this.props.presPaths()} - </svg> - </> - ); + return CollectionFreeFormView.ShowPresPaths ? PresBox.Instance.pathLines(this.props.rootDoc) : null; } + // rectangle highlight used when following trail/link to a region of a collection that isn't a document + showViewport = (viewport: { panX: number; panY: number; width: number; height: number } | undefined) => + !viewport ? null : ( + <div + className="collectionFreeFormView-brushView" + style={{ + transform: `translate(${viewport.panX}px, ${viewport.panY}px)`, + width: viewport.width, + height: viewport.height, + border: `orange solid ${viewport.width * 0.005}px`, + }} + /> + ); render() { - const brushedView = this.props.brushedView(); return ( <div className={'collectionfreeformview' + (this.props.viewDefDivClick ? '-viewDef' : '-none')} onScroll={e => { const target = e.target as any; if (getComputedStyle(target)?.overflow === 'visible') { - // if collection is visible, then scrolling will mess things up since there are no scroll bars - target.scrollTop = target.scrollLeft = 0; + target.scrollTop = target.scrollLeft = 0; // if collection is visible, scrolling messes things up since there are no scroll bars } }} style={{ transform: this.props.transform(), transition: this.props.transition, width: this.props.isAnnotationOverlay ? undefined : 0, // if not an overlay, then this will be the size of the collection, but panning and zooming will move it outside the visible border of the collection and make it selectable. This problem shows up after zooming/panning on a background collection -- you can drag the collection by clicking on apparently empty space outside the collection - //willChange: "transform" }}> {this.props.children} - { - <div - className="collectionFreeFormView-brushView" - style={{ - opacity: brushedView.opacity, - transform: `translate(${brushedView.panX}px, ${brushedView.panY}px)`, - width: brushedView.width, - height: brushedView.height, - border: `orange solid ${brushedView.width * 0.005}px`, - }} - /> - } {this.presPaths} + {this.showViewport(this.props.brushedView())} </div> ); } @@ -2250,9 +2162,9 @@ interface CollectionFreeFormViewBackgroundGridProps { @observer class CollectionFreeFormBackgroundGrid extends React.Component<CollectionFreeFormViewBackgroundGridProps> { chooseGridSpace = (gridSpace: number): number => { - if (!this.props.zoomScaling()) return 50; - const divisions = this.props.PanelWidth() / this.props.zoomScaling() / gridSpace + 3; - return divisions < 60 ? gridSpace : this.chooseGridSpace(gridSpace * 10); + if (!this.props.zoomScaling()) return gridSpace; + const divisions = this.props.PanelWidth() / this.props.zoomScaling() / gridSpace; + return divisions < 90 ? gridSpace : this.chooseGridSpace(gridSpace * 2); }; render() { const gridSpace = this.chooseGridSpace(NumCast(this.props.layoutDoc['_backgroundGrid-spacing'], 50)); @@ -2278,14 +2190,22 @@ class CollectionFreeFormBackgroundGrid extends React.Component<CollectionFreeFor ctx.clearRect(0, 0, w, h); if (ctx) { ctx.strokeStyle = strokeStyle; + ctx.fillStyle = strokeStyle; ctx.beginPath(); - for (let x = Cx - renderGridSpace; x <= w - Cx; x += renderGridSpace) { - ctx.moveTo(x, Cy - h); - ctx.lineTo(x, Cy + h); - } - for (let y = Cy - renderGridSpace; y <= h - Cy; y += renderGridSpace) { - ctx.moveTo(Cx - w, y); - ctx.lineTo(Cx + w, y); + if (this.props.zoomScaling() > 1) { + for (let x = Cx - renderGridSpace; x <= w - Cx; x += renderGridSpace) { + ctx.moveTo(x, Cy - h); + ctx.lineTo(x, Cy + h); + } + for (let y = Cy - renderGridSpace; y <= h - Cy; y += renderGridSpace) { + ctx.moveTo(Cx - w, y); + ctx.lineTo(Cx + w, y); + } + } else { + for (let x = Cx - renderGridSpace; x <= w - Cx; x += renderGridSpace) + for (let y = Cy - renderGridSpace; y <= h - Cy; y += renderGridSpace) { + ctx.fillRect(Math.round(x), Math.round(y), 1, 1); + } } ctx.stroke(); } @@ -2299,20 +2219,21 @@ class CollectionFreeFormBackgroundGrid extends React.Component<CollectionFreeFor export function CollectionBrowseClick(dv: DocumentView, clientX: number, clientY: number) { const browseTransitionTime = 500; SelectionManager.DeselectAll(); - DocumentManager.Instance.showDocument(dv.rootDoc, { zoomScale: 0.8, willZoomCentered: true }, (focused: boolean) => { - if (!focused) { - const selfFfview = !dv.rootDoc._isGroup && dv.ComponentView instanceof CollectionFreeFormView ? dv.ComponentView : undefined; - let containers = dv.props.docViewPath(); - let parFfview = dv.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; - for (var cont of containers) { - parFfview = parFfview ?? cont.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; + dv && + DocumentManager.Instance.showDocument(dv.rootDoc, { zoomScale: 0.8, willZoomCentered: true }, (focused: boolean) => { + if (!focused) { + const selfFfview = !dv.rootDoc._isGroup && dv.ComponentView instanceof CollectionFreeFormView ? dv.ComponentView : undefined; + let containers = dv.props.docViewPath(); + let parFfview = dv.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; + for (var cont of containers) { + parFfview = parFfview ?? cont.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; + } + while (parFfview?.rootDoc._isGroup) parFfview = parFfview.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; + const ffview = selfFfview && selfFfview.rootDoc[selfFfview.scaleFieldKey] !== 0.5 ? selfFfview : parFfview; // if focus doc is a freeform that is not at it's default 0.5 scale, then zoom out on it. Otherwise, zoom out on the parent ffview + ffview?.zoomSmoothlyAboutPt(ffview.getTransform().transformPoint(clientX, clientY), ffview?.isAnnotationOverlay ? 1 : 0.5, browseTransitionTime); + Doc.linkFollowHighlight(dv?.props.Document, false); } - while (parFfview?.rootDoc._isGroup) parFfview = parFfview.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; - const ffview = selfFfview && selfFfview.rootDoc[selfFfview.scaleFieldKey] !== 0.5 ? selfFfview : parFfview; // if focus doc is a freeform that is not at it's default 0.5 scale, then zoom out on it. Otherwise, zoom out on the parent ffview - ffview?.zoomSmoothlyAboutPt(ffview.getTransform().transformPoint(clientX, clientY), ffview?.isAnnotationOverlay ? 1 : 0.5, browseTransitionTime); - Doc.linkFollowHighlight(dv?.props.Document, false); - } - }); + }); } ScriptingGlobals.add(CollectionBrowseClick); ScriptingGlobals.add(function nextKeyFrame(readOnly: boolean) { diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 5c053fefc..edcc17afd 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -7,7 +7,7 @@ import { InkData, InkField, InkTool } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; import { RichTextField } from '../../../../fields/RichTextField'; import { Cast, FieldValue, NumCast, StrCast } from '../../../../fields/Types'; -import { ImageField } from '../../../../fields/URLField'; +import { ImageField, nullAudio } from '../../../../fields/URLField'; import { GetEffectiveAcl } from '../../../../fields/util'; import { intersectRect, lightOrDark, returnFalse, Utils } from '../../../../Utils'; import { CognitiveServices } from '../../../cognitive_services/CognitiveServices'; @@ -35,6 +35,8 @@ interface MarqueeViewProps { selectDocuments: (docs: Doc[]) => void; addLiveTextDocument: (doc: Doc) => void; isSelected: () => boolean; + panXFieldKey: string; + panYFieldKey: string; trySelectCluster: (addToSel: boolean) => boolean; nudge?: (x: number, y: number, nudgeTime?: number) => boolean; ungroup?: () => void; @@ -377,7 +379,6 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque newCollection._height = this.Bounds.height; newCollection._isGroup = makeGroup; newCollection._dragWhenActive = makeGroup; - newCollection.forceActive = makeGroup; newCollection.x = this.Bounds.left; newCollection.y = this.Bounds.top; newCollection.layout_fitWidth = true; @@ -652,11 +653,35 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque </div> ); } + MarqueeRef: HTMLDivElement | null = null; + @action + onDragAutoScroll = (e: CustomEvent<React.DragEvent>) => { + if ((e as any).handlePan || this.props.isAnnotationOverlay) return; + (e as any).handlePan = true; + + const bounds = this.MarqueeRef?.getBoundingClientRect(); + if (!this.props.Document._freeform_noAutoPan && !this.props.renderDepth && bounds) { + const dragX = e.detail.clientX; + const dragY = e.detail.clientY; + + const deltaX = dragX - bounds.left < 25 ? -(25 + (bounds.left - dragX)) : bounds.right - dragX < 25 ? 25 - (bounds.right - dragX) : 0; + const deltaY = dragY - bounds.top < 25 ? -(25 + (bounds.top - dragY)) : bounds.bottom - dragY < 25 ? 25 - (bounds.bottom - dragY) : 0; + if (deltaX !== 0 || deltaY !== 0) { + this.props.Document[this.props.panYFieldKey] = NumCast(this.props.Document[this.props.panYFieldKey]) + deltaY / 2; + this.props.Document[this.props.panXFieldKey] = NumCast(this.props.Document[this.props.panXFieldKey]) + deltaX / 2; + } + } + e.stopPropagation(); + }; render() { return ( <div className="marqueeView" + ref={r => { + r?.addEventListener('dashDragAutoScroll', this.onDragAutoScroll as any); + this.MarqueeRef = r; + }} style={{ overflow: StrCast(this.props.Document._overflow), cursor: [InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool) || this._visible ? 'crosshair' : 'pointer', diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx index faf7501c4..9a2c79a22 100644 --- a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx +++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx @@ -215,6 +215,7 @@ export class CollectionLinearView extends CollectionSubView() { toggleStatus={BoolCast(this.layoutDoc.linearView_IsOpen)} onClick={() => { this.layoutDoc.linearView_IsOpen = !isExpanded; + ScriptCast(this.rootDoc.onClick)?.script.run({ this: this.rootDoc }, console.log); }} tooltip={isExpanded ? 'Close' : 'Open'} fillWidth={true} diff --git a/src/client/views/global/globalCssVariables.scss b/src/client/views/global/globalCssVariables.scss index 20ccd9ebd..c129d29eb 100644 --- a/src/client/views/global/globalCssVariables.scss +++ b/src/client/views/global/globalCssVariables.scss @@ -4,6 +4,7 @@ $white: #ffffff; $off-white: #fdfdfd; $light-gray: #dfdfdf; $medium-gray: #9f9f9f; +$medium-gray-dim: #9f9f9f30; $dark-gray: #323232; $black: #000000; diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts index d8ed93bd7..894afebfd 100644 --- a/src/client/views/global/globalScripts.ts +++ b/src/client/views/global/globalScripts.ts @@ -13,12 +13,13 @@ import { SelectionManager } from '../../util/SelectionManager'; import { undoable, UndoManager } from '../../util/UndoManager'; import { GestureOverlay } from '../GestureOverlay'; import { InkTranscription } from '../InkTranscription'; -import { ActiveFillColor, SetActiveFillColor, ActiveIsInkMask, SetActiveIsInkMask, ActiveInkWidth, SetActiveInkWidth, ActiveInkColor, SetActiveInkColor } from '../InkingStroke'; +import { ActiveFillColor, SetActiveFillColor, ActiveIsInkMask, SetActiveIsInkMask, ActiveInkWidth, SetActiveInkWidth, ActiveInkColor, SetActiveInkColor, InkingStroke } from '../InkingStroke'; import { CollectionFreeFormView } from '../collections/collectionFreeForm'; import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView'; import { WebBox } from '../nodes/WebBox'; import { RichTextMenu } from '../nodes/formattedText/RichTextMenu'; import { DocumentType } from '../../documents/DocumentTypes'; +import { DocumentView } from '../nodes/DocumentView'; ScriptingGlobals.add(function IsNoneSelected() { return SelectionManager.Views().length <= 0; @@ -46,6 +47,7 @@ ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: b const contentFrameNumber = Cast(selView.rootDoc?._currentFrame, 'number', layoutFrameNumber ?? null); // frame number that content is at which determines what content is displayed return CollectionFreeFormDocumentView.getStringValues(selView?.rootDoc, contentFrameNumber)[fieldKey] ?? 'transparent'; } + selectedViews.some(dv => dv.ComponentView instanceof InkingStroke) && SetActiveFillColor(color ?? 'transparent'); selectedViews.forEach(dv => { const fieldKey = dv.rootDoc.type === DocumentType.INK ? 'fillColor' : 'backgroundColor'; const layoutFrameNumber = Cast(dv.props.docViewPath().lastElement()?.rootDoc?._currentFrame, 'number'); // frame number that container is at which determines layout frame values @@ -61,6 +63,7 @@ ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: b if (checkResult) { return selected.lastElement()?._backgroundColor ?? 'transparent'; } + SetActiveFillColor(color ?? 'transparent'); selected.forEach(doc => (doc._backgroundColor = color)); } }); @@ -68,11 +71,18 @@ ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: b // toggle: Set overlay status of selected document ScriptingGlobals.add(function setHeaderColor(color?: string, checkResult?: boolean) { if (checkResult) { - return Doc.SharingDoc().headingColor; + return SelectionManager.Views().length ? StrCast(SelectionManager.Docs().lastElement().layout_headingColor) : Doc.SharingDoc().headingColor; + } + if (SelectionManager.Views().length) { + SelectionManager.Docs().forEach(doc => { + Doc.GetProto(doc).layout_headingColor = color; + doc.layout_showTitle = color === 'transparent' ? undefined : StrCast(doc.layout_showTitle, 'title'); + }); + } else { + Doc.SharingDoc().headingColor = undefined; + Doc.GetProto(Doc.SharingDoc()).headingColor = color; + Doc.UserDoc().layout_showTitle = color === 'transparent' ? undefined : StrCast(Doc.UserDoc().layout_showTitle, 'author_date'); } - Doc.SharingDoc().headingColor = undefined; - Doc.GetProto(Doc.SharingDoc()).headingColor = color; - Doc.UserDoc().layout_showTitle = color === 'transparent' ? undefined : StrCast(Doc.UserDoc().layout_showTitle, 'author_date'); }); // toggle: Set overlay status of selected document @@ -85,43 +95,43 @@ ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) { selected ? selected.props.CollectionFreeFormDocumentView?.().float() : console.log('[FontIconBox.tsx] toggleOverlay failed'); }); -ScriptingGlobals.add(function showFreeform(attr: 'flashcards' | 'center' | 'grid' | 'snaplines' | 'clusters' | 'arrange' | 'viewAll' | 'viewAllPersist', checkResult?: boolean) { +ScriptingGlobals.add(function showFreeform(attr: 'flashcards' | 'center' | 'grid' | 'snaplines' | 'clusters' | 'arrange' | 'viewAll' | 'fitOnce', checkResult?: boolean) { const selected = SelectionManager.Docs().lastElement(); // prettier-ignore - const map: Map<'flashcards' | 'center' |'grid' | 'snaplines' | 'clusters' | 'arrange'| 'viewAll' | 'viewAllPersist', { waitForRender?: boolean, checkResult: (doc:Doc) => any; setDoc: (doc:Doc) => void;}> = new Map([ + const map: Map<'flashcards' | 'center' |'grid' | 'snaplines' | 'clusters' | 'arrange'| 'viewAll' | 'fitOnce', { waitForRender?: boolean, checkResult: (doc:Doc) => any; setDoc: (doc:Doc, dv:DocumentView) => void;}> = new Map([ ['grid', { checkResult: (doc:Doc) => BoolCast(doc._freeform_backgroundGrid, false), - setDoc: (doc:Doc) => doc._freeform_backgroundGrid = !doc._freeform_backgroundGrid, + setDoc: (doc:Doc,dv:DocumentView) => doc._freeform_backgroundGrid = !doc._freeform_backgroundGrid, }], ['snaplines', { checkResult: (doc:Doc) => BoolCast(doc._freeform_snapLines, false), - setDoc: (doc:Doc) => doc._freeform_snapLines = !doc._freeform_snapLines, + setDoc: (doc:Doc, dv:DocumentView) => doc._freeform_snapLines = !doc._freeform_snapLines, }], ['viewAll', { checkResult: (doc:Doc) => BoolCast(doc._freeform_fitContentsToBox, false), - setDoc: (doc:Doc) => doc._freeform_fitContentsToBox = !doc._freeform_fitContentsToBox, + setDoc: (doc:Doc,dv:DocumentView) => doc._freeform_fitContentsToBox = !doc._freeform_fitContentsToBox, }], ['center', { checkResult: (doc:Doc) => BoolCast(doc._stacking_alignCenter, false), - setDoc: (doc:Doc) => doc._stacking_alignCenter = !doc._stacking_alignCenter, + setDoc: (doc:Doc,dv:DocumentView) => doc._stacking_alignCenter = !doc._stacking_alignCenter, }], - ['viewAllPersist', { + ['fitOnce', { checkResult: (doc:Doc) => false, - setDoc: (doc:Doc) => doc.fitContentOnce = true + setDoc: (doc:Doc, dv:DocumentView) => (dv.ComponentView as CollectionFreeFormView)?.fitContentOnce() }], ['clusters', { waitForRender: true, // flags that undo batch should terminate after a re-render giving the script the chance to fire checkResult: (doc:Doc) => BoolCast(doc._freeform_useClusters, false), - setDoc: (doc:Doc) => doc._freeform_useClusters = !doc._freeform_useClusters, + setDoc: (doc:Doc,dv:DocumentView) => doc._freeform_useClusters = !doc._freeform_useClusters, }], ['arrange', { waitForRender: true, // flags that undo batch should terminate after a re-render giving the script the chance to fire checkResult: (doc:Doc) => BoolCast(doc._autoArrange, false), - setDoc: (doc:Doc) => doc._autoArrange = !doc._autoArrange, + setDoc: (doc:Doc,dv:DocumentView) => doc._autoArrange = !doc._autoArrange, }], ['flashcards', { checkResult: (doc:Doc) => BoolCast(Doc.UserDoc().defaultToFlashcards, false), - setDoc: (doc:Doc) => Doc.UserDoc().defaultToFlashcards = !Doc.UserDoc().defaultToFlashcards, + setDoc: (doc:Doc,dv:DocumentView) => Doc.UserDoc().defaultToFlashcards = !Doc.UserDoc().defaultToFlashcards, }], ]); @@ -129,7 +139,7 @@ ScriptingGlobals.add(function showFreeform(attr: 'flashcards' | 'center' | 'grid return map.get(attr)?.checkResult(selected); } const batch = map.get(attr)?.waitForRender ? UndoManager.StartBatch('set freeform attribute') : { end: () => {} }; - SelectionManager.Docs().map(dv => map.get(attr)?.setDoc(dv)); + SelectionManager.Views().map(dv => map.get(attr)?.setDoc(dv.rootDoc, dv)); setTimeout(() => batch.end(), 100); }); @@ -321,7 +331,7 @@ ScriptingGlobals.add(setActiveTool, 'sets the active ink tool mode'); // toggle: Set overlay status of selected document ScriptingGlobals.add(function setInkProperty(option: 'inkMask' | 'fillColor' | 'strokeWidth' | 'strokeColor', value: any, checkResult?: boolean) { - const selected = SelectionManager.Docs().lastElement(); + const selected = SelectionManager.Docs().lastElement() ?? Doc.UserDoc(); // prettier-ignore const map: Map<'inkMask' | 'fillColor' | 'strokeWidth' | 'strokeColor', { checkResult: () => any; setInk: (doc: Doc) => void; setMode: () => void }> = new Map([ ['inkMask', { @@ -337,12 +347,12 @@ ScriptingGlobals.add(function setInkProperty(option: 'inkMask' | 'fillColor' | ' [ 'strokeWidth', { checkResult: () => (selected?.type === DocumentType.INK ? NumCast(selected.stroke_width) : ActiveInkWidth()), setInk: (doc: Doc) => (doc.stroke_width = NumCast(value)), - setMode: () => SetActiveInkWidth(value.toString()), + setMode: () => { SetActiveInkWidth(value.toString()); setActiveTool( GestureOverlay.Instance.InkShape ?? InkTool.Pen, true, false);}, }], ['strokeColor', { checkResult: () => (selected?.type === DocumentType.INK ? StrCast(selected.color) : ActiveInkColor()), setInk: (doc: Doc) => (doc.color = String(value)), - setMode: () => SetActiveInkColor(StrCast(value)), + setMode: () => { SetActiveInkColor(StrCast(value)); setActiveTool(GestureOverlay.Instance.InkShape ?? InkTool.Pen, true, false);}, }], ]); diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 4a438f826..085f9f023 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -12,7 +12,6 @@ import { SelectionManager } from '../../util/SelectionManager'; import { Transform } from '../../util/Transform'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { DocComponent } from '../DocComponent'; -import { InkingStroke } from '../InkingStroke'; import { StyleProp } from '../StyleProvider'; import './CollectionFreeFormDocumentView.scss'; import { DocumentView, DocumentViewProps, OpenWhere } from './DocumentView'; @@ -183,6 +182,9 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF } }; + dragEnding = () => this.props.CollectionFreeFormView?.dragEnding(); + dragStarting = () => this.props.CollectionFreeFormView?.dragStarting(false, true); + nudge = (x: number, y: number) => { this.props.Document.x = NumCast(this.props.Document.x) + x; this.props.Document.y = NumCast(this.props.Document.y) + y; @@ -191,6 +193,17 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF panelHeight = () => this.sizeProvider?.height || this.props.PanelHeight?.(); screenToLocalTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.X, -this.Y); returnThis = () => this; + + /// this indicates whether the doc view is activated because of its relationshop to a group + // 'group' - this is a group that is activated because it's on an active canvas, but is not part of some other group + // 'child' - this is a group child that is activated because its containing group is activated + // 'inactive' - this is a group child but it is not active + // undefined - this is not activated by a group + isGroupActive = () => { + if (this.props.CollectionFreeFormView.isAnyChildContentActive()) return undefined; + const isGroup = this.rootDoc._isGroup && (!this.rootDoc.backgroundColor || this.rootDoc.backgroundColor === 'transparent'); + return isGroup ? (this.props.isDocumentActive?.() ? 'group' : this.props.isGroupActive?.() ? 'child' : 'inactive') : this.props.isGroupActive?.() ? 'child' : undefined; + }; render() { TraceMobx(); const divProps: DocumentViewProps = { @@ -200,8 +213,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF ScreenToLocalTransform: this.screenToLocalTransform, PanelWidth: this.panelWidth, PanelHeight: this.panelHeight, + isGroupActive: this.isGroupActive, }; - const isInk = StrCast(this.layoutDoc.layout).includes(InkingStroke.name) && !this.props.LayoutTemplateString && !this.layoutDoc._stroke_isInkMask; return ( <div className="collectionFreeFormDocumentView-container" @@ -213,7 +226,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF transition: this.dataProvider?.transition ?? (this.props.dataTransition ? this.props.dataTransition : this.dataProvider ? this.dataProvider.transition : StrCast(this.layoutDoc.dataTransition)), zIndex: this.ZInd, display: this.sizeProvider?.width ? undefined : 'none', - pointerEvents: isInk ? 'none' : undefined, + pointerEvents: 'none', }}> {this.props.renderCutoffProvider(this.props.Document) ? ( <div style={{ position: 'absolute', width: this.panelWidth(), height: this.panelHeight(), background: 'lightGreen' }} /> diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index 4db0bf5fa..50a7f5d7b 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -2,21 +2,21 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; import { action, computed, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; -import { Doc, Opt } from '../../../fields/Doc'; +import { Doc } from '../../../fields/Doc'; import { StrCast } from '../../../fields/Types'; import { emptyFunction, returnFalse, setupMoveUpEvents, StopEvent } from '../../../Utils'; import { DocUtils } from '../../documents/Documents'; import { DragManager } from '../../util/DragManager'; import { Hypothesis } from '../../util/HypothesisUtils'; import { LinkManager } from '../../util/LinkManager'; -import { undoable, undoBatch, UndoManager } from '../../util/UndoManager'; +import { undoBatch, UndoManager } from '../../util/UndoManager'; import './DocumentLinksButton.scss'; import { DocumentView } from './DocumentView'; import { LinkDescriptionPopup } from './LinkDescriptionPopup'; import { TaskCompletionBox } from './TaskCompletedBox'; +import { PinProps } from './trails'; import React = require('react'); import _ = require('lodash'); -import { PinProps } from './trails'; interface DocumentLinksButtonProps { View: DocumentView; @@ -27,6 +27,7 @@ interface DocumentLinksButtonProps { StartLink?: boolean; //whether the link HAS been started (i.e. now needs to be completed) ShowCount?: boolean; scaling?: () => number; // how uch doc is scaled so that link buttons can invert it + hideCount?: () => boolean; } @observer export class DocumentLinksButton extends React.Component<DocumentLinksButtonProps, {}> { @@ -239,6 +240,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp } render() { + if (this.props.hideCount?.()) return null; const menuTitle = this.props.StartLink ? 'Drag or tap to start link' : 'Tap to complete link'; const buttonTitle = 'Tap to view links; double tap to open link collection'; const title = this.props.ShowCount ? buttonTitle : menuTitle; diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index b25540dd3..931594568 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -24,6 +24,8 @@ width: 100%; height: 100%; border-radius: inherit; + // bcz: can't clip the title this way because groups need to be able to render outside of overflow region to support drag/drop-extending the group borders + // overflow: hidden; // need this so that title will be clipped when borderRadius is set // transition: outline 0.3s linear; // background: $white; //overflow: hidden; @@ -118,7 +120,7 @@ display: flex; justify-content: center; align-items: center; - + .sharingIndicator { height: 30px; width: 30px; @@ -165,6 +167,7 @@ height: 100%; border-radius: inherit; white-space: normal; + overflow: hidden; // so that titles will clip when borderRadius is set .documentView-styleContentWrapper { width: 100%; @@ -185,6 +188,7 @@ text-overflow: ellipsis; white-space: pre; position: absolute; + display: flex; // this allows title field dropdown to be inline with editable title } .documentView-titleWrapper-hover { @@ -214,7 +218,7 @@ .documentView-node:hover { > .documentView-styleWrapper { > .documentView-titleWrapper-hover { - display: inline-block; + display: flex; } // > .documentView-contentsView { // opacity: 0.5; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index da665a502..98b13f90f 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,5 +1,6 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; -import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +import { Dropdown, DropdownType, Type } from 'browndash-components'; +import { action, computed, IReactionDisposer, observable, reaction, runInAction, trace } from 'mobx'; import { observer } from 'mobx-react'; import { computedFn } from 'mobx-utils'; import { Bounce, Fade, Flip, LightSpeed, Roll, Rotate, Zoom } from 'react-reveal'; @@ -17,7 +18,7 @@ import { GetEffectiveAcl, TraceMobx } from '../../../fields/util'; import { emptyFunction, isTargetChildOf as isParentOf, lightOrDark, returnEmptyString, returnFalse, returnTrue, returnVal, simulateMouseClick, Utils } from '../../../Utils'; import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; import { DocServer } from '../../DocServer'; -import { Docs, DocUtils } from '../../documents/Documents'; +import { DocOptions, Docs, DocUtils, FInfo } from '../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; import { Networking } from '../../Network'; import { DictationManager } from '../../util/DictationManager'; @@ -38,7 +39,6 @@ import { ContextMenuProps } from '../ContextMenuItem'; import { DocComponent } from '../DocComponent'; import { EditableView } from '../EditableView'; import { GestureOverlay } from '../GestureOverlay'; -import { InkingStroke } from '../InkingStroke'; import { LightboxView } from '../LightboxView'; import { StyleProp } from '../StyleProvider'; import { UndoStack } from '../UndoStack'; @@ -48,11 +48,11 @@ import { DocumentLinksButton } from './DocumentLinksButton'; import './DocumentView.scss'; import { FieldViewProps } from './FieldView'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; +import { KeyValueBox } from './KeyValueBox'; import { LinkAnchorBox } from './LinkAnchorBox'; import { PresEffect, PresEffectDirection } from './trails'; import { PinProps, PresBox } from './trails/PresBox'; import React = require('react'); -import { KeyValueBox } from './KeyValueBox'; const { Howl } = require('howler'); interface Window { @@ -119,8 +119,7 @@ export interface DocComponentView { getView?: (doc: Doc) => Promise<Opt<DocumentView>>; // returns a nested DocumentView for the specified doc or undefined addDocTab?: (doc: Doc, where: OpenWhere) => boolean; // determines how to add a document - used in following links to open the target ina local lightbox addDocument?: (doc: Doc | Doc[], annotationKey?: string) => boolean; // add a document (used only by collections) - reverseNativeScaling?: () => boolean; // DocumentView's setup screenToLocal based on the doc having a nativeWidth/Height. However, some content views (e.g., FreeFormView w/ fitContentsToBox set) may ignore the native dimensions so this flags the DocumentView to not do Nativre scaling. - shrinkWrap?: () => void; // requests a document to display all of its contents with no white space. currently only implemented (needed?) for freeform views + ignoreNativeDimScaling?: () => boolean; // DocumentView's setup screenToLocal based on the doc having a nativeWidth/Height. However, some content views (e.g., FreeFormView w/ fitContentsToBox set) may ignore the native dimensions so this flags the DocumentView to not do Nativre scaling. select?: (ctrlKey: boolean, shiftKey: boolean) => void; focus?: (textAnchor: Doc, options: DocFocusOptions) => Opt<number>; menuControls?: () => JSX.Element; // controls to display in the top menu bar when the document is selected. @@ -137,11 +136,12 @@ export interface DocComponentView { componentUI?: (boundsLeft: number, boundsTop: number) => JSX.Element | null; incrementalRendering?: () => void; layout_fitWidth?: () => boolean; // whether the component always fits width (eg, KeyValueBox) - overridePointerEvents?: () => 'all' | 'none' | undefined; // if the conmponent overrides the pointer events for the document + overridePointerEvents?: () => 'all' | 'none' | undefined; // if the conmponent overrides the pointer events for the document (e.g, KeyValueBox always allows pointer events) fieldKey?: string; annotationKey?: string; getTitle?: () => string; getCenter?: (xf: Transform) => { X: number; Y: number }; + screenBounds?: () => { left: number; top: number; right: number; bottom: number; center?: { X: number; Y: number } }; ptToScreen?: (pt: { X: number; Y: number }) => { X: number; Y: number }; ptFromScreen?: (pt: { X: number; Y: number }) => { X: number; Y: number }; snapPt?: (pt: { X: number; Y: number }, excludeSegs?: number[]) => { nearestPt: { X: number; Y: number }; distance: number }; @@ -154,8 +154,8 @@ export interface DocumentViewSharedProps { renderDepth: number; Document: Doc; DataDoc?: Doc; - contentBounds?: () => undefined | { x: number; y: number; r: number; b: number }; fitContentsToBox?: () => boolean; // used by freeformview to fit its contents to its panel. corresponds to _freeform_fitContentsToBox property on a Document + isGroupActive?: () => string | undefined; // is this document part of a group that is active suppressSetHeight?: boolean; setContentView?: (view: DocComponentView) => any; CollectionFreeFormDocumentView?: () => CollectionFreeFormDocumentView; @@ -219,7 +219,7 @@ export interface DocumentViewProps extends DocumentViewSharedProps { hideLinkAnchors?: boolean; isDocumentActive?: () => boolean | undefined; // whether a document should handle pointer events isContentActive: () => boolean | undefined; // whether document contents should handle pointer events - contentPointerEvents?: string; // pointer events allowed for content of a document view. eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents + contentPointerEvents?: 'none' | 'all' | undefined; // pointer events allowed for content of a document view. eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents radialMenu?: String[]; LayoutTemplateString?: string; dontCenter?: 'x' | 'y' | 'xy'; @@ -272,8 +272,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps return this._animateScaleTime ?? 100; } public get displayName() { - return 'DocumentView(' + this.props.Document.title + ')'; + return 'DocumentViewInternal(' + this.props.Document.title + ')'; } // this makes mobx trace() statements more descriptive + public get ContentDiv() { return this._mainCont.current; } @@ -317,8 +318,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @computed get titleHeight() { return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.TitleHeight) || 0; } + @observable _pointerEvents: 'none' | 'all' | 'visiblePainted' | undefined; @computed get pointerEvents(): 'none' | 'all' | 'visiblePainted' | undefined { - return this.props.styleProvider?.(this.Document, this.props, StyleProp.PointerEvents); + TraceMobx(); + return this._pointerEvents; } @computed get finalLayoutKey() { return StrCast(this.Document.layout_fieldKey, 'layout'); @@ -330,6 +333,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps return this.props.NativeHeight(); } @computed get disableClickScriptFunc() { + TraceMobx(); const onScriptDisable = this.props.onClickScriptDisable ?? this._componentView?.onClickScriptDisable?.() ?? this.layoutDoc.onClickScriptDisable; // prettier-ignore return ( @@ -356,6 +360,25 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } componentDidMount() { this.setupHandlers(); + this._disposers.contentActive = reaction( + () => { + // true - if the document has been activated directly or indirectly (by having its children selected) + // false - if its pointer events are explicitly turned off or if it's container tells it that it's inactive + // undefined - it is not active, but it should be responsive to actions that might activate it or its contents (eg clicking) + return this.props.isContentActive() === false || this.props.pointerEvents?.() === 'none' + ? false + : Doc.ActiveTool !== InkTool.None || SnappingManager.GetCanEmbed() || this.rootSelected() || this.rootDoc.forceActive || this._componentView?.isAnyChildContentActive?.() || this.props.isContentActive() + ? true + : undefined; + }, + active => (this._isContentActive = active), + { fireImmediately: true } + ); + this._disposers.pointerevents = reaction( + () => this.props.styleProvider?.(this.Document, this.props, StyleProp.PointerEvents), + pointerevents => (this._pointerEvents = pointerevents), + { fireImmediately: true } + ); } preDropFunc = (e: Event, de: DragManager.DropEvent) => { const dropAction = this.layoutDoc.dropAction as dropActionType; @@ -425,7 +448,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps public static addDocTabFunc: (doc: Doc, location: OpenWhere) => boolean = returnFalse; onClick = action((e: React.MouseEvent | React.PointerEvent) => { - if (!this.Document.ignoreClick && this.pointerEvents !== 'none' && this.props.renderDepth >= 0 && Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) { + if (this.props.isGroupActive?.() === 'child' && !this.props.isDocumentActive?.()) return; + if (!this.Document.ignoreClick && this.props.renderDepth >= 0 && Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) { let stopPropagate = true; let preventDefault = true; !this.rootDoc._keepZWhenDragged && this.props.bringToFront(this.rootDoc); @@ -496,7 +520,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps // prettier-ignore clickFunc ?? (() => (sendToBack ? this.props.DocumentView().props.bringToFront(this.rootDoc, true) : this._componentView?.select?.(e.ctrlKey || e.metaKey, e.shiftKey) ?? - this.props.select(e.ctrlKey || e.metaKey || e.shiftKey))); + this.props.select(e.ctrlKey||e.shiftKey, e.metaKey))); const waitFordblclick = this.props.waitForDoubleClickToClick?.() ?? this.Document.waitForDoubleClickToClick; if ((clickFunc && waitFordblclick !== 'never') || waitFordblclick === 'always') { this._doubleClickTimeout && clearTimeout(this._doubleClickTimeout); @@ -513,6 +537,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @action onPointerDown = (e: React.PointerEvent): void => { + if (this.props.isGroupActive?.() === 'child' && !this.props.isDocumentActive?.()) return; this._longPressSelector = setTimeout(() => { if (DocumentView.LongPress) { if (this.rootDoc.undoIgnoreFields) { @@ -536,7 +561,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps !this.props.onBrowseClick?.() && !this.Document.ignoreClick && e.button === 0 && - this.pointerEvents !== 'none' && !Doc.IsInMyOverlay(this.layoutDoc) ) { e.stopPropagation(); @@ -622,7 +646,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps if (this.props.Document === Doc.ActiveDashboard) { e.stopPropagation(); e.preventDefault(); - alert((e.target as any)?.closest?.('*.lm_content') ? "You can't perform this move most likely because you don't have permission to modify the destination." : 'Linking to document tabs not yet supported. Drop link on document content.'); + alert( + (e.target as any)?.closest?.('*.lm_content') + ? "You can't perform this move most likely because you didn't drag the document's title bar to enable embedding in a different document." + : 'Linking to document tabs not yet supported. Drop link on document content.' + ); return true; } const linkdrag = de.complete.annoDragData ?? de.complete.linkDragData; @@ -635,6 +663,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps if (de.complete.annoDragData || this.rootDoc !== linkdrag.linkSourceDoc.embedContainer) { const dropDoc = de.complete.annoDragData?.dropDocument ?? this._componentView?.getAnchor?.(true) ?? this.rootDoc; de.complete.linkDocument = DocUtils.MakeLink(linkdrag.linkSourceDoc, dropDoc, {}, undefined, [de.x, de.y - 50]); + if (de.complete.linkDocument) { + de.complete.linkDocument.layout_isSvg = true; + this.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView.addDocument(de.complete.linkDocument); + } } e.stopPropagation(); return true; @@ -874,32 +906,33 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps onClickFunc: any = () => (this.disableClickScriptFunc ? undefined : this.onClickHandler); setHeight = (height: number) => (this.layoutDoc._height = height); setContentView = action((view: { getAnchor?: (addAsAnnotation: boolean) => Doc; forward?: () => boolean; back?: () => boolean }) => (this._componentView = view)); - @computed get _isContentActive() { - // true - if the document has been activated directly or indirectly (by having its children selected) - // false - if its pointer events are explicitly turned off or if it's container tells it that it's inactive - // undefined - it is not active, but it should be responsive to actions that might active it or its contents (eg clicking) - return this.props.isContentActive() === false || this.props.pointerEvents?.() === 'none' - ? false - : Doc.ActiveTool !== InkTool.None || SnappingManager.GetIsDragging() || this.rootSelected() || this.rootDoc.forceActive || this._componentView?.isAnyChildContentActive?.() || this.props.isContentActive() - ? true - : undefined; - } + @observable _isContentActive: boolean | undefined; + isContentActive = (): boolean | undefined => this._isContentActive; childFilters = () => [...this.props.childFilters(), ...StrListCast(this.layoutDoc.childFilters)]; /// disable pointer events on content when there's an enabled onClick script (but not the browse script) and the contents aren't forced active, or if contents are marked inactive @computed get _contentPointerEvents() { - return (!this.disableClickScriptFunc && this.onClickHandler && !this.props.onBrowseClick?.() && this.isContentActive() !== true) || this.isContentActive() === false ? 'none' : this.pointerEvents; + TraceMobx(); + return this.props.contentPointerEvents ?? + ((!this.disableClickScriptFunc && // + this.onClickHandler && + !this.props.onBrowseClick?.() && + this.isContentActive() !== true) || + this.isContentActive() === false) + ? 'none' + : this.pointerEvents; } contentPointerEvents = () => this._contentPointerEvents; @computed get contents() { TraceMobx(); - const isInk = StrCast(this.layoutDoc.layout).includes(InkingStroke.name) && !this.props.LayoutTemplateString; + const isInk = this.layoutDoc._layout_isSvg && !this.props.LayoutTemplateString; + const noBackground = this.rootDoc._isGroup && (!this.rootDoc.backgroundColor || this.rootDoc.backgroundColor === 'transparent'); return ( <div className="documentView-contentsView" style={{ - pointerEvents: (isInk ? 'none' : this.contentPointerEvents()) ?? 'all', + pointerEvents: (isInk || noBackground ? 'none' : this.contentPointerEvents()) ?? 'all', height: this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined, }}> <DocumentContentsView @@ -1063,6 +1096,41 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps }; captionStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string) => this.props?.styleProvider?.(doc, props, property + ':caption'); + @observable _changingTitleField = false; + @observable _dropDownInnerWidth = 0; + fieldsDropdown = (inputOptions: string[], dropdownWidth: number, placeholder: string, onChange: (val: string | number) => void, onClose: () => void) => { + const filteredOptions = new Set(inputOptions); + const scaling = this.titleHeight / 30; /* height of Dropdown */ + Object.entries(DocOptions) + .filter(opts => opts[1].filterable) + .forEach((pair: [string, FInfo]) => filteredOptions.add(pair[0])); + filteredOptions.add(StrCast(this.layoutDoc.layout_showTitle)); + const options = Array.from(filteredOptions) + .filter(f => f) + .map(facet => ({ val: facet, text: facet })); + return ( + <div style={{ width: dropdownWidth }}> + <div + ref={action((r: any) => r && (this._dropDownInnerWidth = Number(getComputedStyle(r).width.replace('px', ''))))} + onPointerDown={action(e => (this._changingTitleField = true))} + style={{ width: 'max-content', transformOrigin: 'left', transform: `scale(${scaling})` }}> + <Dropdown + activeChanged={action(isOpen => !isOpen && (this._changingTitleField = false))} + selectedVal={placeholder} + setSelectedVal={onChange} + color={SettingsManager.userColor} + background={SettingsManager.userVariantColor} + type={Type.TERT} + closeOnSelect={true} + dropdownType={DropdownType.SELECT} + items={options} + width={100} + fillWidth + /> + </div> + </div> + ); + }; @computed get innards() { TraceMobx(); const showTitle = this.layout_showTitle?.split(':')[0]; @@ -1090,8 +1158,15 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps </div> ); const targetDoc = showTitle?.startsWith('_') ? this.layoutDoc : this.rootDoc; - const background = StrCast(SharingManager.Instance.users.find(u => u.user.email === this.dataDoc.author)?.sharingDoc.headingColor, StrCast(Doc.SharingDoc().headingColor, SettingsManager.userBackgroundColor)); + const background = StrCast( + this.layoutDoc.layout_headingColor, + StrCast(SharingManager.Instance.users.find(u => u.user.email === this.dataDoc.author)?.sharingDoc.headingColor, StrCast(this.layoutDoc.layout_headingColor, StrCast(Doc.SharingDoc().headingColor, SettingsManager.userBackgroundColor))) + ); + const dropdownWidth = this._titleRef.current?._editing || this._changingTitleField ? Math.max(10, (this._dropDownInnerWidth * this.titleHeight) / 30) : 0; const sidebarWidthPercent = +StrCast(this.layoutDoc.layout_sidebarWidthPercent).replace('%', ''); + // displays a 'title' at the top of a document. The title contents default to the 'title' field, but can be changed to one or more fields by + // setting layout_showTitle using the format: field1[;field2[...][:hover]] + // from the UI, this is done by clicking the title field and prefixin the format with '#'. eg., #field1[;field2;...][:hover] const titleView = !showTitle ? null : ( <div className={`documentView-titleWrapper${showTitleHover ? '-hover' : ''}`} @@ -1099,39 +1174,58 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps style={{ position: this.headerMargin ? 'relative' : 'absolute', height: this.titleHeight, - width: !this.headerMargin ? `calc(${sidebarWidthPercent || 100}% - 18px)` : (sidebarWidthPercent || 100) + '%', // leave room for annotation button + width: 100 - sidebarWidthPercent + '%', color: background === 'transparent' ? SettingsManager.userColor : lightOrDark(background), background, pointerEvents: (!this.disableClickScriptFunc && this.onClickHandler) || this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined, }}> - <EditableView - ref={this._titleRef} - contents={showTitle - .split(';') - .map(field => field.trim()) - .map(field => targetDoc[field]?.toString()) - .join('\\')} - display={'block'} - fontSize={10} - GetValue={() => { - return showTitle.split(';').length === 1 ? showTitle + '=' + Field.toString(targetDoc[showTitle.split(';')[0]] as any as Field) : '#' + showTitle; - }} - SetValue={undoBatch((input: string) => { - if (input?.startsWith('#')) { - if (this.rootDoc.layout_showTitle) { - this.rootDoc._layout_showTitle = input?.substring(1) ? input.substring(1) : undefined; - } else if (!this.props.layout_showTitle) { - Doc.UserDoc().layout_showTitle = input?.substring(1) ? input.substring(1) : 'author_date'; + {!dropdownWidth + ? null + : this.fieldsDropdown( + [], + dropdownWidth, + StrCast(this.layoutDoc.layout_showTitle).split(':')[0], + action((field: string | number) => { + if (this.rootDoc.layout_showTitle) { + this.rootDoc._layout_showTitle = field; + } else if (!this.props.layout_showTitle) { + Doc.UserDoc().layout_showTitle = field; + } + this._changingTitleField = false; + }), + action(() => (this._changingTitleField = false)) + )} + <div + style={{ + width: `calc(100% - ${dropdownWidth}px)`, + minWidth: '100px', + color: this._titleRef.current?._editing || this._changingTitleField ? 'black' : undefined, + background: this._titleRef.current?._editing || this._changingTitleField ? 'yellow' : undefined, + }}> + <EditableView + ref={this._titleRef} + contents={showTitle + .split(';') + .map(field => targetDoc[field.trim()]?.toString()) + .join(' \\ ')} + display="block" + oneLine={true} + fontSize={(this.titleHeight / 15) * 10} + GetValue={() => (showTitle.split(';').length !== 1 ? '#' + showTitle : Field.toKeyValueString(this.rootDoc, showTitle.split(';')[0]))} + SetValue={undoBatch((input: string) => { + if (input?.startsWith('#')) { + if (this.rootDoc.layout_showTitle) { + this.rootDoc._layout_showTitle = input?.substring(1); + } else if (!this.props.layout_showTitle) { + Doc.UserDoc().layout_showTitle = input?.substring(1) ?? 'author_date'; + } + } else if (showTitle && !showTitle.includes('Date') && showTitle !== 'author') { + KeyValueBox.SetField(targetDoc, showTitle, input); } - } else { - var value = input.replace(new RegExp(showTitle + '='), '') as string | number; - if (showTitle !== 'title' && Number(value).toString() === value) value = Number(value); - if (showTitle.includes('Date') || showTitle === 'author') return true; - Doc.SetInPlace(targetDoc, showTitle, value, true); - } - return true; - })} - /> + return true; + })} + /> + </div> </div> ); return this.props.hideTitle || (!showTitle && !this.layout_showCaption) ? ( @@ -1232,15 +1326,15 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick} - onPointerEnter={e => (!SnappingManager.GetIsDragging() || DragManager.CanEmbed) && Doc.BrushDoc(this.rootDoc)} - onPointerOver={e => (!SnappingManager.GetIsDragging() || DragManager.CanEmbed) && Doc.BrushDoc(this.rootDoc)} + onPointerEnter={e => (!SnappingManager.GetIsDragging() || SnappingManager.GetCanEmbed()) && Doc.BrushDoc(this.rootDoc)} + onPointerOver={e => (!SnappingManager.GetIsDragging() || SnappingManager.GetCanEmbed()) && Doc.BrushDoc(this.rootDoc)} onPointerLeave={e => !isParentOf(this.ContentDiv, document.elementFromPoint(e.nativeEvent.x, e.nativeEvent.y)) && Doc.UnBrushDoc(this.rootDoc)} style={{ borderRadius: this.borderRounding, - pointerEvents: this.pointerEvents === 'visiblePainted' ? 'none' : this.pointerEvents, + pointerEvents: this.pointerEvents === 'visiblePainted' ? 'none' : this.pointerEvents, // visible painted means that the underlying doc contents are irregular and will process their own pointer events (otherwise, the contents are expected to fill the entire doc view box so we can handle pointer events here) }}> <> - {DocumentViewInternal.AnimationEffect(renderDoc, this.rootDoc[Animation], this.rootDoc)} + {this._componentView instanceof KeyValueBox ? renderDoc : DocumentViewInternal.AnimationEffect(renderDoc, this.rootDoc[Animation], this.rootDoc)} {borderPath?.jsx} </> </div> @@ -1354,9 +1448,9 @@ export class DocumentView extends React.Component<DocumentViewProps> { @computed get hideLinkButton() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HideLinkBtn + (this.isSelected() ? ':selected' : '')); } + hideLinkCount = () => this.props.renderDepth === -1 || (this.isSelected() && this.props.renderDepth) || !this._isHovering || this.hideLinkButton; @computed get linkCountView() { - const hideCount = this.props.renderDepth === -1 || SnappingManager.GetIsDragging() || (this.isSelected() && this.props.renderDepth) || !this._isHovering || this.hideLinkButton; - return hideCount ? null : <DocumentLinksButton View={this} scaling={this.scaleToScreenSpace} OnHover={true} Bottom={this.topMost} ShowCount={true} />; + return <DocumentLinksButton hideCount={this.hideLinkCount} View={this} scaling={this.scaleToScreenSpace} OnHover={true} Bottom={this.topMost} ShowCount={true} />; } @computed get docViewPath(): DocumentView[] { return this.props.docViewPath ? [...this.props.docViewPath(), this] : [this]; @@ -1365,10 +1459,10 @@ export class DocumentView extends React.Component<DocumentViewProps> { return Doc.Layout(this.Document, this.props.LayoutTemplate?.()); } @computed get nativeWidth() { - return this.docView?._componentView?.reverseNativeScaling?.() ? 0 : returnVal(this.props.NativeWidth?.(), Doc.NativeWidth(this.layoutDoc, this.props.DataDoc, !this.layout_fitWidth)); + return this.docView?._componentView?.ignoreNativeDimScaling?.() ? 0 : returnVal(this.props.NativeWidth?.(), Doc.NativeWidth(this.layoutDoc, this.props.DataDoc, !this.layout_fitWidth)); } @computed get nativeHeight() { - return this.docView?._componentView?.reverseNativeScaling?.() ? 0 : returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this.props.DataDoc, !this.layout_fitWidth)); + return this.docView?._componentView?.ignoreNativeDimScaling?.() ? 0 : returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this.props.DataDoc, !this.layout_fitWidth)); } @computed get shouldNotScale() { return this.props.shouldNotScale?.() || (this.layout_fitWidth && !this.nativeWidth) || [CollectionViewType.Docking].includes(this.Document._type_collection as any); @@ -1414,16 +1508,20 @@ export class DocumentView extends React.Component<DocumentViewProps> { return this.props.dontCenter?.includes('y') ? 0 : this.Yshift; } - public toggleNativeDimensions = () => this.docView && Doc.toggleNativeDimensions(this.layoutDoc, this.docView.NativeDimScaling, this.props.PanelWidth(), this.props.PanelHeight()); + public toggleNativeDimensions = () => this.docView && this.rootDoc.type !== DocumentType.INK && Doc.toggleNativeDimensions(this.layoutDoc, this.docView.NativeDimScaling, this.props.PanelWidth(), this.props.PanelHeight()); public getBounds = () => { if (!this.docView?.ContentDiv || this.props.treeViewDoc || Doc.AreProtosEqual(this.props.Document, Doc.UserDoc())) { return undefined; } + if (this.docView._componentView?.screenBounds) { + return this.docView._componentView.screenBounds(); + } const xf = this.docView.props .ScreenToLocalTransform() .scale(this.trueNativeWidth() ? this.nativeScaling : 1) .inverse(); const [[left, top], [right, bottom]] = [xf.transformPoint(0, 0), xf.transformPoint(this.panelWidth, this.panelHeight)]; + if (this.docView.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) { const docuBox = this.docView.ContentDiv.getElementsByClassName('linkAnchorBox-cont'); if (docuBox.length) return { ...docuBox[0].getBoundingClientRect(), center: undefined }; @@ -1482,7 +1580,16 @@ export class DocumentView extends React.Component<DocumentViewProps> { scaleToScreenSpace = () => (1 / (this.props.NativeDimScaling?.() || 1)) * this.screenToLocalTransform().Scale; docViewPathFunc = () => this.docViewPath; isSelected = (outsideReaction?: boolean) => SelectionManager.IsSelected(this, outsideReaction); - select = (extendSelection: boolean) => SelectionManager.SelectView(this, extendSelection); + select = (extendSelection: boolean, focusSelection?: boolean) => { + SelectionManager.SelectView(this, extendSelection); + if (focusSelection) { + DocumentManager.Instance.showDocument(this.rootDoc, { + willZoomCentered: true, + zoomScale: 0.9, + zoomTime: 500, + }); + } + }; NativeWidth = () => this.effectiveNativeWidth; NativeHeight = () => this.effectiveNativeHeight; PanelWidth = () => this.panelWidth; diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index 673f711be..95aeb5331 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -41,7 +41,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { componentDidMount() { this.props.setContentView?.(this); } - reverseNativeScaling = returnTrue; + ignoreNativeDimScaling = returnTrue; able = returnAlways; layout_fitWidth = returnTrue; overridePointerEvents = returnAll; diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx index efb949a47..e66fed84b 100644 --- a/src/client/views/nodes/LinkBox.tsx +++ b/src/client/views/nodes/LinkBox.tsx @@ -1,6 +1,14 @@ import React = require('react'); +import { Bezier } from 'bezier-js'; +import { computed, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; -import { emptyFunction, returnAlways, returnFalse, returnTrue } from '../../../Utils'; +import { Height, Width } from '../../../fields/DocSymbols'; +import { Id } from '../../../fields/FieldSymbols'; +import { DocCast, NumCast, StrCast } from '../../../fields/Types'; +import { aggregateBounds, emptyFunction, returnAlways, returnFalse, Utils } from '../../../Utils'; +import { DocumentManager } from '../../util/DocumentManager'; +import { Transform } from '../../util/Transform'; +import { CollectionFreeFormView } from '../collections/collectionFreeForm'; import { ViewBoxBaseComponent } from '../DocComponent'; import { StyleProp } from '../StyleProvider'; import { ComparisonBox } from './ComparisonBox'; @@ -14,11 +22,138 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { } onClickScriptDisable = returnAlways; + @computed get anchor1() { + const anchor1 = DocCast(this.rootDoc.link_anchor_1); + const anchor_1 = anchor1?.layout_unrendered ? DocCast(anchor1.annotationOn) : anchor1; + return DocumentManager.Instance.getDocumentView(anchor_1, this.props.docViewPath()[this.props.docViewPath().length - 2]); // this.props.docViewPath().lastElement()); + } + @computed get anchor2() { + const anchor2 = DocCast(this.rootDoc.link_anchor_2); + const anchor_2 = anchor2?.layout_unrendered ? DocCast(anchor2.annotationOn) : anchor2; + return DocumentManager.Instance.getDocumentView(anchor_2, this.props.docViewPath()[this.props.docViewPath().length - 2]); // this.props.docViewPath().lastElement()); + } + screenBounds = () => { + if (this.layoutDoc._layout_isSvg && this.anchor1 && this.anchor2 && this.anchor1.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView) { + const a_invXf = this.anchor1.props.ScreenToLocalTransform().inverse(); + const b_invXf = this.anchor2.props.ScreenToLocalTransform().inverse(); + const a_scrBds = { tl: a_invXf.transformPoint(0, 0), br: a_invXf.transformPoint(this.anchor1.rootDoc[Width](), this.anchor1.rootDoc[Height]()) }; + const b_scrBds = { tl: b_invXf.transformPoint(0, 0), br: b_invXf.transformPoint(this.anchor2.rootDoc[Width](), this.anchor2.rootDoc[Height]()) }; + + const pts = [] as number[][]; + pts.push([(a_scrBds.tl[0] + a_scrBds.br[0]) / 2, (a_scrBds.tl[1] + a_scrBds.br[1]) / 2]); + pts.push(Utils.getNearestPointInPerimeter(a_scrBds.tl[0], a_scrBds.tl[1], a_scrBds.br[0] - a_scrBds.tl[0], a_scrBds.br[1] - a_scrBds.tl[1], (b_scrBds.tl[0] + b_scrBds.br[0]) / 2, (b_scrBds.tl[1] + b_scrBds.br[1]) / 2)); + pts.push(Utils.getNearestPointInPerimeter(b_scrBds.tl[0], b_scrBds.tl[1], b_scrBds.br[0] - b_scrBds.tl[0], b_scrBds.br[1] - b_scrBds.tl[1], (a_scrBds.tl[0] + a_scrBds.br[0]) / 2, (a_scrBds.tl[1] + a_scrBds.br[1]) / 2)); + pts.push([(b_scrBds.tl[0] + b_scrBds.br[0]) / 2, (b_scrBds.tl[1] + b_scrBds.br[1]) / 2]); + const agg = aggregateBounds( + pts.map(pt => ({ x: pt[0], y: pt[1] })), + 0, + 0 + ); + return { left: agg.x, top: agg.y, right: agg.r, bottom: agg.b, center: undefined }; + } + return { left: 0, top: 0, right: 0, bottom: 0, center: undefined }; + }; + disposer: IReactionDisposer | undefined; componentDidMount() { this.props.setContentView?.(this); + this.disposer = reaction( + () => { + if (this.layoutDoc._layout_isSvg && (this.anchor1 || this.anchor2)?.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView) { + const a = (this.anchor1 ?? this.anchor2)!; + const b = (this.anchor2 ?? this.anchor1)!; + + const parxf = this.props.docViewPath()[this.props.docViewPath().length - 2].ComponentView as CollectionFreeFormView; + const this_xf = parxf?.getTransform() ?? Transform.Identity; //this.props.ScreenToLocalTransform(); + const a_invXf = a.props.ScreenToLocalTransform().inverse(); + const b_invXf = b.props.ScreenToLocalTransform().inverse(); + const a_scrBds = { tl: a_invXf.transformPoint(0, 0), br: a_invXf.transformPoint(a.rootDoc[Width](), a.rootDoc[Height]()) }; + const b_scrBds = { tl: b_invXf.transformPoint(0, 0), br: b_invXf.transformPoint(b.rootDoc[Width](), b.rootDoc[Height]()) }; + const a_bds = { tl: this_xf.transformPoint(a_scrBds.tl[0], a_scrBds.tl[1]), br: this_xf.transformPoint(a_scrBds.br[0], a_scrBds.br[1]) }; + const b_bds = { tl: this_xf.transformPoint(b_scrBds.tl[0], b_scrBds.tl[1]), br: this_xf.transformPoint(b_scrBds.br[0], b_scrBds.br[1]) }; + + const ppt1 = [(a_bds.tl[0] + a_bds.br[0]) / 2, (a_bds.tl[1] + a_bds.br[1]) / 2]; + const pt1 = Utils.getNearestPointInPerimeter(a_bds.tl[0], a_bds.tl[1], a_bds.br[0] - a_bds.tl[0], a_bds.br[1] - a_bds.tl[1], (b_bds.tl[0] + b_bds.br[0]) / 2, (b_bds.tl[1] + b_bds.br[1]) / 2); + const pt2 = Utils.getNearestPointInPerimeter(b_bds.tl[0], b_bds.tl[1], b_bds.br[0] - b_bds.tl[0], b_bds.br[1] - b_bds.tl[1], (a_bds.tl[0] + a_bds.br[0]) / 2, (a_bds.tl[1] + a_bds.br[1]) / 2); + const ppt2 = [(b_bds.tl[0] + b_bds.br[0]) / 2, (b_bds.tl[1] + b_bds.br[1]) / 2]; + + const pts = [ppt1, pt1, pt2, ppt2].map(pt => [pt[0], pt[1]]); + const [lx, rx, ty, by] = [Math.min(pt1[0], pt2[0]), Math.max(pt1[0], pt2[0]), Math.min(pt1[1], pt2[1]), Math.max(pt1[1], pt2[1])]; + return { pts, lx, rx, ty, by }; + } + return undefined; + }, + params => { + this.renderProps = params; + if (params) { + if ( + Math.abs(params.lx - NumCast(this.layoutDoc.x)) > 1e-5 || + Math.abs(params.ty - NumCast(this.layoutDoc.y)) > 1e-5 || + Math.abs(params.rx - params.lx - NumCast(this.layoutDoc._width)) > 1e-5 || + Math.abs(params.by - params.ty - NumCast(this.layoutDoc._height)) > 1e-5 + ) { + this.layoutDoc.x = params?.lx; + this.layoutDoc.y = params?.ty; + this.layoutDoc._width = params.rx - params?.lx; + this.layoutDoc._height = params?.by - params?.ty; + } + } + }, + { fireImmediately: true } + ); } + componentWillUnmount(): void { + this.disposer?.(); + } + @observable renderProps: { lx: number; rx: number; ty: number; by: number; pts: number[][] } | undefined; render() { - if (this.dataDoc.treeView_Open === undefined) setTimeout(() => (this.dataDoc.treeView_Open = true)); + if (this.renderProps) { + const highlight = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Highlighting); + const highlightColor = highlight?.highlightIndex ? highlight?.highlightColor : undefined; + + const bez = new Bezier(this.renderProps.pts.map(p => ({ x: p[0], y: p[1] }))); + const text = bez.get(0.5); + const linkDesc = StrCast(this.rootDoc.link_description) || 'description'; + const strokeWidth = NumCast(this.rootDoc.stroke_width, 4); + const dash = StrCast(this.rootDoc.stroke_dash); + const strokeDasharray = dash && Number(dash) ? String(strokeWidth * Number(dash)) : undefined; + const { pts, lx, ty, rx, by } = this.renderProps; + return ( + <div style={{ transition: 'inherit', pointerEvents: 'none', position: 'absolute', width: '100%', height: '100%' }}> + <svg width={Math.max(100, rx - lx)} height={Math.max(100, by - ty)} style={{ transition: 'inherit', overflow: 'visible' }}> + <defs> + <filter x="0" y="0" width="1" height="1" id={`${this.rootDoc[Id] + 'background'}`}> + <feFlood floodColor={`${StrCast(this.rootDoc._backgroundColor, 'lightblue')}`} result="bg" /> + <feMerge> + <feMergeNode in="bg" /> + <feMergeNode in="SourceGraphic" /> + </feMerge> + </filter> + </defs> + <path + className="collectionfreeformlinkview-linkLine" + style={{ + pointerEvents: this.props.pointerEvents?.() === 'none' ? 'none' : 'visibleStroke', // + stroke: highlightColor ?? 'lightblue', + strokeDasharray, + strokeWidth, + transition: 'inherit', + }} + d={`M ${pts[1][0] - lx} ${pts[1][1] - ty} C ${pts[1][0] + pts[1][0] - pts[0][0] - lx} ${pts[1][1] + pts[1][1] - pts[0][1] - ty}, + ${pts[2][0] + pts[2][0] - pts[3][0] - lx} ${pts[2][1] + pts[2][1] - pts[3][1] - ty}, ${pts[2][0] - lx} ${pts[2][1] - ty}`} + /> + <text + filter={`url(#${this.rootDoc[Id] + 'background'})`} + style={{ pointerEvents: this.props.pointerEvents?.() === 'none' ? 'none' : 'all', textAnchor: 'middle', fontSize: '12', stroke: 'black' }} + x={text.x - lx} + y={text.y - ty}> + <tspan> </tspan> + <tspan dy="2">{linkDesc.substring(0, 50) + (linkDesc.length > 50 ? '...' : '')}</tspan> + <tspan dy="2"> </tspan> + </text> + </svg> + </div> + ); + } return ( <div className={`linkBox-container${this.props.isContentActive() ? '-interactive' : ''}`} style={{ background: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor) }}> <ComparisonBox diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 537da5055..c068d9dd7 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -212,8 +212,6 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps ); } - brushView = (view: { width: number; height: number; panX: number; panY: number }, transTime: number) => this._pdfViewer?.brushView(view, transTime); - sidebarAddDocTab = (doc: Doc, where: OpenWhere) => { if (DocListCast(this.props.Document[this.props.fieldKey + '_sidebar']).includes(doc) && !this.SidebarShown) { this.toggleSidebar(false); diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx index 481e43feb..116069cbd 100644 --- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx @@ -123,7 +123,7 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() { Doc.AddToMyOverlay(value); DocumentManager.Instance.AddViewRenderedCb(value, docView => { Doc.UserDoc().currentRecording = docView.rootDoc; - SelectionManager.SelectSchemaViewDoc(value); + docView.select(false); RecordingBox.resumeWorkspaceReplaying(value); }); } diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 58a765d61..2aca314da 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -10,13 +10,12 @@ import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; import { RefField } from '../../../fields/RefField'; import { listSpec } from '../../../fields/Schema'; -import { Cast, ImageCast, NumCast, StrCast, WebCast } from '../../../fields/Types'; +import { Cast, NumCast, StrCast, WebCast } from '../../../fields/Types'; import { ImageField, WebField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, getWordAtPoint, lightOrDark, returnFalse, returnOne, returnZero, setupMoveUpEvents, smoothScroll, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentManager } from '../../util/DocumentManager'; -import { DragManager } from '../../util/DragManager'; import { ScriptingGlobals } from '../../util/ScriptingGlobals'; import { SnappingManager } from '../../util/SnappingManager'; import { undoBatch, UndoManager } from '../../util/UndoManager'; @@ -51,7 +50,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps public static sidebarResizerWidth = 5; static webStyleSheet = addStyleSheet(); private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void); - private _setBrushViewer: undefined | ((view: { width: number; height: number; panX: number; panY: number }, transTime: number) => void); private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); private _outerRef: React.RefObject<HTMLDivElement> = React.createRef(); private _disposers: { [name: string]: IReactionDisposer } = {}; @@ -275,8 +273,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps return this._savedAnnotations; }; - setBrushViewer = (func?: (view: { width: number; height: number; panX: number; panY: number }, transTime: number) => void) => (this._setBrushViewer = func); - brushView = (view: { width: number; height: number; panX: number; panY: number }, transTime: number) => this._setBrushViewer?.(view, transTime); focus = (anchor: Doc, options: DocFocusOptions) => { if (anchor !== this.rootDoc && this._outerRef.current) { const windowHeight = this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); @@ -864,7 +860,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps key="sidebar" title="Toggle Sidebar" style={{ - display: !this.props.isContentActive() ? 'none' : undefined, top: StrCast(this.layoutDoc._layout_showTitle) === 'title' ? 20 : 5, backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, }} @@ -953,43 +948,48 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @computed get SidebarShown() { return this._showSidebar || this.layoutDoc._layout_showSidebar ? true : false; } + renderAnnotations = (childFilters: () => string[]) => ( + <CollectionFreeFormView + {...this.props} + setContentView={this.setInnerContent} + NativeWidth={returnZero} + NativeHeight={returnZero} + originTopLeft={false} + isAnnotationOverlayScrollable={true} + renderDepth={this.props.renderDepth + 1} + isAnnotationOverlay={true} + fieldKey={this.annotationKey} + setPreviewCursor={this.setPreviewCursor} + PanelWidth={this.panelWidth} + PanelHeight={this.panelHeight} + ScreenToLocalTransform={this.scrollXf} + NativeDimScaling={returnOne} + focus={this.focus} + childFilters={childFilters} + select={emptyFunction} + isAnyChildContentActive={returnFalse} + bringToFront={emptyFunction} + styleProvider={this.childStyleProvider} + whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} + removeDocument={this.removeDocument} + moveDocument={this.moveDocument} + addDocument={this.addDocumentWrapper} + childPointerEvents={this.childPointerEvents} + pointerEvents={this.annotationPointerEvents} + /> + ); + @computed get renderOpaqueAnnotations() { + return this.renderAnnotations(this.opaqueFilter); + } + @computed get renderTransparentAnnotations() { + return this.renderAnnotations(this.transparentFilter); + } childPointerEvents = () => (this.props.isContentActive() ? 'all' : undefined); @computed get webpage() { const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1; const pointerEvents = this.layoutDoc._lockedPosition ? 'none' : (this.props.pointerEvents?.() as any); const scale = previewScale * (this.props.NativeDimScaling?.() || 1); - const renderAnnotations = (childFilters: () => string[]) => ( - <CollectionFreeFormView - {...this.props} - setContentView={this.setInnerContent} - NativeWidth={returnZero} - NativeHeight={returnZero} - originTopLeft={false} - isAnnotationOverlayScrollable={true} - renderDepth={this.props.renderDepth + 1} - isAnnotationOverlay={true} - fieldKey={this.annotationKey} - setPreviewCursor={this.setPreviewCursor} - setBrushViewer={this.setBrushViewer} - PanelWidth={this.panelWidth} - PanelHeight={this.panelHeight} - ScreenToLocalTransform={this.scrollXf} - NativeDimScaling={returnOne} - focus={this.focus} - childFilters={childFilters} - select={emptyFunction} - isAnyChildContentActive={returnFalse} - bringToFront={emptyFunction} - styleProvider={this.childStyleProvider} - whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} - removeDocument={this.removeDocument} - moveDocument={this.moveDocument} - addDocument={this.addDocumentWrapper} - childPointerEvents={this.childPointerEvents} - pointerEvents={this.annotationPointerEvents} - /> - ); return ( <div className="webBox-outerContent" @@ -1004,8 +1004,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps onPointerDown={this.onMarqueeDown}> <div className="webBox-innerContent" style={{ height: (this._webPageHasBeenRendered && this._scrollHeight) || '100%', pointerEvents }}> {this.content} - {<div style={{ display: DragManager.docsBeingDragged.length ? 'none' : undefined, mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div>} - {renderAnnotations(this.opaqueFilter)} + <div style={{ display: SnappingManager.GetCanEmbed() ? 'none' : undefined, mixBlendMode: 'multiply' }}>{this.renderTransparentAnnotations}</div> + {this.renderOpaqueAnnotations} {this.annotationLayer} </div> </div> @@ -1054,7 +1054,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._layout_scrollTop)); anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick; transparentFilter = () => [...this.props.childFilters(), Utils.IsTransparentFilter()]; - opaqueFilter = () => [...this.props.childFilters(), Utils.noDragsDocFilter, ...(DragManager.docsBeingDragged.length ? [] : [Utils.IsOpaqueFilter()])]; + opaqueFilter = () => [...this.props.childFilters(), Utils.noDragsDocFilter, ...(SnappingManager.GetCanEmbed() ? [] : [Utils.IsOpaqueFilter()])]; childStyleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string): any => { if (doc instanceof Doc && property === StyleProp.PointerEvents) { if (this.inlineTextAnnotations.includes(doc)) return 'none'; @@ -1135,8 +1135,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps removeDocument={this.removeDocument} /> </div> - {this.sidebarHandle} - {!this.props.isContentActive() ? null : this.searchUI} + {!this.props.isContentActive() || SnappingManager.GetIsDragging() ? null : this.sidebarHandle} + {!this.props.isContentActive() || SnappingManager.GetIsDragging() ? null : this.searchUI} </div> ); } diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss index 348bdd79e..818c0cbe7 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss @@ -5,6 +5,14 @@ height: 100%; min-height: 100%; } +.formattedTextBox-inner.centered, +.formattedTextBox-inner-rounded.centered { + align-items: center; + display: flex; + .ProseMirror { + min-height: unset; + } +} .ProseMirror:focus { outline: none !important; @@ -29,7 +37,6 @@ audiotag:hover { background: inherit; padding: 0; border-width: 0px; - border-radius: inherit; border-color: $medium-gray; box-sizing: border-box; background-color: inherit; diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index fcdfbdf2a..41b1c59b0 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -2149,7 +2149,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps onScroll={this.onScroll} onDrop={this.ondrop}> <div - className={`formattedTextBox-inner${rounded}`} + className={`formattedTextBox-inner${rounded} ${this.layoutDoc.layout_centered ? 'centered' : ''}`} ref={this.createDropTarget} style={{ padding: StrCast(this.layoutDoc._textBoxPadding), diff --git a/src/client/views/nodes/generativeFill/GenerativeFill.tsx b/src/client/views/nodes/generativeFill/GenerativeFill.tsx index 1ec6d6e3f..2b0927148 100644 --- a/src/client/views/nodes/generativeFill/GenerativeFill.tsx +++ b/src/client/views/nodes/generativeFill/GenerativeFill.tsx @@ -9,7 +9,9 @@ import { NumCast } from '../../../../fields/Types'; import { Utils } from '../../../../Utils'; import { Docs, DocUtils } from '../../../documents/Documents'; import { Networking } from '../../../Network'; +import { DocumentManager } from '../../../util/DocumentManager'; import { CollectionDockingView } from '../../collections/CollectionDockingView'; +import { CollectionFreeFormView } from '../../collections/collectionFreeForm'; import { OpenWhereMod } from '../DocumentView'; import { ImageBox } from '../ImageBox'; import './GenerativeFill.scss'; @@ -303,19 +305,21 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD originalImg.current = currImg.current; originalDoc.current = parentDoc.current; const { urls } = res as APISuccess; - const imgUrls = await Promise.all(urls.map(url => ImageUtility.convertImgToCanvasUrl(url, canvasDims.width, canvasDims.height))); - const imgRes = await Promise.all( - imgUrls.map(async url => { - const saveRes = await onSave(url); - return [url, saveRes as Doc]; - }) - ); - setEdits(imgRes); - const image = new Image(); - image.src = imgUrls[0]; - ImageUtility.drawImgToCanvas(image, canvasRef, canvasDims.width, canvasDims.height); - currImg.current = image; - parentDoc.current = imgRes[0][1] as Doc; + if (res.status !== 'error') { + const imgUrls = await Promise.all(urls.map(url => ImageUtility.convertImgToCanvasUrl(url, canvasDims.width, canvasDims.height))); + const imgRes = await Promise.all( + imgUrls.map(async url => { + const saveRes = await onSave(url); + return [url, saveRes as Doc]; + }) + ); + setEdits(imgRes); + const image = new Image(); + image.src = imgUrls[0]; + ImageUtility.drawImgToCanvas(image, canvasRef, canvasDims.width, canvasDims.height); + currImg.current = image; + parentDoc.current = imgRes[0][1] as Doc; + } } catch (err) { console.log(err); } @@ -418,7 +422,7 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD ImageBox.setImageEditorOpen(false); ImageBox.setImageEditorSource(''); if (newCollectionRef.current) { - newCollectionRef.current.fitContentOnce = true; + DocumentManager.Instance.AddViewRenderedCb(newCollectionRef.current, dv => (dv.ComponentView as CollectionFreeFormView)?.fitContentOnce()); } setEdits([]); }; diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index 383b400c8..3d7c68bcd 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -3,7 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; import { action, computed, IReactionDisposer, observable, ObservableSet, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; -import { Doc, DocListCast, FieldResult, Opt, StrListCast } from '../../../../fields/Doc'; +import { Doc, DocListCast, FieldResult, NumListCast, Opt, StrListCast } from '../../../../fields/Doc'; import { Animation } from '../../../../fields/DocSymbols'; import { Copy, Id } from '../../../../fields/FieldSymbols'; import { InkField } from '../../../../fields/InkField'; @@ -518,13 +518,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { changed = true; } } + if (pinDataTypes?.temporal || (!pinDataTypes && activeItem.timecodeToShow !== undefined)) { + if (bestTarget._layout_currentTimecode !== activeItem.timecodeToShow) { + bestTarget._layout_currentTimecode = activeItem.timecodeToShow; + changed = true; + } + } if (pinDataTypes?.inkable || (!pinDataTypes && (activeItem.config_fillColor !== undefined || activeItem.color !== undefined))) { if (bestTarget.fillColor !== activeItem.config_fillColor) { - Doc.GetProto(bestTarget).fillColor = activeItem.config_fillColor; + Doc.GetProto(bestTarget).fillColor = StrCast(activeItem.config_fillColor, StrCast(bestTarget.fillColor)); changed = true; } if (bestTarget.color !== activeItem.config_color) { - Doc.GetProto(bestTarget).color = activeItem.config_color; + Doc.GetProto(bestTarget).color = StrCast(activeItem.config_color, StrCast(bestTarget.color)); changed = true; } if (bestTarget.width !== activeItem.width) { @@ -565,11 +571,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (bestTarget._layout_scrollTop !== activeItem.config_scrollTop) { bestTarget._layout_scrollTop = activeItem.config_scrollTop; changed = true; - const contentBounds = Cast(activeItem.config_viewBounds, listSpec('number')); - if (contentBounds) { - const dv = DocumentManager.Instance.getDocumentView(bestTarget)?.ComponentView; - dv?.brushView?.({ panX: (contentBounds[0] + contentBounds[2]) / 2, panY: (contentBounds[1] + contentBounds[3]) / 2, width: contentBounds[2] - contentBounds[0], height: contentBounds[3] - contentBounds[1] }, transTime); - } } } if (pinDataTypes?.dataannos || (!pinDataTypes && activeItem.config_annotations !== undefined)) { @@ -830,7 +831,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.childDocs.forEach((doc, index) => { const curDoc = Cast(doc, Doc, null); const tagDoc = PresBox.targetRenderedDoc(curDoc); - const itemIndexes: number[] = this.getAllIndexes(this.tagDocs, tagDoc); + const itemIndexes: number[] = this.getAllIndexes(this.tagDocs, curDoc); let opacity: Opt<number> = index === this.itemIndex ? 1 : undefined; if (curDoc.presentation_hide) { if (index !== this.itemIndex) { @@ -864,7 +865,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { opacity = 0; } } - opacity !== undefined && (tagDoc.opacity = opacity); + opacity !== undefined && (tagDoc.opacity = opacity === 1 ? undefined : opacity); }); }; @@ -1301,71 +1302,55 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { getAllIndexes = (arr: Doc[], val: Doc) => arr.map((doc, i) => (doc === val ? i : -1)).filter(i => i !== -1); // Adds the index in the pres path graphically - @computed get order() { + orderedPathLabels = (collection: Doc) => { const order: JSX.Element[] = []; - const docs: Doc[] = []; - const presCollection = DocumentManager.GetContextPath(this.activeItem).reverse().lastElement(); + const docs = new Set<Doc>(); + const presCollection = collection; const dv = DocumentManager.Instance.getDocumentView(presCollection); - this.childDocs - .filter(doc => Cast(doc.presentation_targetDoc, Doc, null)) - .forEach((doc, index) => { - const tagDoc = Cast(doc.presentation_targetDoc, Doc, null); - const srcContext = Cast(tagDoc.embedContainer, Doc, null); + this.childDocs.forEach((doc, index) => { + const tagDoc = PresBox.targetRenderedDoc(doc); + const srcContext = Cast(tagDoc.embedContainer, Doc, null); + const labelCreator = (top: number, left: number, edge: number, fontSize: number) => ( + <div className="pathOrder" key={tagDoc.id + 'pres' + index} style={{ top, left, width: edge, height: edge, fontSize }} onClick={() => this.selectElement(doc)}> + <div className="pathOrder-frame">{index + 1}</div> + </div> + ); + if (presCollection === srcContext) { + const gap = 2; const width = NumCast(tagDoc._width) / 10; const height = Math.max(NumCast(tagDoc._height) / 10, 15); const edge = Math.max(width, height); const fontSize = edge * 0.8; - const gap = 2; - if (presCollection === srcContext) { - // Case A: Document is contained within the collection - if (docs.includes(tagDoc)) { - const prevOccurances: number = this.getAllIndexes(docs, tagDoc).length; - docs.push(tagDoc); - order.push( - <div - className="pathOrder" - key={tagDoc.id + 'pres' + index} - style={{ top: NumCast(tagDoc.y) + (prevOccurances * (edge + gap) - edge / 2), left: NumCast(tagDoc.x) - edge / 2, width: edge, height: edge, fontSize: fontSize }} - onClick={() => this.selectElement(doc)}> - <div className="pathOrder-frame">{index + 1}</div> - </div> - ); - } else { - docs.push(tagDoc); - order.push( - <div - className="pathOrder" - key={tagDoc.id + 'pres' + index} - style={{ top: NumCast(tagDoc.y) - edge / 2, left: NumCast(tagDoc.x) - edge / 2, width: edge, height: edge, fontSize: fontSize }} - onClick={() => this.selectElement(doc)}> - <div className="pathOrder-frame">{index + 1}</div> - </div> - ); - } - } else if (doc.config_pinView && presCollection === tagDoc && dv) { - // Case B: Document is presPinView and is presCollection - const scale: number = 1 / NumCast(doc.config_viewScale); - const height: number = dv.props.PanelHeight() * scale; - const width: number = dv.props.PanelWidth() * scale; - const indWidth = width / 10; - const indHeight = Math.max(height / 10, 15); - const indEdge = Math.max(indWidth, indHeight); - const indFontSize = indEdge * 0.8; - const xLoc: number = NumCast(doc.config_panX) - width / 2; - const yLoc: number = NumCast(doc.config_panY) - height / 2; - docs.push(tagDoc); - order.push( - <> - <div className="pathOrder" key={tagDoc.id + 'pres' + index} style={{ top: yLoc - indEdge / 2, left: xLoc - indEdge / 2, width: indEdge, height: indEdge, fontSize: indFontSize }} onClick={() => this.selectElement(doc)}> - <div className="pathOrder-frame">{index + 1}</div> - </div> - <div className="pathOrder-presPinView" style={{ top: yLoc, left: xLoc, width: width, height: height, borderWidth: indEdge / 10 }}></div> - </> - ); + // Case A: Document is contained within the collection + if (docs.has(tagDoc)) { + const prevOccurences = this.getAllIndexes(Array.from(docs), tagDoc).length; + order.push(labelCreator(NumCast(tagDoc.y) + (prevOccurences * (edge + gap) - edge / 2), NumCast(tagDoc.x) - edge / 2, edge, fontSize)); + } else { + order.push(labelCreator(NumCast(tagDoc.y) - edge / 2, NumCast(tagDoc.x) - edge / 2, edge, fontSize)); } - }); + } else if (doc.config_pinView && presCollection === tagDoc && dv) { + // Case B: Document is presPinView and is presCollection + const scale = 1 / NumCast(doc.config_viewScale); + const viewBounds = NumListCast(doc.config_viewBounds, [0, 0, dv.props.PanelWidth(), dv.props.PanelHeight()]); + const height = (viewBounds[3] - viewBounds[1]) * scale; + const width = (viewBounds[2] - viewBounds[0]) * scale; + const indWidth = width / 10; + const indHeight = Math.max(height / 10, 15); + const indEdge = Math.max(indWidth, indHeight); + const indFontSize = indEdge * 0.8; + const left = NumCast(doc.config_panX) - width / 2; + const top = NumCast(doc.config_panY) - height / 2; + order.push( + <> + {labelCreator(top - indEdge / 2, left - indEdge / 2, indEdge, indFontSize)} + <div className="pathOrder-presPinView" style={{ top, left, width, height, borderWidth: indEdge / 10 }}></div> + </> + ); + } + docs.add(tagDoc); + }); return order; - } + }; /** * Method called for viewing paths which adds a single line with @@ -1375,41 +1360,57 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { * (Design needed for when documents in presentation trail are in another * collection) */ - @computed get paths() { + pathLines = (collection: Doc) => { let pathPoints = ''; - this.childDocs.forEach((doc, index) => { - const tagDoc = PresBox.targetRenderedDoc(doc); - if (tagDoc) { - const n1x = NumCast(tagDoc.x) + NumCast(tagDoc._width) / 2; - const n1y = NumCast(tagDoc.y) + NumCast(tagDoc._height) / 2; - if ((index = 0)) pathPoints = n1x + ',' + n1y; - else pathPoints = pathPoints + ' ' + n1x + ',' + n1y; - } else if (doc.config_pinView) { - const n1x = NumCast(doc.config_panX); - const n1y = NumCast(doc.config_panY); - if ((index = 0)) pathPoints = n1x + ',' + n1y; - else pathPoints = pathPoints + ' ' + n1x + ',' + n1y; - } - }); + this.childDocs + .filter(doc => PresBox.targetRenderedDoc(doc)?.embedContainer === collection) + .forEach((doc, index) => { + const tagDoc = PresBox.targetRenderedDoc(doc); + if (tagDoc) { + const n1x = NumCast(tagDoc.x) + NumCast(tagDoc._width) / 2; + const n1y = NumCast(tagDoc.y) + NumCast(tagDoc._height) / 2; + if ((index = 0)) pathPoints = n1x + ',' + n1y; + else pathPoints = pathPoints + ' ' + n1x + ',' + n1y; + } else if (doc.config_pinView) { + const n1x = NumCast(doc.config_panX); + const n1y = NumCast(doc.config_panY); + if ((index = 0)) pathPoints = n1x + ',' + n1y; + else pathPoints = pathPoints + ' ' + n1x + ',' + n1y; + } + }); return ( - <polyline - points={pathPoints} - style={{ - opacity: 1, - stroke: '#69a6db', - strokeWidth: 5, - strokeDasharray: '10 5', - boxShadow: '0px 4px 4px rgba(0, 0, 0, 0.25)', - }} - fill="none" - markerStart="url(#markerArrow)" - markerMid="url(#markerSquare)" - markerEnd="url(#markerSquareFilled)" - /> + <> + <div className="presPathLabels">{PresBox.Instance?.orderedPathLabels(collection)}</div> + <svg key="svg" className="presPaths"> + <defs> + <marker id="markerSquare" markerWidth="3" markerHeight="3" refX="1.5" refY="1.5" orient="auto" overflow="visible"> + <rect x="0" y="0" width="3" height="3" stroke="#69a6db" strokeWidth="1" fill="white" fillOpacity="0.8" /> + </marker> + <marker id="markerSquareFilled" markerWidth="3" markerHeight="3" refX="1.5" refY="1.5" orient="auto" overflow="visible"> + <rect x="0" y="0" width="3" height="3" stroke="#69a6db" strokeWidth="1" fill="#69a6db" /> + </marker> + <marker id="markerArrow" markerWidth="3" markerHeight="3" refX="2" refY="4" orient="auto" overflow="visible"> + <path d="M2,2 L2,6 L6,4 L2,2 Z" stroke="#69a6db" strokeLinejoin="round" strokeWidth="1" fill="white" fillOpacity="0.8" /> + </marker> + </defs> + <polyline + points={pathPoints} + style={{ + opacity: 1, + stroke: '#69a6db', + strokeWidth: 5, + strokeDasharray: '10 5', + boxShadow: '0px 4px 4px rgba(0, 0, 0, 0.25)', + }} + fill="none" + markerStart="url(#markerArrow)" + markerMid="url(#markerSquare)" + markerEnd="url(#markerSquareFilled)" + /> + </svg> + </> ); - } - getPaths = (collection: Doc) => this.paths; // needs to be smarter and figure out the paths to draw for this specific collection. or better yet, draw everything in an overlay layer instad of within a collection - + }; // Converts seconds to ms and updates presentation_transition public static SetTransitionTime = (number: String, setter: (timeInMS: number) => void, change?: number) => { let timeInMS = Number(number) * 1000; diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 58a54764d..43b662f0f 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -8,9 +8,8 @@ import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { Cast, NumCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; -import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, returnAll, returnFalse, returnNone, returnTrue, returnZero, smoothScroll, Utils } from '../../../Utils'; +import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, returnAll, returnFalse, returnNone, returnZero, smoothScroll, Utils } from '../../../Utils'; import { DocUtils } from '../../documents/Documents'; -import { DragManager } from '../../util/DragManager'; import { SelectionManager } from '../../util/SelectionManager'; import { SharingManager } from '../../util/SharingManager'; import { SnappingManager } from '../../util/SnappingManager'; @@ -23,10 +22,9 @@ import { LinkDocPreview } from '../nodes/LinkDocPreview'; import { StyleProp } from '../StyleProvider'; import { AnchorMenu } from './AnchorMenu'; import { Annotation } from './Annotation'; +import { GPTPopup } from './GPTPopup/GPTPopup'; import './PDFViewer.scss'; import React = require('react'); -import { GPTPopup } from './GPTPopup/GPTPopup'; -import { InkingStroke } from '../InkingStroke'; const PDFJSViewer = require('pdfjs-dist/web/pdf_viewer'); const pdfjsLib = require('pdfjs-dist'); const _global = (window /* browser */ || global) /* node */ as any; @@ -68,7 +66,6 @@ export class PDFViewer extends React.Component<IViewerProps> { private _styleRule: any; // stylesheet rule for making hyperlinks clickable private _retries = 0; // number of times tried to create the PDF viewer private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void); - private _setBrushViewer: undefined | ((view: { width: number; height: number; panX: number; panY: number }, transTime: number) => void); private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); private _disposers: { [name: string]: IReactionDisposer } = {}; private _viewer: React.RefObject<HTMLDivElement> = React.createRef(); @@ -193,7 +190,6 @@ export class PDFViewer extends React.Component<IViewerProps> { return focusSpeed; }; crop = (region: Doc | undefined, addCrop?: boolean) => this.props.crop(region, addCrop); - brushView = (view: { width: number; height: number; panX: number; panY: number }, transTime: number) => this._setBrushViewer?.(view, transTime); @action setupPdfJsViewer = async () => { @@ -469,7 +465,6 @@ export class PDFViewer extends React.Component<IViewerProps> { }; setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void) => (this._setPreviewCursor = func); - setBrushViewer = (func?: (view: { width: number; height: number; panX: number; panY: number }, transTime: number) => void) => (this._setBrushViewer = func); @action onZoomWheel = (e: React.WheelEvent) => { @@ -515,11 +510,11 @@ export class PDFViewer extends React.Component<IViewerProps> { panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1); panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); transparentFilter = () => [...this.props.childFilters(), Utils.IsTransparentFilter()]; - opaqueFilter = () => [...this.props.childFilters(), Utils.noDragsDocFilter, ...(DragManager.docsBeingDragged.length && this.props.isContentActive() ? [] : [Utils.IsOpaqueFilter()])]; + opaqueFilter = () => [...this.props.childFilters(), Utils.noDragsDocFilter, ...(SnappingManager.GetCanEmbed() && this.props.isContentActive() ? [] : [Utils.IsOpaqueFilter()])]; childStyleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string): any => { if (doc instanceof Doc && property === StyleProp.PointerEvents) { if (this.inlineTextAnnotations.includes(doc) || this.props.isContentActive() === false) return 'none'; - const isInk = doc && StrCast(Doc.Layout(doc).layout).includes(InkingStroke.name) && !props?.LayoutTemplateString; + const isInk = doc.layout_isSvg && !props?.LayoutTemplateString; return isInk ? 'visiblePainted' : 'all'; } return this.props.styleProvider?.(doc, props, property); @@ -546,7 +541,6 @@ export class PDFViewer extends React.Component<IViewerProps> { fieldKey={this.props.fieldKey + '_annotations'} getScrollHeight={this.getScrollHeight} setPreviewCursor={this.setPreviewCursor} - setBrushViewer={this.setBrushViewer} PanelHeight={this.panelHeight} PanelWidth={this.panelWidth} ScreenToLocalTransform={this.overlayTransform} @@ -561,7 +555,7 @@ export class PDFViewer extends React.Component<IViewerProps> { ); @computed get overlayTransparentAnnotations() { const transparentChildren = DocUtils.FilterDocs(DocListCast(this.props.dataDoc[this.props.fieldKey + '_annotations']), this.transparentFilter(), []); - return !transparentChildren.length ? null : this.renderAnnotations(this.transparentFilter, 'multiply', DragManager.docsBeingDragged.length && this.props.isContentActive() ? 'none' : undefined); + return !transparentChildren.length ? null : this.renderAnnotations(this.transparentFilter, 'multiply', SnappingManager.GetCanEmbed() && this.props.isContentActive() ? 'none' : undefined); } @computed get overlayOpaqueAnnotations() { return this.renderAnnotations(this.opaqueFilter, this.allAnnotations.some(anno => anno.mixBlendMode) ? 'hard-light' : undefined); diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 9e3eb28f9..feacdc9c5 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -687,16 +687,12 @@ export namespace Doc { */ export function ComputeContentBounds(docList: Doc[]) { const bounds = docList.reduce( - (bounds, doc) => { - const [sptX, sptY] = [NumCast(doc.x), NumCast(doc.y)]; - const [bptX, bptY] = [sptX + doc[Width](), sptY + doc[Height]()]; - return { - x: Math.min(sptX, bounds.x), - y: Math.min(sptY, bounds.y), - r: Math.max(bptX, bounds.r), - b: Math.max(bptY, bounds.b), - }; - }, + (bounds, doc) => ({ + x: Math.min(NumCast(doc.x), bounds.x), + y: Math.min(NumCast(doc.y), bounds.y), + r: Math.max(NumCast(doc.x) + doc[Width](), bounds.r), + b: Math.max(NumCast(doc.y) + doc[Height](), bounds.b), + }), { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: -Number.MAX_VALUE, b: -Number.MAX_VALUE } ); return bounds; diff --git a/src/fields/List.ts b/src/fields/List.ts index f3fcc87f7..da007e972 100644 --- a/src/fields/List.ts +++ b/src/fields/List.ts @@ -236,7 +236,10 @@ class ListImpl<T extends Field> extends ObjectField { const list = new Proxy<this>(this, { set: setter, get: ListImpl.listGetter, - ownKeys: target => Object.keys(target.__fieldTuples), + ownKeys: target => { + const keys = Object.keys(target.__fieldTuples); + return [...keys, '__realFields']; + }, getOwnPropertyDescriptor: (target, prop) => { if (prop in target[FieldTuples]) { return { |