diff options
Diffstat (limited to 'src')
41 files changed, 1274 insertions, 1048 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 99c04b673..c9424a92e 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -1727,7 +1727,8 @@ export namespace DocUtils { .replace(/\.[a-z0-9]*$/, ''); if (Upload.isImageInformation(result)) { const maxNativeDim = Math.min(Math.max(result.nativeHeight, result.nativeWidth), defaultNativeImageDim); - proto['data-nativeOrientation'] = result.exifData?.data?.image?.Orientation ?? (StrCast((result.exifData?.data as any)?.Orientation).includes('Rotate 90') ? 5 : undefined); + const exifRotation = StrCast((result.exifData?.data as any)?.Orientation).toLowerCase(); + proto['data-nativeOrientation'] = result.exifData?.data?.image?.Orientation ?? (exifRotation.includes('rotate 90') || exifRotation.includes('rotate 270') ? 5 : undefined); proto['data-nativeWidth'] = result.nativeWidth < result.nativeHeight ? (maxNativeDim * result.nativeWidth) / result.nativeHeight : maxNativeDim; proto['data-nativeHeight'] = result.nativeWidth < result.nativeHeight ? maxNativeDim : maxNativeDim / (result.nativeWidth / result.nativeHeight); if (NumCast(proto['data-nativeOrientation']) >= 5) { diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index f7d072d80..99a8c895f 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -235,9 +235,9 @@ export class CurrentUserUtils { const header = Docs.Create.RTFDocument(new RichTextField(JSON.stringify(json), ""), { ...opts, title: "text", layout: "<HTMLdiv transformOrigin='top left' width='{100/scale}%' height='{100/scale}%' transform='scale({scale})'>" + - ` <FormattedTextBox {...props} dontScale='true' fieldKey={'text'} height='calc(100% - ${headerBtnHgt}px - {this._headerHeight||0}px)'/>` + - " <FormattedTextBox {...props} dontScale='true' fieldKey={'header'} dontSelectOnLoad='true' ignoreAutoHeight='true' fontSize='{this._headerFontSize||9}px' height='{(this._headerHeight||0)}px' background='{this._headerColor || MySharedDocs().userColor||`lightGray`}' />" + - ` <HTMLdiv fontSize='${headerBtnHgt - 1}px' height='${headerBtnHgt}px' background='yellow' onClick={‘(this._headerHeight=scale*Math.min(Math.max(0,this._height-30),this._headerHeight===0?50:0)) + (this._autoHeightMargins=this._headerHeight ? this._headerHeight+${headerBtnHgt}:0)’} >Metadata</HTMLdiv>` + + ` <FormattedTextBox {...props} dontScale='true' fieldKey={'text'} height='calc(100% - ${headerBtnHgt}px - {this._headerHeight||0}px)'/>` + + " <FormattedTextBox {...props} dontScale='true' fieldKey={'header'} dontSelectOnLoad='true' ignoreAutoHeight='true' fontSize='{this._headerFontSize||9}px' height='{(this._headerHeight||0)}px' backgroundColor='{this._headerColor || MySharedDocs().userColor||`lightGray`}' />" + + ` <HTMLdiv fontSize='${headerBtnHgt - 1}px' height='${headerBtnHgt}px' backgroundColor='yellow' onClick={‘(this._headerHeight=scale*Math.min(Math.max(0,this._height-30),this._headerHeight===0?50:0)) + (this._autoHeightMargins=this._headerHeight ? this._headerHeight+${headerBtnHgt}:0)’} >Metadata</HTMLdiv>` + "</HTMLdiv>" }, "header"); @@ -267,7 +267,7 @@ export class CurrentUserUtils { {key: "Script", creator: opts => Docs.Create.ScriptingDocument(null, opts), opts: { _width: 200, _height: 250, }}, // {key: "DataViz", creator: opts => Docs.Create.DataVizDocument(opts), opts: { _width: 300, _height: 300 }}, {key: "Header", creator: headerTemplate, opts: { _width: 300, _height: 70, _headerPointerEvents: "all", _headerHeight: 12, _headerFontSize: 9, _autoHeight: true,}}, - {key: "Presentation",creator: Docs.Create.PresDocument, opts: { _width: 400, _height: 500, _viewType: CollectionViewType.Stacking, targetDropAction: "alias" as any, _chromeHidden: true, boxShadow: "0 0" }}, + {key: "Presentation",creator: Docs.Create.PresDocument, opts: { _width: 400, _height: 500, _viewType: CollectionViewType.Stacking, targetDropAction: "alias" as any, treeViewHideTitle: true, _chromeHidden: true, boxShadow: "0 0" }}, {key: "Tab", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 500, _height: 800, _backgroundGridShow: true, }}, {key: "Slide", creator: opts => Docs.Create.TreeDocument([], opts), opts: { _width: 300, _height: 200, _viewType: CollectionViewType.Tree, treeViewHasOverlay: true, _fontSize: "20px", _autoHeight: true, @@ -633,7 +633,7 @@ export class CurrentUserUtils { { title: "Left", toolTip: "Left align", btnType: ButtonType.ToggleButton, icon: "align-left", scripts: {onClick:'{ return setAlignment("left", _readOnly_);}' }}, { title: "Center", toolTip: "Center align", btnType: ButtonType.ToggleButton, icon: "align-center", scripts: {onClick:'{ return setAlignment("center", _readOnly_);}'} }, { title: "Right", toolTip: "Right align", btnType: ButtonType.ToggleButton, icon: "align-right", scripts: {onClick:'{ return setAlignment("right", _readOnly_);}'} }, - { title: "NoLink", toolTip: "Auto Link", btnType: ButtonType.ToggleButton, icon: "link", scripts: {onClick:'{ return toggleNoAutoLinkAnchor(_readOnly_);}'}}, + { title: "NoLink", toolTip: "Auto Link", btnType: ButtonType.ToggleButton, icon: "link", scripts: {onClick:'{ return toggleNoAutoLinkAnchor(_readOnly_);}'}, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}}, { title: "Dictate",toolTip: "Dictate", btnType: ButtonType.ToggleButton, icon: "microphone", scripts: {onClick:'{ return toggleDictation(_readOnly_);}'}}, ]; } @@ -675,8 +675,9 @@ export class CurrentUserUtils { CollectionViewType.Carousel3D, CollectionViewType.Linear, CollectionViewType.Map, 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, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "tab")'}, width: 20, scripts: { onClick: 'pinWithView(_readOnly_)'}}, { title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}}, - { title: "Num", icon: "", toolTip: "Frame Number", btnType: ButtonType.TextButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()', buttonText: 'selectedDocs()?.lastElement().currentFrame.toString()'}, width: 20, scripts: {}}, + { title: "Num", icon: "", toolTip: "Frame Number", btnType: ButtonType.TextButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()', buttonText: 'selectedDocs()?.lastElement()?.currentFrame.toString()'}, width: 20, scripts: {}}, { title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20, scripts: { onClick: 'nextKeyFrame(_readOnly_)'}}, { title: "Fill", icon: "fill-drip", toolTip: "Background Fill Color",btnType: ButtonType.ColorButton, funcs: {hidden: '!SelectionManager_selectedDocType()'}, ignoreClick: true, width: 20, scripts: { script: 'return setBackgroundColor(value, _readOnly_)'}}, // Only when a document is selected { title: "Header", icon: "heading", toolTip: "Header Color", btnType: ButtonType.ColorButton, funcs: {hidden: '!SelectionManager_selectedDocType()'}, ignoreClick: true, scripts: { script: 'return setHeaderColor(value, _readOnly_)'}}, diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index d781a87ab..6386c87a0 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -457,8 +457,7 @@ export namespace DragManager { document.removeEventListener('pointerup', upHandler, true); SnappingManager.SetIsDragging(false); SnappingManager.clearSnapLines(); - const ended = batch.end(); - if (undo && ended) UndoManager.Undo(); + if (batch.end() && undo) UndoManager.Undo(); docsBeingDragged.length = 0; }); var startWindowDragTimer: any; diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts index 256ab5c44..7c209d1e0 100644 --- a/src/client/util/DropConverter.ts +++ b/src/client/util/DropConverter.ts @@ -1,37 +1,41 @@ -import { DragManager } from "./DragManager"; -import { Doc, DocListCast, Opt } from "../../fields/Doc"; -import { DocumentType } from "../documents/DocumentTypes"; -import { ObjectField } from "../../fields/ObjectField"; -import { StrCast, Cast } from "../../fields/Types"; -import { Docs } from "../documents/Documents"; -import { ScriptField, ComputedField } from "../../fields/ScriptField"; -import { RichTextField } from "../../fields/RichTextField"; -import { ImageField } from "../../fields/URLField"; -import { ScriptingGlobals } from "./ScriptingGlobals"; -import { listSpec } from "../../fields/Schema"; +import { DragManager } from './DragManager'; +import { Doc, DocListCast, Opt } from '../../fields/Doc'; +import { DocumentType } from '../documents/DocumentTypes'; +import { ObjectField } from '../../fields/ObjectField'; +import { StrCast, Cast } from '../../fields/Types'; +import { Docs } from '../documents/Documents'; +import { ScriptField, ComputedField } from '../../fields/ScriptField'; +import { RichTextField } from '../../fields/RichTextField'; +import { ImageField } from '../../fields/URLField'; +import { ScriptingGlobals } from './ScriptingGlobals'; +import { listSpec } from '../../fields/Schema'; +import { ButtonType } from '../views/nodes/button/FontIconBox'; -export function MakeTemplate(doc: Doc, first: boolean = true, rename: Opt<string> = undefined, templateField: string = "") { +export function MakeTemplate(doc: Doc, first: boolean = true, rename: Opt<string> = undefined, templateField: string = '') { if (templateField) Doc.GetProto(doc).title = templateField; /// the title determines which field is being templated doc.isTemplateDoc = makeTemplate(doc, first, rename); return doc; } -// +// // converts 'doc' into a template that can be used to render other documents. // the title of doc is used to determine which field is being templated, so -// passing a value for 'rename' allows the doc to be given a meangingful name +// passing a value for 'rename' allows the doc to be given a meangingful name // after it has been converted to function makeTemplate(doc: Doc, first: boolean = true, rename: Opt<string> = undefined): boolean { const layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc; - if (layoutDoc.layout instanceof Doc) { // its already a template + if (layoutDoc.layout instanceof Doc) { + // its already a template return true; } const layout = StrCast(layoutDoc.layout).match(/fieldKey={'[^']*'}/)![0]; - const fieldKey = layout.replace("fieldKey={'", "").replace(/'}$/, ""); + const fieldKey = layout.replace("fieldKey={'", '').replace(/'}$/, ''); const docs = DocListCast(layoutDoc[fieldKey]); let any = false; docs.forEach(d => { - if (!StrCast(d.title).startsWith("-")) { - const params = StrCast(d.title).match(/\(([a-zA-Z0-9._\-]*)\)/)?.[1].replace("()", ""); + if (!StrCast(d.title).startsWith('-')) { + const params = StrCast(d.title) + .match(/\(([a-zA-Z0-9._\-]*)\)/)?.[1] + .replace('()', ''); if (params) { any = makeTemplate(d, false) || any; d.PARAMS = params; @@ -43,12 +47,13 @@ function makeTemplate(doc: Doc, first: boolean = true, rename: Opt<string> = und } }); if (first) { - if (!docs.length) { // bcz: feels hacky : if the root level document has items, it's not a field template + if (!docs.length) { + // bcz: feels hacky : if the root level document has items, it's not a field template any = Doc.MakeMetadataFieldTemplate(doc, Doc.GetProto(layoutDoc)) || any; } } if (layoutDoc[fieldKey] instanceof RichTextField || layoutDoc[fieldKey] instanceof ImageField) { - if (!StrCast(layoutDoc.title).startsWith("-")) { + if (!StrCast(layoutDoc.title).startsWith('-')) { any = Doc.MakeMetadataFieldTemplate(layoutDoc, Doc.GetProto(layoutDoc)); } } @@ -59,23 +64,29 @@ 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')) { if (data.removeDropProperties || dbox.removeDropProperties) { //dbox = Doc.MakeAlias(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.MakeAlias(dbox); - const dragProps = Cast(dbox.removeDropProperties, listSpec("string"), []); + const dragProps = Cast(dbox.removeDropProperties, listSpec('string'), []); const remProps = (data.removeDropProperties || []).concat(Array.from(dragProps)); - remProps.map(prop => dbox[prop] = undefined); + remProps.map(prop => (dbox[prop] = undefined)); } } else if (!doc.onDragStart && !doc.isButtonBar) { - const layoutDoc = doc;// doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc; + const layoutDoc = doc; // doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc; if (layoutDoc.type !== DocumentType.FONTICON) { !layoutDoc.isTemplateDoc && makeTemplate(layoutDoc); } layoutDoc.isTemplateDoc = true; dbox = Docs.Create.FontIconDocument({ - _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, - backgroundColor: StrCast(doc.backgroundColor), title: StrCast(layoutDoc.title), icon: layoutDoc.isTemplateDoc ? "font" : "bolt" + _nativeWidth: 100, + _nativeHeight: 100, + _width: 100, + _height: 100, + backgroundColor: StrCast(doc.backgroundColor), + title: StrCast(layoutDoc.title), + btnType: ButtonType.ClickButton, + icon: layoutDoc.isTemplateDoc ? 'font' : 'bolt', }); dbox.dragFactory = layoutDoc; dbox.removeDropProperties = doc.removeDropProperties instanceof ObjectField ? ObjectField.MakeCopy(doc.removeDropProperties) : undefined; @@ -86,5 +97,10 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) { data.droppedDocuments[i] = dbox; }); } -ScriptingGlobals.add(function convertToButtons(dragData: any) { convertDropDataToButtons(dragData as DragManager.DocumentDragData); }, - "converts the dropped data to buttons", "(dragData: any)");
\ No newline at end of file +ScriptingGlobals.add( + function convertToButtons(dragData: any) { + convertDropDataToButtons(dragData as DragManager.DocumentDragData); + }, + 'converts the dropped data to buttons', + '(dragData: any)' +); diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx index 289c5bc51..4af51b9a0 100644 --- a/src/client/util/InteractionUtils.tsx +++ b/src/client/util/InteractionUtils.tsx @@ -1,12 +1,12 @@ -import React = require("react"); -import { Utils } from "../../Utils"; -import "./InteractionUtils.scss"; +import React = require('react'); +import { Utils } from '../../Utils'; +import './InteractionUtils.scss'; export namespace InteractionUtils { - export const MOUSETYPE = "mouse"; - export const TOUCHTYPE = "touch"; - export const PENTYPE = "pen"; - export const ERASERTYPE = "eraser"; + export const MOUSETYPE = 'mouse'; + export const TOUCHTYPE = 'touch'; + export const PENTYPE = 'pen'; + export const ERASERTYPE = 'eraser'; const POINTER_PEN_BUTTON = -1; const REACT_POINTER_PEN_BUTTON = 0; @@ -19,24 +19,23 @@ export namespace InteractionUtils { readonly touches: T extends React.TouchEvent ? React.Touch[] : Touch[], readonly changedTouches: T extends React.TouchEvent ? React.Touch[] : Touch[], readonly touchEvent: T extends React.TouchEvent ? React.TouchEvent : TouchEvent - ) { } + ) {} } - export interface MultiTouchEventDisposer { (): void; } + export interface MultiTouchEventDisposer { + (): void; + } /** * * @param element - element to turn into a touch target * @param startFunc - event handler, typically Touchable.onTouchStart (classes that inherit touchable can pass in this.onTouchStart) */ - export function MakeMultiTouchTarget( - element: HTMLElement, - startFunc: (e: Event, me: MultiTouchEvent<React.TouchEvent>) => void - ): MultiTouchEventDisposer { + export function MakeMultiTouchTarget(element: HTMLElement, startFunc: (e: Event, me: MultiTouchEvent<React.TouchEvent>) => void): MultiTouchEventDisposer { const onMultiTouchStartHandler = (e: Event) => startFunc(e, (e as CustomEvent<MultiTouchEvent<React.TouchEvent>>).detail); // const onMultiTouchMoveHandler = moveFunc ? (e: Event) => moveFunc(e, (e as CustomEvent<MultiTouchEvent<TouchEvent>>).detail) : undefined; // const onMultiTouchEndHandler = endFunc ? (e: Event) => endFunc(e, (e as CustomEvent<MultiTouchEvent<TouchEvent>>).detail) : undefined; - element.addEventListener("dashOnTouchStart", onMultiTouchStartHandler); + element.addEventListener('dashOnTouchStart', onMultiTouchStartHandler); // if (onMultiTouchMoveHandler) { // element.addEventListener("dashOnTouchMove", onMultiTouchMoveHandler); // } @@ -44,7 +43,7 @@ export namespace InteractionUtils { // element.addEventListener("dashOnTouchEnd", onMultiTouchEndHandler); // } return () => { - element.removeEventListener("dashOnTouchStart", onMultiTouchStartHandler); + element.removeEventListener('dashOnTouchStart', onMultiTouchStartHandler); // if (onMultiTouchMoveHandler) { // element.removeEventListener("dashOnTouchMove", onMultiTouchMoveHandler); // } @@ -59,14 +58,11 @@ export namespace InteractionUtils { * @param element - element to add events to * @param func - function to add to the event */ - export function MakeHoldTouchTarget( - element: HTMLElement, - func: (e: Event, me: MultiTouchEvent<React.TouchEvent>) => void - ): MultiTouchEventDisposer { + export function MakeHoldTouchTarget(element: HTMLElement, func: (e: Event, me: MultiTouchEvent<React.TouchEvent>) => void): MultiTouchEventDisposer { const handler = (e: Event) => func(e, (e as CustomEvent<MultiTouchEvent<React.TouchEvent>>).detail); - element.addEventListener("dashOnTouchHoldStart", handler); + element.addEventListener('dashOnTouchHoldStart', handler); return () => { - element.removeEventListener("dashOnTouchHoldStart", handler); + element.removeEventListener('dashOnTouchHoldStart', handler); }; } @@ -89,71 +85,108 @@ export namespace InteractionUtils { return myTouches; } - export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number, - color: string, width: number, strokeWidth: number, lineJoin: string, lineCap: string, bezier: string, fill: string, arrowStart: string, arrowEnd: string, - markerScale: number, dash: string | undefined, scalex: number, scaley: number, shape: string, pevents: string, opacity: number, nodefs: boolean, - downHdlr?: ((e: React.PointerEvent) => void)) { + export function CreatePolyline( + points: { X: number; Y: number }[], + left: number, + top: number, + color: string, + width: number, + strokeWidth: number, + lineJoin: string, + lineCap: string, + bezier: string, + fill: string, + arrowStart: string, + arrowEnd: string, + markerScale: number, + dash: string | undefined, + scalex: number, + scaley: number, + shape: string, + pevents: string, + opacity: number, + nodefs: boolean, + downHdlr?: (e: React.PointerEvent) => void + ) { const pts = shape ? makePolygon(shape, points) : points; if (isNaN(scalex)) scalex = 1; if (isNaN(scaley)) scaley = 1; - const toScr = (p: { X: number, Y: number }) => ` ${!p ? 0 : (p.X - left - width / 2) * scalex + width / 2}, ${!p ? 0 : (p.Y - top - width / 2) * scaley + width / 2} `; - const strpts = bezier ? - pts.reduce((acc: string, pt, i) => acc + (i % 4 !== 0 ? "" : (i === 0 ? "M" + toScr(pt) : "") + "C" + toScr(pts[i + 1]) + toScr(pts[i + 2]) + toScr(pts[i + 3])), "") : - pts.reduce((acc: string, pt) => acc + `${toScr(pt)} `, ""); + const toScr = (p: { X: number; Y: number }) => ` ${!p ? 0 : (p.X - left - width / 2) * scalex + width / 2}, ${!p ? 0 : (p.Y - top - width / 2) * scaley + width / 2} `; + const strpts = bezier + ? pts.reduce((acc: string, pt, i) => acc + (i % 4 !== 0 ? '' : (i === 0 ? 'M' + toScr(pt) : '') + 'C' + toScr(pts[i + 1]) + toScr(pts[i + 2]) + toScr(pts[i + 3])), '') + : pts.reduce((acc: string, pt) => acc + `${toScr(pt)} `, ''); const dashArray = dash && Number(dash) ? String(Number(width) * Number(dash)) : undefined; const defGuid = Utils.GenerateGuid(); - const Tag = (bezier ? "path" : "polyline") as keyof JSX.IntrinsicElements; + const Tag = (bezier ? 'path' : 'polyline') as keyof JSX.IntrinsicElements; const markerStrokeWidth = strokeWidth / 2; - const arrowWidthFactor = 3 * (markerScale || 0.5);// used to be 1.5 + const arrowWidthFactor = 3 * (markerScale || 0.5); // used to be 1.5 const arrowLengthFactor = 5 * (markerScale || 0.5); const arrowNotchFactor = 2 * (markerScale || 0.5); - return (<svg fill={color} onPointerDown={downHdlr}> {/* setting the svg fill sets the arrowStart fill */} - {nodefs ? (null) : <defs> - {arrowStart !== "dot" && arrowEnd !== "dot" ? (null) : - <marker id={`dot${defGuid}`} orient="auto" markerUnits="userSpaceOnUse" refX={0} refY="0" overflow="visible"> - <circle r={strokeWidth * arrowWidthFactor} fill="context-stroke" /> - </marker>} - {arrowStart !== "arrow" ? (null) : - <marker id={`arrowStart${defGuid}`} markerUnits="userSpaceOnUse" orient="auto" overflow="visible" refX={markerStrokeWidth * (arrowLengthFactor - arrowNotchFactor)} refY={0} markerWidth="10" markerHeight="7"> - <polygon style={{ stroke: color }} strokeLinejoin={lineJoin as any} strokeWidth={markerStrokeWidth * 2 / 3} - points={`${arrowLengthFactor * markerStrokeWidth} ${-markerStrokeWidth * arrowWidthFactor}, ${markerStrokeWidth * (arrowLengthFactor - arrowNotchFactor)} 0, ${arrowLengthFactor * markerStrokeWidth} ${markerStrokeWidth * arrowWidthFactor}, 0 0`} /> - </marker>} - {arrowEnd !== "arrow" ? (null) : - <marker id={`arrowEnd${defGuid}`} markerUnits="userSpaceOnUse" orient="auto" overflow="visible" refX={markerStrokeWidth * arrowNotchFactor} refY={0} markerWidth="10" markerHeight="7"> - <polygon style={{ stroke: color }} strokeLinejoin={lineJoin as any} strokeWidth={markerStrokeWidth * 2 / 3} - points={`0 ${-markerStrokeWidth * arrowWidthFactor}, ${markerStrokeWidth * arrowNotchFactor} 0, 0 ${markerStrokeWidth * arrowWidthFactor}, ${arrowLengthFactor * markerStrokeWidth} 0`} /> - </marker>} - </defs>} - - <Tag - d={bezier ? strpts : undefined} - points={bezier ? undefined : strpts} - style={{ - // filter: drawHalo ? "url(#inkSelectionHalo)" : undefined, - fill: fill && fill !== "transparent" ? fill : "none", - opacity: 1.0, - // opacity: strokeWidth !== width ? 0.5 : undefined, - pointerEvents: pevents as any, - stroke: color ?? "rgb(0, 0, 0)", - strokeWidth: strokeWidth, - strokeLinecap: lineCap as any, - strokeDasharray: dashArray - }} - markerStart={`url(#${arrowStart === "dot" ? arrowStart + defGuid : arrowStart + "Start" + defGuid})`} - markerEnd={`url(#${arrowEnd === "dot" ? arrowEnd + defGuid : arrowEnd + "End" + defGuid})`} - /> - - </svg>); + return ( + <svg fill={color} style={{ transition: 'inherit' }} onPointerDown={downHdlr}> + {' '} + {/* setting the svg fill sets the arrowStart fill */} + {nodefs ? null : ( + <defs> + {arrowStart !== 'dot' && arrowEnd !== 'dot' ? null : ( + <marker id={`dot${defGuid}`} orient="auto" markerUnits="userSpaceOnUse" refX={0} refY="0" overflow="visible"> + <circle r={strokeWidth * arrowWidthFactor} fill="context-stroke" /> + </marker> + )} + {arrowStart !== 'arrow' ? null : ( + <marker id={`arrowStart${defGuid}`} markerUnits="userSpaceOnUse" orient="auto" overflow="visible" refX={markerStrokeWidth * (arrowLengthFactor - arrowNotchFactor)} refY={0} markerWidth="10" markerHeight="7"> + <polygon + style={{ stroke: color }} + strokeLinejoin={lineJoin as any} + strokeWidth={(markerStrokeWidth * 2) / 3} + points={`${arrowLengthFactor * markerStrokeWidth} ${-markerStrokeWidth * arrowWidthFactor}, ${markerStrokeWidth * (arrowLengthFactor - arrowNotchFactor)} 0, ${arrowLengthFactor * markerStrokeWidth} ${ + markerStrokeWidth * arrowWidthFactor + }, 0 0`} + /> + </marker> + )} + {arrowEnd !== 'arrow' ? null : ( + <marker id={`arrowEnd${defGuid}`} markerUnits="userSpaceOnUse" orient="auto" overflow="visible" refX={markerStrokeWidth * arrowNotchFactor} refY={0} markerWidth="10" markerHeight="7"> + <polygon + style={{ stroke: color }} + strokeLinejoin={lineJoin as any} + strokeWidth={(markerStrokeWidth * 2) / 3} + points={`0 ${-markerStrokeWidth * arrowWidthFactor}, ${markerStrokeWidth * arrowNotchFactor} 0, 0 ${markerStrokeWidth * arrowWidthFactor}, ${arrowLengthFactor * markerStrokeWidth} 0`} + /> + </marker> + )} + </defs> + )} + <Tag + d={bezier ? strpts : undefined} + points={bezier ? undefined : strpts} + style={{ + // filter: drawHalo ? "url(#inkSelectionHalo)" : undefined, + fill: fill && fill !== 'transparent' ? fill : 'none', + opacity: 1.0, + // opacity: strokeWidth !== width ? 0.5 : undefined, + pointerEvents: pevents as any, + stroke: color ?? 'rgb(0, 0, 0)', + strokeWidth: strokeWidth, + strokeLinecap: lineCap as any, + strokeDasharray: dashArray, + transition: 'inherit', + }} + markerStart={`url(#${arrowStart === 'dot' ? arrowStart + defGuid : arrowStart + 'Start' + defGuid})`} + markerEnd={`url(#${arrowEnd === 'dot' ? arrowEnd + defGuid : arrowEnd + 'End' + defGuid})`} + /> + </svg> + ); } - export function makePolygon(shape: string, points: { X: number, Y: number }[]) { + export function makePolygon(shape: string, points: { X: number; Y: number }[]) { if (points.length > 1 && points[points.length - 1].X === points[0].X && points[points.length - 1].Y + 1 === points[0].Y) { //pointer is up (first and last points are the same) - if (shape === "arrow" || shape === "line" || shape === "circle") { + if (shape === 'arrow' || shape === 'line' || shape === 'circle') { //if arrow or line, the two end points should be the starting and the ending point var left = points[0].X; var top = points[0].Y; @@ -175,7 +208,7 @@ export namespace InteractionUtils { left = points[0].X; bottom = points[points.length - 1].Y; top = points[0].Y; - if (shape !== "arrow" && shape !== "line" && shape !== "circle") { + if (shape !== 'arrow' && shape !== 'line' && shape !== 'circle') { //switch left/right and top/bottom if needed if (left > right) { const temp = right; @@ -191,14 +224,13 @@ export namespace InteractionUtils { } points = []; switch (shape) { - case "rectangle": + case 'rectangle': points.push({ X: left, Y: top }); points.push({ X: right, Y: top }); points.push({ X: right, Y: bottom }); points.push({ X: left, Y: bottom }); points.push({ X: left, Y: top }); - return points; - case "triangle": + case 'triangle': // points.push({ X: left, Y: bottom }); // points.push({ X: right, Y: bottom }); // points.push({ X: (right + left) / 2, Y: top }); @@ -219,62 +251,39 @@ export namespace InteractionUtils { points.push({ X: left, Y: bottom }); points.push({ X: left, Y: bottom }); - - - return points; - case "circle": + case 'circle': const centerX = (Math.max(left, right) + Math.min(left, right)) / 2; const centerY = (Math.max(top, bottom) + Math.min(top, bottom)) / 2; const radius = Math.max(centerX - Math.min(left, right), centerY - Math.min(top, bottom)); if (centerX - Math.min(left, right) < centerY - Math.min(top, bottom)) { for (var y = Math.min(top, bottom); y < Math.max(top, bottom); y++) { - const x = Math.sqrt(Math.pow(radius, 2) - (Math.pow((y - centerY), 2))) + centerX; + const x = Math.sqrt(Math.pow(radius, 2) - Math.pow(y - centerY, 2)) + centerX; points.push({ X: x, Y: y }); } for (var y = Math.max(top, bottom); y > Math.min(top, bottom); y--) { - const x = Math.sqrt(Math.pow(radius, 2) - (Math.pow((y - centerY), 2))) + centerX; + const x = Math.sqrt(Math.pow(radius, 2) - Math.pow(y - centerY, 2)) + centerX; const newX = centerX - (x - centerX); points.push({ X: newX, Y: y }); } - points.push({ X: Math.sqrt(Math.pow(radius, 2) - (Math.pow((Math.min(top, bottom) - centerY), 2))) + centerX, Y: Math.min(top, bottom) }); + points.push({ X: Math.sqrt(Math.pow(radius, 2) - Math.pow(Math.min(top, bottom) - centerY, 2)) + centerX, Y: Math.min(top, bottom) }); } else { for (var x = Math.min(left, right); x < Math.max(left, right); x++) { - const y = Math.sqrt(Math.pow(radius, 2) - (Math.pow((x - centerX), 2))) + centerY; + const y = Math.sqrt(Math.pow(radius, 2) - Math.pow(x - centerX, 2)) + centerY; points.push({ X: x, Y: y }); } for (var x = Math.max(left, right); x > Math.min(left, right); x--) { - const y = Math.sqrt(Math.pow(radius, 2) - (Math.pow((x - centerX), 2))) + centerY; + const y = Math.sqrt(Math.pow(radius, 2) - Math.pow(x - centerX, 2)) + centerY; const newY = centerY - (y - centerY); points.push({ X: x, Y: newY }); } - points.push({ X: Math.min(left, right), Y: Math.sqrt(Math.pow(radius, 2) - (Math.pow((Math.min(left, right) - centerX), 2))) + centerY }); + points.push({ X: Math.min(left, right), Y: Math.sqrt(Math.pow(radius, 2) - Math.pow(Math.min(left, right) - centerX, 2)) + centerY }); } - return points; - // case "arrow": - // const x1 = left; - // const y1 = top; - // const x2 = right; - // const y2 = bottom; - // const L1 = Math.sqrt(Math.pow(Math.abs(x1 - x2), 2) + (Math.pow(Math.abs(y1 - y2), 2))); - // const L2 = L1 / 5; - // const angle = 0.785398; - // const x3 = x2 + (L2 / L1) * ((x1 - x2) * Math.cos(angle) + (y1 - y2) * Math.sin(angle)); - // const y3 = y2 + (L2 / L1) * ((y1 - y2) * Math.cos(angle) - (x1 - x2) * Math.sin(angle)); - // const x4 = x2 + (L2 / L1) * ((x1 - x2) * Math.cos(angle) - (y1 - y2) * Math.sin(angle)); - // const y4 = y2 + (L2 / L1) * ((y1 - y2) * Math.cos(angle) + (x1 - x2) * Math.sin(angle)); - // points.push({ X: x1, Y: y1 }); - // points.push({ X: x2, Y: y2 }); - // points.push({ X: x3, Y: y3 }); - // points.push({ X: x4, Y: y4 }); - // points.push({ X: x2, Y: y2 }); - // return points; - case "line": + case 'line': points.push({ X: left, Y: top }); points.push({ X: right, Y: bottom }); return points; - default: - return points; } + return points; } /** * Returns whether or not the pointer event passed in is of the type passed in @@ -284,11 +293,14 @@ export namespace InteractionUtils { export function IsType(e: PointerEvent | React.PointerEvent, type: string): boolean { switch (type) { // pen and eraser are both pointer type 'pen', but pen is button 0 and eraser is button 5. -syip2 - case PENTYPE: return e.pointerType === PENTYPE && (e.button === -1 || e.button === 0); - case ERASERTYPE: return e.pointerType === PENTYPE && e.button === (e instanceof PointerEvent ? ERASER_BUTTON : ERASER_BUTTON); + case PENTYPE: + return e.pointerType === PENTYPE && (e.button === -1 || e.button === 0); + case ERASERTYPE: + return e.pointerType === PENTYPE && e.button === (e instanceof PointerEvent ? ERASER_BUTTON : ERASER_BUTTON); case TOUCHTYPE: return e.pointerType === TOUCHTYPE; - default: return e.pointerType === type; + default: + return e.pointerType === type; } } @@ -305,7 +317,7 @@ export namespace InteractionUtils { * Returns the centroid of an n-arbitrary long list of points (takes the average the x and y components of each point) * @param pts - n-arbitrary long list of points */ - export function CenterPoint(pts: React.Touch[]): { X: number, Y: number } { + export function CenterPoint(pts: React.Touch[]): { X: number; Y: number } { const centerX = pts.map(pt => pt.clientX).reduce((a, b) => a + b, 0) / pts.length; const centerY = pts.map(pt => pt.clientY).reduce((a, b) => a + b, 0) / pts.length; return { X: centerX, Y: centerY }; @@ -324,9 +336,9 @@ export namespace InteractionUtils { const newDist = TwoPointEuclidist(pt1, pt2); /** if they have the same sign, then we are either pinching in or out. - * threshold it by 10 (it has to be pinching by at least threshold to be a valid pinch) - * so that it can still pan without freaking out - */ + * threshold it by 10 (it has to be pinching by at least threshold to be a valid pinch) + * so that it can still pan without freaking out + */ if (Math.sign(oldDist) === Math.sign(newDist) && Math.abs(oldDist - newDist) > threshold) { return Math.sign(oldDist - newDist); } @@ -372,8 +384,6 @@ export namespace InteractionUtils { // These might not be very useful anymore, but I'll leave them here for now -syip2 { - - /** * Returns the type of Touch Interaction from a list of points. * Also returns any data that is associated with a Touch Interaction diff --git a/src/client/util/ReportManager.scss b/src/client/util/ReportManager.scss new file mode 100644 index 000000000..5a2f2fcad --- /dev/null +++ b/src/client/util/ReportManager.scss @@ -0,0 +1,88 @@ +@import '../views/global/globalCssVariables'; + +.issue-list-wrapper { + position: relative; + min-width: 250px; + background-color: $light-blue; + overflow-y: scroll; +} + +.issue-list { + display: flex; + align-items: center; + justify-content: space-between; + padding: 5px; + margin: 5px; + border-radius: 5px; + border: 1px solid grey; + background-color: lightgoldenrodyellow; +} + +// issue should pop up when the user hover over the issue +.issue-list:hover { + box-shadow: 2px; + cursor: pointer; + border: 3px solid #252b33; +} + +.issue-content { + background-color: white; + padding: 10px; + flex: 1 1 auto; + overflow-y: scroll; +} + +.issue-title { + font-size: 20px; + font-weight: 600; + color: black; +} + +.issue-body { + padding: 0 10px; + width: 100%; + text-align: left; +} + +.issue-body > * { + margin-top: 5px; +} + +.issue-body img, +.issue-body video { + display: block; + max-width: 100%; +} + +.report-issue-fab { + position: fixed; + bottom: 20px; + right: 20px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; +} + +.loading-center { + margin: auto 0; +} + +.settings-content label { + margin-top: 10px; +} + +.report-disclaimer { + font-size: 8px; + color: grey; + padding-right: 50px; + font-style: italic; + text-align: left; +} + +.flex-select { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; +} diff --git a/src/client/util/ReportManager.tsx b/src/client/util/ReportManager.tsx new file mode 100644 index 000000000..55c5ca87f --- /dev/null +++ b/src/client/util/ReportManager.tsx @@ -0,0 +1,282 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, computed, observable, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { ColorState, SketchPicker } from 'react-color'; +import { Doc } from '../../fields/Doc'; +import { Id } from '../../fields/FieldSymbols'; +import { BoolCast, Cast, StrCast } from '../../fields/Types'; +import { addStyleSheet, addStyleSheetRule, Utils } from '../../Utils'; +import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager'; +import { DocServer } from '../DocServer'; +import { Networking } from '../Network'; +import { MainViewModal } from '../views/MainViewModal'; +import { FontIconBox } from '../views/nodes/button/FontIconBox'; +import { DragManager } from './DragManager'; +import { GroupManager } from './GroupManager'; +import './SettingsManager.scss'; +import './ReportManager.scss'; +import { undoBatch } from './UndoManager'; +import { Octokit } from "@octokit/core"; +import { CheckBox } from '../views/search/CheckBox'; +import ReactLoading from 'react-loading'; +import ReactMarkdown from 'react-markdown'; +import rehypeRaw from 'rehype-raw'; +import remarkGfm from 'remark-gfm'; +const higflyout = require('@hig/flyout'); +export const { anchorPoints } = higflyout; +export const Flyout = higflyout.default; + +@observer +export class ReportManager extends React.Component<{}> { + public static Instance: ReportManager; + @observable private isOpen = false; + + private octokit: Octokit; + + @observable public issues: any[] = []; + @action setIssues = action((issues: any[]) => { this.issues = issues; }); + + // undefined is the default - null is if the user is making an issue + @observable public selectedIssue: any = undefined; + @action setSelectedIssue = action((issue: any) => { this.selectedIssue = issue; }); + + // only get the open issues + @observable public shownIssues = this.issues.filter(issue => issue.state === 'open'); + + public updateIssueSearch = action((query: string = '') => { + if (query === '') { + this.shownIssues = this.issues.filter(issue => issue.state === 'open'); + return; + } + this.shownIssues = this.issues.filter(issue => issue.title.toLowerCase().includes(query.toLowerCase())); + }); + + constructor(props: {}) { + super(props); + ReportManager.Instance = this; + + this.octokit = new Octokit({ + auth: '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' + }); + } + + public close = action(() => (this.isOpen = false)); + public open = action(() => { + if (this.issues.length === 0) { + // load in the issues if not already loaded + this.getAllIssues() + .then(issues => { + this.setIssues(issues); + this.updateIssueSearch(); + }) + .catch(err => console.log(err)); + } + (this.isOpen = true) + }); + + @observable private bugTitle = ''; + @action setBugTitle = action((title: string) => { this.bugTitle = title; }); + @observable private bugDescription = ''; + @action setBugDescription = action((description: string) => { this.bugDescription = description; }); + @observable private bugType = ''; + @action setBugType = action((type: string) => { this.bugType = type; }); + @observable private bugPriority = ''; + @action setBugPriority = action((priortiy: string) => { this.bugPriority = priortiy; }); + + // private toGithub = false; + // will always be set to true - no alterntive option yet + private toGithub = true; + + private formatTitle = (title: string, userEmail: string) => `${title} - ${userEmail.replace('@brown.edu', '')}`; + + public async getAllIssues() : Promise<any[]> { + const res = await this.octokit.request('GET /repos/{owner}/{repo}/issues', { + owner: 'brown-dash', + repo: 'Dash-Web', + }); + + // 200 status means success + if (res.status === 200) { + return res.data; + } else { + throw new Error('Error getting issues'); + } + } + + public async reportIssue() { + if (this.bugTitle === '' || this.bugDescription === '' + || this.bugType === '' || this.bugPriority === '') { + alert('Please fill out all required fields to report an issue.'); + return; + } + + + if (this.toGithub) { + + const req = await this.octokit.request('POST /repos/{owner}/{repo}/issues', { + owner: 'brown-dash', + repo: 'Dash-Web', + title: this.formatTitle(this.bugTitle, Doc.CurrentUserEmail), + body: `${this.bugDescription} \n\nfiles:\n${(this.fileLinks ?? []).join('\n')}`, + labels: [ + 'from-dash-app', + this.bugType, + this.bugPriority + ] + }); + + // 201 status means success + if (req.status !== 201) { + alert('Error creating issue on github.'); + // on error, don't close the modal + return; + } + } + else { + // if not going to github issues, not sure what to do yet... + } + + // if we're down here, then we're good to go. reset the fields. + this.setBugTitle(''); + this.setBugDescription(''); + this.toGithub = false; + this.setFileLinks([]); + this.setBugType(''); + this.setBugPriority(''); + this.close(); + } + + @observable public fileLinks: any = []; + @action setFileLinks = action((links: any) => { this.fileLinks = links; }); + + private getServerPath = (link: any) => { return link.result.accessPaths.agnostic.server } + + private uploadFiles = (input: any) => { + // keep null while uploading + this.setFileLinks(null); + // upload the files to the server + if (input.files && input.files.length !== 0) { + const fileArray: File[] = Array.from(input.files); + (Networking.UploadFilesToServer(fileArray)).then(links => { + console.log('finshed uploading', links.map(this.getServerPath)); + this.setFileLinks((links ?? []).map(this.getServerPath)); + }) + } + + } + + + private renderIssue = (issue: any) => { + + const isReportingIssue = issue === null; + + return isReportingIssue ? + // report issue + (<div className="settings-content"> + <h3 style={{ 'textDecoration': 'underline'}}>Report an Issue</h3> + <label>Please leave a title for the bug.</label><br /> + <input type="text" placeholder='title' onChange={(e) => this.bugTitle = e.target.value} required/> + <br /> + <label>Please leave a description for the bug and how it can be recreated.</label> + <textarea placeholder='description' onChange={(e) => this.bugDescription = e.target.value} required/> + <br /> + {/* {<label>Send to github issues? </label> + <input type="checkbox" onChange={(e) => this.toGithub = e.target.checked} /> + <br /> } */} + + <label>Please label the issue</label> + <div className='flex-select'> + <select name="bugType"> + <option value="" disabled selected>Type</option> + <option value="bug">Bug</option> + <option value="cosmetic">Poor Design or Cosmetic</option> + <option value="documentation">Poor Documentation</option> + </select> + + <select name="bigPriority"> + <option value="" disabled selected>Priority</option> + <option value="priority-low">Low</option> + <option value="priority-medium">Medium</option> + <option value="priority-high">High</option> + </select> + </div> + + + <div> + <label>Upload media that shows the bug (optional)</label> + <input type="file" name="file" multiple accept='audio/*, video/*' onChange={e => this.uploadFiles(e.target)}/> + </div> + <br /> + + <button onClick={() => this.reportIssue()} disabled={this.fileLinks === null} style={{ backgroundColor: this.fileLinks === null ? 'grey' : '' }}>{this.fileLinks === null ? 'Uploading...' : 'Submit'}</button> + </div>) + : + // view issue + ( + <div className='issue-container'> + <h5 style={{'textAlign': "left"}}><a href={issue.html_url} target="_blank">Issue #{issue.number}</a></h5> + <div className='issue-title'> + {issue.title} + </div> + <ReactMarkdown children={issue.body} className='issue-body' linkTarget={"_blank"} remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]} /> + </div> + ); + } + + private showReportIssueScreen = () => { + this.setSelectedIssue(null); + } + + private closeReportIssueScreen = () => { + this.setSelectedIssue(undefined); + } + + private get reportInterface() { + + const isReportingIssue = this.selectedIssue === null; + + return ( + <div className="settings-interface"> + <div className='issue-list-wrapper'> + <h3>Current Issues</h3> + <input type="text" placeholder='search issues' onChange={(e => this.updateIssueSearch(e.target.value))}></input><br /> + {this.issues.length === 0 ? <ReactLoading className='loading-center'/> : this.shownIssues.map(issue => <div className='issue-list' key={issue.number} onClick={() => this.setSelectedIssue(issue)}>{issue.title}</div>)} + + {/* <div className="settings-user"> + <button onClick={() => this.getAllIssues().then(issues => this.issues = issues)}>Poll Issues</button> + </div> */} + </div> + + <div className="close-button" onClick={this.close}> + <FontAwesomeIcon icon={'times'} color="black" size={'lg'} /> + </div> + + <div className="issue-content" style={{'paddingTop' : this.selectedIssue === undefined ? '50px' : 'inherit'}}> + {this.selectedIssue === undefined ? "no issue selected" : this.renderIssue(this.selectedIssue)} + </div> + + <div className='report-issue-fab'> + <span className='report-disclaimer' hidden={!isReportingIssue}>Note: issue reporting is not anonymous.</span> + <button + onClick={() => isReportingIssue ? this.closeReportIssueScreen() : this.showReportIssueScreen()} + >{isReportingIssue ? 'Cancel' : 'Report New Issue'}</button> + </div> + + + </div> + ); + } + + render() { + return ( + <MainViewModal + contents={this.reportInterface} + isDisplayed={this.isOpen} + interactive={true} + closeOnExternalClick={this.close} + dialogueBoxStyle={{ width: 'auto', height: '500px', background: Cast(Doc.SharingDoc().userColor, 'string', null) }} + /> + ); + } +} diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index 1c84af94a..7a555d5f8 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -101,6 +101,9 @@ export namespace SelectionManager { } } ScriptingGlobals.add(function SelectionManager_selectedDocType(docType?: DocumentType, colType?: CollectionViewType, checkContext?: boolean) { + if (colType === ('tab' as any)) { + return SelectionManager.Views().lastElement()?.props.renderDepth === 0; + } let selected = (sel => (checkContext ? DocCast(sel?.context) : sel))(SelectionManager.SelectedSchemaDoc() ?? SelectionManager.Docs().lastElement()); return docType ? selected?.type === docType : colType ? selected?.viewType === colType : true; }); diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index 280ca8a8c..886dd974b 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -132,20 +132,6 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>() lookupField = (field: string) => ScriptCast((this.layoutDoc as any).lookupField)?.script.run({ self: this.layoutDoc, data: this.rootDoc, field: field }).result; - styleFromLayoutString = (scale: number) => { - const style: { [key: string]: any } = {}; - const divKeys = ['width', 'height', 'fontSize', 'transform', 'left', 'background', 'left', 'right', 'top', 'bottom', 'pointerEvents', 'position']; - const replacer = (match: any, expr: string, offset: any, string: any) => { - // bcz: this executes a script to convert a property expression string: { script } into a value - return ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name, scale: 'number' })?.script.run({ self: this.rootDoc, this: this.layoutDoc, scale }).result?.toString() ?? ''; - }; - divKeys.map((prop: string) => { - const p = (this.props as any)[prop]; - typeof p === 'string' && (style[prop] = p?.replace(/{([^.'][^}']+)}/g, replacer)); - }); - return style; - }; - protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; @computed public get annotationKey() { diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 6d1397395..3589e014a 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -203,8 +203,8 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P if (this._deleteAfterIconify) { views.forEach(iconView => { Doc.setNativeView(iconView.props.Document); - if (iconView.props.Document.isInkMask && iconView.props.Document.activeFrame !== undefined) { - iconView.props.Document.opacity = 0; // bcz: hacky ... allows inkMaks to be "turned off" without removing them from the collection which allows them to function properly in a presenation. + if (iconView.props.Document.activeFrame) { + iconView.props.Document.opacity = 0; // bcz: hacky ... allows inkMasks and other documents to be "turned off" without removing them from the animated collection which allows them to function properly in a presenation. } else { iconView.props.removeDocument?.(iconView.props.Document); } @@ -358,8 +358,8 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P @action onPointerDown = (e: React.PointerEvent): void => { - DragManager.docsBeingDragged.push(...SelectionManager.Views().map(dv => dv.rootDoc)); - this._inkDragDocs = DragManager.docsBeingDragged + const views = SelectionManager.Views().map(dv => dv.rootDoc); + this._inkDragDocs = views .filter(doc => doc.type === DocumentType.INK) .map(doc => { if (InkStrokeProperties.Instance._lock) { diff --git a/src/client/views/InkStroke.scss b/src/client/views/InkStroke.scss index 664f2448b..bed7caf7f 100644 --- a/src/client/views/InkStroke.scss +++ b/src/client/views/InkStroke.scss @@ -1,22 +1,23 @@ .inkstroke-UI { - // transform-origin: top left; - position: absolute; - overflow: visible; - pointer-events: none; - z-index: 2001; // 1 higher than documentdecorations - - svg:not(:root) { - overflow: visible !important; + // transform-origin: top left; position: absolute; - left:0; - top:0; - } + overflow: visible; + pointer-events: none; + z-index: 2001; // 1 higher than documentdecorations + + svg:not(:root) { + overflow: visible !important; + position: absolute; + left: 0; + top: 0; + } } .inkStroke-wrapper { display: flex; align-items: center; height: 100%; + transition: inherit; .inkStroke { mix-blend-mode: multiply; stroke-linejoin: round; @@ -26,8 +27,10 @@ width: 100%; height: 100%; pointer-events: none; + transition: inherit; svg:not(:root) { overflow: visible !important; + transition: inherit; } } diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts index 821e2f739..d19a916f9 100644 --- a/src/client/views/InkStrokeProperties.ts +++ b/src/client/views/InkStrokeProperties.ts @@ -347,7 +347,6 @@ export class InkStrokeProperties { const deltaX = snapData.nearestPt.X - ink[controlIndex].X; const deltaY = snapData.nearestPt.Y - ink[controlIndex].Y; const res = this.moveControlPtHandle(inkView, deltaX, deltaY, controlIndex, ink.slice()); - console.log('X = ' + snapData.nearestPt.X + ' ' + snapData.nearestPt.Y); return res; } } diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 2671aea56..dae1c10bb 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -25,7 +25,7 @@ import { action, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import { Doc, HeightSym, WidthSym } from '../../fields/Doc'; import { InkData, InkField, InkTool } from '../../fields/InkField'; -import { BoolCast, Cast, NumCast, RTFCast, StrCast } from '../../fields/Types'; +import { BoolCast, Cast, FieldValue, NumCast, RTFCast, StrCast } from '../../fields/Types'; import { TraceMobx } from '../../fields/util'; import { OmitKeys, returnFalse, setupMoveUpEvents } from '../../Utils'; import { CognitiveServices } from '../cognitive_services/CognitiveServices'; @@ -45,6 +45,10 @@ import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; import { INK_MASK_SIZE } from './global/globalCssVariables.scss'; import './InkStroke.scss'; import Color = require('color'); +import { ComputedField } from '../../fields/ScriptField'; +import { listSpec } from '../../fields/Schema'; +import { List } from '../../fields/List'; +import { StyleProp } from './StyleProvider'; @observer export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { @@ -56,7 +60,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { return inkData && inkData.lastElement().X === inkData[0].X && inkData.lastElement().Y === inkData[0].Y; } private _handledClick = false; // flag denoting whether ink stroke has handled a psuedo-click onPointerUp so that the real onClick event can be stopPropagated - private _selDisposer?: IReactionDisposer; + private _disposers: { [key: string]: IReactionDisposer } = {}; @observable _nearestSeg?: number; // nearest Bezier segment along the ink stroke to the cursor (used for displaying the Add Point highlight) @observable _nearestT?: number; // nearest t value within the nearest Bezier segment " @@ -64,13 +68,13 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { componentDidMount() { this.props.setContentView?.(this); - this._selDisposer = reaction( + this._disposers.selfDisper = reaction( () => this.props.isSelected(), // react to stroke being deselected by turning off ink handles selected => !selected && (InkStrokeProperties.Instance._controlButton = false) ); } componentWillUnmount() { - this._selDisposer?.(); + Object.keys(this._disposers).forEach(key => this._disposers[key]()); } // transform is the inherited screentolocal xf plus any scaling that was done to make the stroke @@ -356,7 +360,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { const closed = InkingStroke.IsClosed(inkData); const isInkMask = BoolCast(this.layoutDoc.isInkMask); const fillColor = isInkMask ? '#aaaaaa' : StrCast(this.layoutDoc.fillColor, 'transparent'); - const strokeColor = !closed && fillColor && fillColor !== 'transparent' ? fillColor : StrCast(this.layoutDoc.color); + const strokeColor = !closed && fillColor && fillColor !== 'transparent' ? fillColor : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color) ?? StrCast(this.layoutDoc.color); // bcz: Hack!! Not really sure why, but having fractional values for width/height of mask ink strokes causes the dragging clone (see DragManager) to be offset from where it should be. if (isInkMask && (this.layoutDoc[WidthSym]() !== Math.round(this.layoutDoc[WidthSym]()) || this.layoutDoc[HeightSym]() !== Math.round(this.layoutDoc[HeightSym]()))) { @@ -401,13 +405,13 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { inkTop, highlightColor, inkStrokeWidth, - fillColor && closed && highlightIndex ? highlightIndex / 2 : inkStrokeWidth + (fillColor ? (closed ? 0 : highlightIndex + 2) : 0), + Math.max(5, fillColor && closed && highlightIndex ? highlightIndex / 2 : inkStrokeWidth + (fillColor ? (closed ? 0 : highlightIndex + 2) : 0)), StrCast(this.layoutDoc.strokeLineJoin), StrCast(this.layoutDoc.strokeLineCap), StrCast(this.layoutDoc.strokeBezier), !closed ? 'none' : fillColor === 'transparent' || suppressFill ? 'none' : fillColor, - startMarker, - endMarker, + '', + '', markerScale, undefined, inkScaleX, diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index c166594e5..06be4d194 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -20,6 +20,7 @@ import { DocumentManager } from '../util/DocumentManager'; import { GroupManager } from '../util/GroupManager'; import { HistoryUtil } from '../util/History'; import { Hypothesis } from '../util/HypothesisUtils'; +import { ReportManager } from '../util/ReportManager'; import { ScriptingGlobals } from '../util/ScriptingGlobals'; import { SelectionManager } from '../util/SelectionManager'; import { ColorScheme, SettingsManager } from '../util/SettingsManager'; @@ -171,6 +172,7 @@ export class MainView extends React.Component { 'curPage', 'viewType', 'chromeHidden', + 'currentFrame', 'width', 'nativeWidth', ]); // can play with these fields on someone else's @@ -975,10 +977,11 @@ export class MainView extends React.Component { <DictationOverlay /> <SharingManager /> <SettingsManager /> + <ReportManager /> <CaptureManager /> <GroupManager /> <GoogleAuthenticationManager /> - <DocumentDecorations boundsLeft={this.leftScreenOffsetOfMainDocView} boundsTop={this.topOfHeaderBarDoc} PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} /> + <DocumentDecorations boundsLeft={this.leftScreenOffsetOfMainDocView} boundsTop={this.topOfSidebarDoc} PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} /> <ComponentDecorations boundsLeft={this.leftScreenOffsetOfMainDocView} boundsTop={this.topOfMainDocContent} /> {this._hideUI ? null : <TopBar />} {LinkDescriptionPopup.descriptionPopup ? <LinkDescriptionPopup /> : null} @@ -991,7 +994,7 @@ export class MainView extends React.Component { default: return ( <> - <div style={{ position: 'relative', display: this._hideUI || LightboxView.LightboxDoc ? 'none' : undefined, zIndex: 1999 }}> + <div style={{ position: 'relative', display: this._hideUI || LightboxView.LightboxDoc ? 'none' : undefined, zIndex: 2001 }}> <CollectionMenu panelWidth={this.topMenuWidth} panelHeight={this.topMenuHeight} /> </div> {this.mainDashboardArea} diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index 3bd4f5152..c0ba170c6 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -40,7 +40,8 @@ export enum StyleProp { JitterRotation = 'jitterRotation', // whether documents should be randomly rotated BorderPath = 'customBorder', // border path for document view FontSize = 'fontSize', // size of text font - FontFamily = 'fontFamily', // size of text font + FontFamily = 'fontFamily', // font family of text + FontWeight = 'fontWeight', // font weight of text } function darkScheme() { @@ -117,9 +118,11 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps case StyleProp.HideLinkButton: return props?.hideLinkButton || (!selected && (doc?.isLinkButton || doc?.hideLinkButton)); case StyleProp.FontSize: - return StrCast(doc?.[fieldKey + 'fontSize'], StrCast(doc?.fontSize, StrCast(Doc.UserDoc().fontSize))); + return StrCast(doc?.[fieldKey + 'fontSize'], StrCast(doc?._fontSize, StrCast(Doc.UserDoc().fontSize))); case StyleProp.FontFamily: - return StrCast(doc?.[fieldKey + 'fontFamily'], StrCast(doc?.fontFamily, StrCast(Doc.UserDoc().fontFamily))); + return StrCast(doc?.[fieldKey + 'fontFamily'], StrCast(doc?._fontFamily, StrCast(Doc.UserDoc().fontFamily))); + case StyleProp.FontWeight: + return StrCast(doc?.[fieldKey + 'fontWeight'], StrCast(doc?._fontWeight, StrCast(Doc.UserDoc().fontWeight))); case StyleProp.ShowTitle: return ( (doc && diff --git a/src/client/views/_nodeModuleOverrides.scss b/src/client/views/_nodeModuleOverrides.scss index 17eff022f..b1cce8705 100644 --- a/src/client/views/_nodeModuleOverrides.scss +++ b/src/client/views/_nodeModuleOverrides.scss @@ -44,7 +44,8 @@ div .lm_header { position: absolute; width: calc(100% - 60px); overflow: scroll; - background: $dark-gray; + background: #6b6b6b6b; //$dark-gray; + border-radius: 5px; } .lm_tab { diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx index eb55650e4..0dc30e0fd 100644 --- a/src/client/views/collections/CollectionMenu.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -587,49 +587,6 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu } @undoBatch - @action - pinWithView = (targetDoc: Opt<Doc>) => { - if (targetDoc) { - TabDocView.PinDoc(targetDoc); - const presArray: Doc[] = PresBox.Instance?.sortArray(); - const size: number = PresBox.Instance?._selectedArray.size; - const presSelected: Doc | undefined = presArray && size ? presArray[size - 1] : undefined; - const activeDoc = presSelected ? PresBox.Instance?.childDocs[PresBox.Instance?.childDocs.indexOf(presSelected) + 1] : PresBox.Instance?.childDocs[PresBox.Instance?.childDocs.length - 1]; - if (targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.RTF || targetDoc.type === DocumentType.WEB || targetDoc._viewType === CollectionViewType.Stacking || targetDoc._viewType === CollectionViewType.NoteTaking) { - const scroll = targetDoc._scrollTop; - activeDoc.presPinView = true; - activeDoc.presPinViewScroll = scroll; - } else if ((targetDoc.type === DocumentType.COL && targetDoc._viewType === CollectionViewType.Freeform) || targetDoc.type === DocumentType.IMG || targetDoc.type === DocumentType.MAP) { - const x = targetDoc._panX; - const y = targetDoc._panY; - const scale = targetDoc._viewScale; - activeDoc.presPinView = true; - activeDoc.presPinViewX = x; - activeDoc.presPinViewY = y; - activeDoc.presPinViewScale = scale; - } else if (targetDoc.type === DocumentType.VID) { - activeDoc.presPinView = true; - } else if (targetDoc.type === DocumentType.COMPARISON) { - const width = targetDoc._clipWidth; - activeDoc.presPinClipWidth = width; - activeDoc.presPinView = true; - } - } - }; - - @computed - get pinWithViewButton() { - const presPinWithViewIcon = <img src={`/assets/pinWithView.png`} style={{ margin: 'auto', width: 19 }} />; - return !this.selectedDoc ? null : ( - <Tooltip title={<div className="dash-tooltip">{'Pin with current view'}</div>} placement="top"> - <button className="antimodeMenu-button" style={{ justifyContent: 'center' }} onClick={() => this.pinWithView(this.selectedDoc)}> - {presPinWithViewIcon} - </button> - </Tooltip> - ); - } - - @undoBatch onAlias = () => { if (this.selectedDoc && this.selectedDocumentView) { // const copy = Doc.MakeCopy(this.selectedDocumentView.props.Document, true); @@ -722,7 +679,6 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu {this.aliasButton} {/* {this.pinButton} */} {this.toggleOverlayButton} - {this.pinWithViewButton} <div className="collectionMenu-divider" key="divider2"></div> {this.subChrome} <div className="collectionMenu-divider" key="divider3"></div> diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx index 5c8b10ae1..b359ef420 100644 --- a/src/client/views/collections/CollectionNoteTakingView.tsx +++ b/src/client/views/collections/CollectionNoteTakingView.tsx @@ -1,6 +1,6 @@ import React = require('react'); import { CursorProperty } from 'csstype'; -import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import { DataSym, Doc, Field, HeightSym, Opt, WidthSym } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; @@ -37,84 +37,82 @@ export type collectionNoteTakingViewProps = { NativeHeight?: () => number; }; -//TODO: somehow need to update the mapping and then have everything else rerender. Maybe with a refresh boolean like -// in Hypermedia? - +/** + * CollectionNoteTakingView is a column-based view for displaying documents. In this view, the user can (1) + * add and remove columns (2) change column sizes and (3) move documents within and between columns. This + * view is reminiscent of Kanban-style web apps like Trello, or the 'Board' view in Notion. Each column is + * headed by a SchemaHeaderField followed by the column's documents. SchemaHeaderFields are NOT present in + * the rest of Dash, so it may be worthwhile to transition the headers to simple documents. + */ @observer export class CollectionNoteTakingView extends CollectionSubView<Partial<collectionNoteTakingViewProps>>() { _disposers: { [key: string]: IReactionDisposer } = {}; _masonryGridRef: HTMLDivElement | null = null; - _draggerRef = React.createRef<HTMLDivElement>(); // change to relative widths for deleting. change storage from columnStartXCoords to columnHeaders (schemaHeaderFields has a widgth alrady) - @observable columnStartXCoords: number[] = []; // columnHeaders -- SchemaHeaderField -- widht + _draggerRef = React.createRef<HTMLDivElement>(); @observable docsDraggedRowCol: number[] = []; @observable _cursor: CursorProperty = 'grab'; - @observable _scroll = 0; // used to force the document decoration to update when scrolling + @observable _scroll = 0; @computed get chromeHidden() { return this.props.chromeHidden || BoolCast(this.layoutDoc.chromeHidden); } + // columnHeaders returns the list of SchemaHeaderFields currently being used by the layout doc to render the columns @computed get columnHeaders() { - const columnHeaders = Array.from(Cast(this.dataDoc.columnHeaders, listSpec(SchemaHeaderField), null)); + const columnHeaders = Cast(this.dataDoc.columnHeaders, listSpec(SchemaHeaderField), null); const needsUnsetCategory = this.childDocs.some(d => !d[this.notetakingCategoryField] && !columnHeaders.find(sh => sh.heading === 'unset')); - - // @#Oberable draggedColIndex = ... - //@observable cloneDivXYcoords - // @observable clonedDiv... - - // render() { - // { this.clonedDiv ? <div clone styule={{transform: clonedDivXYCoords}} : null} - // } - - // in NoteatakinView Column code, add a poinerDown handler that calls setupMoveUpEvents() which will clone the column div - // and re-render it under the cursor during move events. - // that move move event will update 2 observales -- the draggedColIndex up above, and the location of the clonedDiv so that the render in this view will know where to render the cloned div - // add observable variable that tells drag column to rnder in a different location than where the schemaHeaderFiel sa y ot. - // if (col 1 is where col 3) { - // return 3 2 1 4 56 - // } if (needsUnsetCategory) { - columnHeaders.push(new SchemaHeaderField('unset')); + setTimeout(() => columnHeaders.push(new SchemaHeaderField('unset', undefined, undefined, 1))); } return columnHeaders; } + // notetakingCategoryField returns the key to accessing a document's column value @computed get notetakingCategoryField() { return 'NotetakingCategory'; } - @computed get filteredChildren() { - return this.childLayoutPairs.filter(pair => pair.layout instanceof Doc && !pair.layout.hidden).map(pair => pair.layout); - } @computed get headerMargin() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin); } @computed get xMargin() { return NumCast(this.layoutDoc._xMargin, 2 * Math.min(this.gridGap, 0.05 * this.props.PanelWidth())); } + // dividerWidth returns the width of a CollectionNoteTakingViewDivider + @computed get dividerWidth() { + return 32; + } @computed get yMargin() { return this.props.yPadding || NumCast(this.layoutDoc._yMargin, 5); - } // 2 * this.gridGap)); } + } @computed get gridGap() { return NumCast(this.layoutDoc._gridGap, 10); } + // numGroupColumns returns the number of columns @computed get numGroupColumns() { return this.columnHeaders.length; } + // PanelWidth returns the size of the total available space the view occupies @computed get PanelWidth() { return this.props.PanelWidth(); } - @computed get maxColWdith() { + // maxColWidth returns the maximum column width, which is slightly less than the total available space. + @computed get maxColWidth() { return this.props.PanelWidth() - 2 * this.xMargin; } + // availableWidth is the total amount of non-divider width. Since widths are stored relatively, + // we use availableWidth to convert from a percentage to a pixel count. + @computed get availableWidth() { + const numDividers = this.columnHeaders.length - 1; + return this.maxColWidth - numDividers * this.dividerWidth; + } - // If the user has not yet created any docs (in another view), this will create a single column. Otherwise, - // it will adjust according to the + // Documents should NOT have column category fields until entering this view, so the contructor creates the 'New Column' + // category for the user to then edit later. constructor(props: any) { super(props); if (this.columnHeaders === undefined) { - this.dataDoc.columnHeaders = new List<SchemaHeaderField>([new SchemaHeaderField('New Column')]); - // add all of the docs that have not been added to a column to this new column + this.dataDoc.columnHeaders = new List<SchemaHeaderField>([new SchemaHeaderField('New Column', undefined, undefined, 1)]); } } - // passed as a prop to the NoteTakingField, which uses this function + // children is passed as a prop to the NoteTakingField, which uses this function // to render the docs you see within an individual column. children = (docs: Doc[]) => { TraceMobx(); @@ -130,39 +128,30 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti }); }; + // Sections is one of the more important functions in this file, rendering the the documents + // for the UI. It properly renders documents being dragged between columns. // [CAVEATS] (1) keep track of the offsetting // (2) documentView gets unmounted as you remove it from the list @computed get Sections() { TraceMobx(); const columnHeaders = this.columnHeaders; - let docs = this.childDocs; + // filter out the currently dragged docs from the child docs, since we will insert them later + const docs = this.childDocs.filter(d => !DragManager.docsBeingDragged.includes(d)); const sections = new Map<SchemaHeaderField, Doc[]>(columnHeaders.map(sh => [sh, []] as [SchemaHeaderField, []])); const rowCol = this.docsDraggedRowCol; - - // filter out the currently dragged docs from the child docs, since we will insert them later - if (rowCol.length && DragManager.docsBeingDragged.length) { - const docIdsToRemove = new Set(); - DragManager.docsBeingDragged.forEach(d => docIdsToRemove.add(d[Id])); - docs = docs.filter(d => !docIdsToRemove.has(d[Id])); - } - // this will sort the docs into the correct columns (minus the ones you're currently dragging) docs.map(d => { const sectionValue = (d[this.notetakingCategoryField] as object) ?? `unset`; - // look for if header exists already const existingHeader = columnHeaders.find(sh => sh.heading === sectionValue.toString()); if (existingHeader) { sections.get(existingHeader)!.push(d); } }); - // now we add back in the docs that we're dragging - if (rowCol.length && DragManager.docsBeingDragged.length) { - const colHeader = columnHeaders[rowCol[1]]; - // TODO: get the actual offset that occurs if the docs were in that column + if (rowCol.length) { const offset = 0; - sections.get(colHeader)?.splice(rowCol[0] - offset, 0, ...DragManager.docsBeingDragged); + sections.get(columnHeaders[rowCol[1]])?.splice(rowCol[0] - offset, 0, ...DragManager.docsBeingDragged); } return sections; } @@ -173,6 +162,7 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti 100 ); }; + componentDidMount() { super.componentDidMount?.(); document.addEventListener('pointerup', this.removeDocDragHighlight, true); @@ -180,11 +170,6 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti () => this.layoutDoc._autoHeight, autoHeight => autoHeight && this.props.setHeight?.(Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), this.headerMargin + Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace('px', '')))))) ); - this._disposers.headers = reaction( - () => this.columnHeaders.slice(), - headers => this.resizeColumns(headers.length), - { fireImmediately: true } - ); } componentWillUnmount() { @@ -206,6 +191,7 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti @computed get onChildClickHandler() { return () => this.props.childClickScript || ScriptCast(this.Document.onChildClick); } + @computed get onChildDoubleClickHandler() { return () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); } @@ -225,7 +211,6 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti // let's dive in and get the actual document we want to drag/move around focusDocument = (doc: Doc, options?: DocFocusOptions) => { Doc.BrushDoc(doc); - let focusSpeed = 0; const found = this._mainCont && Array.from(this._mainCont.getElementsByClassName('documentView-node')).find((node: any) => node.id === doc[Id]); if (found) { @@ -260,7 +245,7 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti isContentActive = () => this.props.isSelected() || this.props.isContentActive(); - // rules for displaying the documents + // getDisplayDoc returns the rules for displaying a document in this view (ie. DocumentView) getDisplayDoc(doc: Doc, width: () => number) { const dataDoc = !doc.isTemplateDoc && !doc.isTemplateForField && !doc.PARAMS ? undefined : this.props.DataDoc; const height = () => this.getDocHeight(doc); @@ -316,7 +301,7 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti ); } - // This is used to get the coordinates of a document when we go from a view like freeform to columns + // getDocTransform is used to get the coordinates of a document when we go from a view like freeform to columns getDocTransform(doc: Doc, dref?: DocumentView) { const y = this._scroll; // required for document decorations to update when the text box container is scrolled const { scale, translateX, translateY } = Utils.GetScreenTransform(dref?.ContentDiv || undefined); @@ -329,9 +314,8 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti getDocWidth(d: Doc) { const heading = !d[this.notetakingCategoryField] ? 'unset' : Field.toString(d[this.notetakingCategoryField] as Field); const existingHeader = this.columnHeaders.find(sh => sh.heading === heading); - const index = existingHeader ? this.columnHeaders.indexOf(existingHeader) : 0; - const endColValue = index === this.columnHeaders.length - 1 || index > this.columnStartXCoords.length - 1 ? this.PanelWidth : this.columnStartXCoords[index + 1]; - const maxWidth = index > this.columnStartXCoords.length - 1 ? this.PanelWidth : endColValue - this.columnStartXCoords[index] - 3 * this.xMargin; + const existingWidth = existingHeader?.width ? existingHeader.width : 0; + const maxWidth = existingWidth > 0 ? existingWidth * this.availableWidth - 2 * this.xMargin : this.maxColWidth - 2 * this.xMargin; if (d.type === DocumentType.RTF) { return maxWidth; } @@ -358,28 +342,36 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti return Math.min(childHeight, maxHeight, panelHeight); } - // called when a column is either added or deleted. This function creates n evenly spaced columns + // resizeColumns is called whenever a user adds or removes a column. When removing, + // this function renormalizes the column widths to fill the newly available space + // in the panel. When adding, this function renormalizes the existing columns to take up + // (n - 1)/n space, since the new column will be allocated 1/n of the total space. + // Column widths are relative (portion of available space) and stored in the 'width' + // field of SchemaHeaderFields. + // + // Removing example: column widths are [0.5, 0.30, 0.20] --> user deletes the final column --> column widths are [0.625, 0.375]. + // Adding example: column widths are [0.6, 0.4] --> user adds column at end --> column widths are [0.4, 0.267, 0.33] @action - resizeColumns = (n: number) => { - const totalWidth = this.PanelWidth; - const dividerWidth = 32; - const totaldividerWidth = (n - 1) * dividerWidth; - const colWidth = (totalWidth - totaldividerWidth) / n; - const newColXCoords: number[] = []; - let colStart = 0; - for (let i = 0; i < n; i++) { - newColXCoords.push(colStart); - colStart += colWidth + dividerWidth; + resizeColumns = (isAdd: boolean, colWidth: number, colIndex: number) => { + const n = this.columnHeaders.length; + if (n == 1) { + this.columnHeaders[0].setWidth(1); + return true; } - this.columnStartXCoords = newColXCoords; + const scaleFactor = isAdd ? 1 - colWidth : 1 / (1 - colWidth); + this.columnHeaders.forEach((h, i) => { + if (!(isAdd && i == colIndex)) { + h.width < 0 ? h.setWidth(1 / n) : h.setWidth(h.width * scaleFactor); + } + }); + return true; }; - // This function is used to preview where a document will drop in a column once a drag is complete. + // onPointerMove is used to preview where a document will drop in a column once a drag is complete. @action onPointerMove = (force: boolean, ex: number, ey: number) => { if (this.childDocList && (this.childDocList.includes(DragManager.DocDragData?.draggedDocuments.lastElement()!) || force || this.isContentActive())) { // get the current docs for the column based on the mouse's x coordinate - // will use again later, which is why we're saving as local const xCoord = this.props.ScreenToLocalTransform().transformPoint(ex, ey)[0] - 2 * this.gridGap; const colDocs = this.getDocsFromXCoord(xCoord); // get the index for where you need to insert the doc you are currently dragging @@ -408,10 +400,16 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti } }; - // returns the column index for a given x-coordinate + // getColumnFromXCoord returns the column index for a given x-coordinate (currently always the client's mouse coordinate). + // This function is used to know which document a column SHOULD be in while it is being dragged. getColumnFromXCoord = (xCoord: number): number => { const numColumns = this.columnHeaders.length; - const coords = this.columnStartXCoords.slice(); + const coords = []; + let colStartXCoord = 0; + for (let i = 0; i < numColumns; i++) { + coords.push(colStartXCoord); + colStartXCoord += this.columnHeaders[i].width * this.availableWidth + this.dividerWidth; + } coords.push(this.PanelWidth); let colIndex = 0; for (let i = 0; i < numColumns; i++) { @@ -423,7 +421,7 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti return colIndex; }; - // returns the docs of a column based on the x-coordinate provided. + // getDocsFromXCoord returns the docs of a column based on the x-coordinate provided. getDocsFromXCoord = (xCoord: number): Doc[] => { const colIndex = this.getColumnFromXCoord(xCoord); const colHeader = StrCast(this.columnHeaders[colIndex].heading); @@ -455,6 +453,8 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti } }; + // onInternalDrop is used when dragging and dropping a document within the view, such as dragging + // a document to a new column or changing its order within the column. @undoBatch @action onInternalDrop = (e: Event, de: DragManager.DropEvent) => { @@ -464,14 +464,11 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti const rowCol = this.docsDraggedRowCol; const droppedDocs = this.childDocs.slice().filter((d: Doc, ind: number) => ind >= this.childDocs.length); // if the drop operation adds something to the end of the list, then use that as the new document (may be different than what was dropped e.g., in the case of a button which is dropped but which creates say, a note). const newDocs = droppedDocs.length ? droppedDocs : de.complete.docDragData.droppedDocuments; - - // const docs = this.childDocs const docs = this.childDocList; if (docs && newDocs.length) { // remove the dragged documents from the childDocList newDocs.filter(d => docs.indexOf(d) !== -1).forEach(d => docs.splice(docs.indexOf(d), 1)); // if the doc starts a columnm (or the drop index is undefined), we can just push it to the front. Otherwise we need to add it to the column properly - //TODO: you need to update childDocList instead. It seems that childDocs is a copy of the actual array we want to modify if (rowCol[0] <= 0) { docs.splice(0, 0, ...newDocs); } else { @@ -482,8 +479,7 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti } } } - } // it seems like we're creating a link here. Weird. I didn't know that you could establish links by dragging - else if (de.complete.linkDragData?.dragDocument.context === this.props.Document && de.complete.linkDragData?.linkDragView?.props.CollectionFreeFormDocumentView?.()) { + } else if (de.complete.linkDragData?.dragDocument.context === this.props.Document && de.complete.linkDragData?.linkDragView?.props.CollectionFreeFormDocumentView?.()) { const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, _fitWidth: true, title: 'dropped annotation' }); this.props.addDocument?.(source); de.complete.linkDocument = DocUtils.MakeLink({ doc: source }, { doc: de.complete.linkDragData.linkSourceGetAnchor() }, 'doc annotation', ''); // TODODO this is where in text links get passed @@ -502,7 +498,8 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti return true; } - // when dropping outside of the current noteTaking context (like a new tab, freeform view, etc...) + // onExternalDrop is used when dragging a document out from a CollectionNoteTakingView + // to another tab/view/collection onExternalDrop = async (e: React.DragEvent): Promise<void> => { const targInd = this.docsDraggedRowCol?.[0] || 0; const colInd = this.docsDraggedRowCol?.[1] || 0; @@ -530,12 +527,14 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti headings = () => Array.from(this.Sections); refList: any[] = []; + editableViewProps = () => ({ GetValue: () => '', SetValue: this.addGroup, contents: '+ New Column', }); + // sectionNoteTaking returns a CollectionNoteTakingViewColumn (which is an individual column) sectionNoteTaking = (heading: SchemaHeaderField | undefined, docList: Doc[]) => { const type = 'number'; return ( @@ -558,8 +557,6 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti } }} addDocument={this.addDocument} - // docsByColumnHeader={this._docsByColumnHeader} - // setDocsForColHeader={this.setDocsForColHeader} chromeHidden={this.chromeHidden} columnHeaders={this.columnHeaders} Document={this.props.Document} @@ -569,8 +566,9 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti numGroupColumns={this.numGroupColumns} gridGap={this.gridGap} pivotField={this.notetakingCategoryField} - columnStartXCoords={this.columnStartXCoords} - maxColWidth={this.maxColWdith} + dividerWidth={this.dividerWidth} + maxColWidth={this.maxColWidth} + availableWidth={this.availableWidth} PanelWidth={this.PanelWidth} key={heading?.heading ?? ''} headings={this.headings} @@ -586,19 +584,20 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti ); }; - // called when adding a new columnHeader + // addGroup is called when adding a new columnHeader, adding a SchemaHeaderField to our list of + // columnHeaders and resizing the existing columns to make room for our new one. @undoBatch @action addGroup = (value: string) => { + for (const header of this.columnHeaders) { + if (header.heading == value) { + alert('You cannot use an existing column name. Please try a new column name'); + return value; + } + } const columnHeaders = Cast(this.props.Document.columnHeaders, listSpec(SchemaHeaderField), null); - return value && columnHeaders?.push(new SchemaHeaderField(value)) ? true : false; - }; - - sortFunc = (a: [SchemaHeaderField, Doc[]], b: [SchemaHeaderField, Doc[]]): 1 | -1 => { - const descending = StrCast(this.layoutDoc._columnsSort) === 'descending'; - const firstEntry = descending ? b : a; - const secondEntry = descending ? a : b; - return firstEntry[0].heading > secondEntry[0].heading ? 1 : -1; + const newColWidth = 1 / (this.numGroupColumns + 1); + return value && columnHeaders?.push(new SchemaHeaderField(value, undefined, undefined, newColWidth)) && this.resizeColumns(true, newColWidth, this.columnHeaders.length - 1) ? true : false; }; onContextMenu = (e: React.MouseEvent): void => { @@ -612,29 +611,29 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti } }; - // used to reset column sizes when using the drag handlers + // setColumnStartXCoords is used to update column widths when using the drag handlers between columns @action - setColumnStartXCoords = (movementX: number, colIndex: number) => { - const coords = [...this.columnStartXCoords]; - coords[colIndex] += movementX; - this.columnStartXCoords = coords; + setColumnStartXCoords = (movementXScreen: number, colIndex: number) => { + const movementX = this.props.ScreenToLocalTransform().transformDirection(movementXScreen, 0)[0]; + const leftHeader = this.columnHeaders[colIndex]; + const rightHeader = this.columnHeaders[colIndex + 1]; + leftHeader.setWidth(leftHeader.width + movementX / this.availableWidth); + rightHeader.setWidth(rightHeader.width - movementX / this.availableWidth); }; + // renderedSections returns a list of all of the JSX elements used (columns and dividers). If the view + // has more than one column, those columns will be separated by a CollectionNoteTakingViewDivider that + // allows the user to adjust the column widths. @computed get renderedSections() { TraceMobx(); - // let sections = [[undefined, this.filteredChildren] as [SchemaHeaderField | undefined, Doc[]]]; - // if (this.pivotField) { - // const entries = Array.from(this.Sections.entries()); - // sections = this.layoutDoc._columnsSort ? entries.sort(this.sortFunc) : entries; - // } const entries = Array.from(this.Sections.entries()); - const sections = entries; //.sort(this.sortFunc); + const sections = entries; const eles: JSX.Element[] = []; for (let i = 0; i < sections.length; i++) { const col = this.sectionNoteTaking(sections[i][0], sections[i][1]); eles.push(col); if (i < sections.length - 1) { - eles.push(<CollectionNoteTakingViewDivider key={`divider${i}`} index={i + 1} setColumnStartXCoords={this.setColumnStartXCoords} xMargin={this.xMargin} />); + eles.push(<CollectionNoteTakingViewDivider key={`divider${i}`} index={i} setColumnStartXCoords={this.setColumnStartXCoords} xMargin={this.xMargin} />); } } return eles; @@ -642,7 +641,6 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti @computed get buttonMenu() { const menuDoc: Doc = Cast(this.rootDoc.buttonMenuDoc, Doc, null); - // TODO:glr Allow support for multiple buttons if (menuDoc) { const width: number = NumCast(menuDoc._width, 30); const height: number = NumCast(menuDoc._height, 30); @@ -693,7 +691,9 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti @computed get backgroundEvents() { return SnappingManager.GetIsDragging(); } + observer: any; + render() { TraceMobx(); const buttonMenu = this.rootDoc.buttonMenu; diff --git a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx index 624beca96..4610da4e3 100644 --- a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx +++ b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx @@ -5,11 +5,12 @@ import { observer } from 'mobx-react'; import { Doc, DocListCast, Opt } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { RichTextField } from '../../../fields/RichTextField'; +import { listSpec } from '../../../fields/Schema'; import { SchemaHeaderField } from '../../../fields/SchemaHeaderField'; -import { ScriptField } from '../../../fields/ScriptField'; +import { Cast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, returnEmptyString, setupMoveUpEvents } from '../../../Utils'; +import { returnEmptyString } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; import { DragManager } from '../../util/DragManager'; @@ -21,13 +22,10 @@ import { ContextMenuProps } from '../ContextMenuItem'; import { EditableView } from '../EditableView'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; import './CollectionNoteTakingView.scss'; -import { listSpec } from '../../../fields/Schema'; -import { Cast } from '../../../fields/Types'; const higflyout = require('@hig/flyout'); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; -// So this is how we are storing a column interface CSVFieldColumnProps { Document: Doc; DataDoc: Opt<Doc>; @@ -38,7 +36,6 @@ interface CSVFieldColumnProps { columnHeaders: SchemaHeaderField[] | undefined; headingObject: SchemaHeaderField | undefined; yMargin: number; - // columnWidth: number; numGroupColumns: number; gridGap: number; type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | undefined; @@ -49,32 +46,32 @@ interface CSVFieldColumnProps { screenToLocalTransform: () => Transform; observeHeight: (myref: any) => void; unobserveHeight: (myref: any) => void; - //setDraggedCol:(clonedDiv:any, header:SchemaHeaderField, xycoors: ) editableViewProps: () => any; - resizeColumns: (n: number) => void; - columnStartXCoords: number[]; + resizeColumns: (isAdd: boolean, colWidth: number, colIndex: number) => boolean; PanelWidth: number; maxColWidth: number; - // docsByColumnHeader: Map<string, Doc[]> - // setDocsForColHeader: (key: string, docs: Doc[]) => void + dividerWidth: number; + availableWidth: number; } +/** + * CollectionNoteTakingViewColumn represents an individual column rendered in CollectionNoteTakingView. The + * majority of functions here are for rendering styles. + */ @observer export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColumnProps> { @observable private _background = 'inherit'; + // columnWidth returns the width of a column in absolute pixels @computed get columnWidth() { - // base cases - if (!this.props.columnHeaders || !this.props.headingObject || this.props.columnHeaders.length == 1) { + if (!this.props.columnHeaders || !this.props.headingObject) { return this.props.maxColWidth; } - const i = this.props.columnHeaders.indexOf(this.props.headingObject); - if (i < 0 || i > this.props.columnStartXCoords.length - 1) { + if (this.props.columnHeaders.length == 1) { return this.props.maxColWidth; } - const endColValue = i == this.props.numGroupColumns - 1 ? this.props.PanelWidth : this.props.columnStartXCoords[i + 1]; - // TODO make the math work here. 35 is half of 70, which is the current width of the divider - return endColValue - this.props.columnStartXCoords[i] - 30; + const i = this.props.columnHeaders.indexOf(this.props.headingObject); + return this.props.columnHeaders[i].width * this.props.availableWidth; } private dropDisposer?: DragManager.DragDropDisposer; @@ -84,8 +81,6 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu @observable _color = this.props.headingObject ? this.props.headingObject.color : '#f1efeb'; _ele: HTMLElement | null = null; - // This is likely similar to what we will be doing. Why do we need to make these refs? - // is that the only way to have drop targets? createColumnDropRef = (ele: HTMLDivElement | null) => { this.dropDisposer?.(); if (ele) { @@ -134,6 +129,7 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu @action pointerLeave = () => (this._background = 'inherit'); textCallback = (char: string) => this.addNewTextDoc('-typed text-', false, true); + // addNewTextDoc is called when a user starts typing in a column to create a new node @action addNewTextDoc = (value: string, shiftDown?: boolean, forceEmptyNote?: boolean) => { if (!value && !forceEmptyNote) return false; @@ -146,14 +142,22 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu return this.props.addDocument?.(newDoc) || false; }; + // deleteColumn is called when a user deletes a column using the 'trash' icon in the button area. + // If the user deletes the first column, the documents get moved to the second column. Otherwise, + // all docs are added to the column directly to the left. @undoBatch @action deleteColumn = () => { const columnHeaders = Cast(this.props.Document.columnHeaders, listSpec(SchemaHeaderField), null); if (columnHeaders && this.props.headingObject) { const index = columnHeaders.indexOf(this.props.headingObject); - this.props.docList.forEach(d => (d[this.props.pivotField] = 'unset')); + const newColIndex = index > 0 ? index - 1 : 1; + const newColHeader = this.props.columnHeaders ? this.props.columnHeaders[newColIndex] : undefined; + const newHeading = newColHeader ? newColHeader.heading : 'unset'; + this.props.docList.forEach(d => (d[this.props.pivotField] = newHeading)); + const colWidth = this.props.columnHeaders ? this.props.columnHeaders[index].width : 0; columnHeaders.splice(index, 1); + this.props.resizeColumns(false, colWidth, index); } }; @@ -255,7 +259,6 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu </div> </div> ) : null; - // const templatecols = `${this.props.columnWidth / this.props.numGroupColumns}px `; const templatecols = `${this.columnWidth}px `; const type = this.props.Document.type; return ( @@ -280,10 +283,7 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu </div> {!this.props.chromeHidden && type !== DocumentType.PRES ? ( - <div - className="collectionNoteTakingView-DocumentButtons" - // style={{ width: this.props.columnWidth / this.props.numGroupColumns, marginBottom: 10 }}> - style={{ width: this.columnWidth - 20, marginBottom: 10 }}> + <div className="collectionNoteTakingView-DocumentButtons" style={{ width: this.columnWidth - 20, marginBottom: 10 }}> <div key={`${heading}-add-document`} className="collectionNoteTakingView-addDocumentButton"> <EditableView GetValue={returnEmptyString} SetValue={this.addNewTextDoc} textCallback={this.textCallback} placeholder={"Type ':' for commands"} contents={'+ New Node'} menuCallback={this.menuCallback} /> </div> @@ -311,7 +311,6 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu className={'collectionNoteTakingViewFieldColumn' + (SnappingManager.GetIsDragging() ? 'Dragging' : '')} key={heading} style={{ - //TODO: change this so that it's based on the column width width: this.columnWidth, background: this._background, }} diff --git a/src/client/views/collections/CollectionNoteTakingViewDivider.tsx b/src/client/views/collections/CollectionNoteTakingViewDivider.tsx index 7d31b3193..8d659f790 100644 --- a/src/client/views/collections/CollectionNoteTakingViewDivider.tsx +++ b/src/client/views/collections/CollectionNoteTakingViewDivider.tsx @@ -1,5 +1,7 @@ import { action, observable } from 'mobx'; import * as React from 'react'; +import { emptyFunction, setupMoveUpEvents } from '../../../Utils'; +import { UndoManager } from '../../util/UndoManager'; interface DividerProps { index: number; @@ -7,34 +9,35 @@ interface DividerProps { setColumnStartXCoords: (movementX: number, colIndex: number) => void; } +/** + * CollectionNoteTakingViewDivider are dividers between CollectionNoteTakingViewColumns, + * which only appear when there is more than 1 column in CollectionNoteTakingView. Dividers + * are two simple vertical lines that allow the user to alter the widths of CollectionNoteTakingViewColumns. + */ export class CollectionNoteTakingViewDivider extends React.Component<DividerProps> { @observable private isHoverActive = false; @observable private isResizingActive = false; @action private registerResizing = (e: React.PointerEvent<HTMLDivElement>) => { - e.stopPropagation(); - e.preventDefault(); - window.removeEventListener('pointermove', this.onPointerMove); - window.removeEventListener('pointerup', this.onPointerUp); - window.addEventListener('pointermove', this.onPointerMove); - window.addEventListener('pointerup', this.onPointerUp); + const batch = UndoManager.StartBatch('resizing'); + setupMoveUpEvents( + this, + e, + (e, down, delta) => { + this.props.setColumnStartXCoords(delta[0], this.props.index); + return false; + }, + action(() => { + this.isResizingActive = false; + this.isHoverActive = false; + batch.end(); + }), + emptyFunction + ); this.isResizingActive = true; }; - @action - private onPointerUp = () => { - this.isResizingActive = false; - this.isHoverActive = false; - window.removeEventListener('pointermove', this.onPointerMove); - window.removeEventListener('pointerup', this.onPointerUp); - }; - - @action - onPointerMove = ({ movementX }: PointerEvent) => { - this.props.setColumnStartXCoords(movementX, this.props.index); - }; - render() { return ( <div diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index d4efef47a..71834607c 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -455,8 +455,6 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection newDocs.filter(ndoc => docs.indexOf(ndoc) !== -1).forEach(ndoc => docs.splice(docs.indexOf(ndoc), 1)); docs.splice(insertInd - offset, 0, ...newDocs); } - // reset drag manager docs, because we just dropped - DragManager.docsBeingDragged.length = 0; } } else if (de.complete.linkDragData?.dragDocument.context === this.props.Document && de.complete.linkDragData?.linkDragView?.props.CollectionFreeFormDocumentView?.()) { const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, _fitWidth: true, title: 'dropped annotation' }); @@ -493,13 +491,14 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection targInd = i; } }); - super.onExternalDrop(e, {}, () => { - if (targInd !== -1) { - const newDoc = this.childDocs[this.childDocs.length - 1]; - const docs = this.childDocList; - if (docs) { - docs.splice(docs.length - 1, 1); - docs.splice(targInd, 0, newDoc); + super.onExternalDrop(e, {}, (docs: Doc[]) => { + if (targInd === -1) { + this.addDocument(docs); + } + else { + const childDocs = this.childDocList; + if (childDocs) { + childDocs.splice(targInd, 0, ...docs); } } }); diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index e147f34d2..73574bdb3 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -242,43 +242,8 @@ export class TabDocView extends React.Component<TabDocViewProps> { const size: number = PresBox.Instance?._selectedArray.size; const presSelected: Doc | undefined = presArray && size ? presArray[size - 1] : undefined; const duration = NumCast(doc[`${Doc.LayoutFieldKey(pinDoc)}-duration`], null); - // If pinWithView option set then update scale and x / y props of slide - if (pinProps?.pinWithView) { - const viewProps = pinProps.pinWithView; - pinDoc.presPinView = true; - pinDoc.presPinViewX = viewProps.bounds.left + viewProps.bounds.width / 2; - pinDoc.presPinViewY = viewProps.bounds.top + viewProps.bounds.height / 2; - pinDoc.presPinViewScale = viewProps.scale; - pinDoc.contentBounds = new List<number>([viewProps.bounds.left, viewProps.bounds.top, viewProps.bounds.left + viewProps.bounds.width, viewProps.bounds.top + viewProps.bounds.height]); - } - if (pinProps?.pinDocView) { - const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(pinDoc.type as any) || pinDoc._viewType === CollectionViewType.Stacking; - const pannable: boolean = (pinDoc.type === DocumentType.COL && doc._viewType === CollectionViewType.Freeform) || doc.type === DocumentType.IMG; - if (scrollable) { - const scroll = doc._scrollTop; - pinDoc.presPinView = true; - pinDoc.presPinViewScroll = scroll; - } else if ([DocumentType.AUDIO, DocumentType.VID].includes(doc.type as any)) { - pinDoc.presPinView = true; - pinDoc.presStartTime = doc._currentTimecode; - pinDoc.presEndTime = NumCast(doc._currentTimecode) + 0.1; - } else if (pannable) { - pinDoc.presPinView = true; - pinDoc.presPinViewX = pinDoc._panX; - pinDoc.presPinViewY = pinDoc._panY; - pinDoc.presPinViewScale = pinDoc._viewScale; - const pw = NumCast(pinProps.panelWidth); - const ph = NumCast(pinProps.panelHeight); - const ps = NumCast(pinDoc._viewScale); - if (pw && ph && ps) { - pinDoc.contentBounds = new List<number>([NumCast(pinDoc.panX) - pw / 2 / ps, NumCast(pinDoc.panY) - ph / 2 / ps, NumCast(pinDoc.panX) + pw / 2 / ps, NumCast(pinDoc.panY) + ph / 2 / ps]); - } - } else if (doc.type === DocumentType.COMPARISON) { - const width = doc._clipWidth; - pinDoc.presPinClipWidth = width; - pinDoc.presPinView = true; - } - } + + PresBox.pinDocView(pinDoc, pinProps); pinDoc.onClick = ScriptField.MakeFunction('navigateToDoc(self.presentationTargetDoc, self)'); Doc.AddDocToList(curPres, 'data', pinDoc, presSelected); if (!pinProps?.audioRange && duration !== undefined) { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx index ee01c341b..89cc22d07 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx @@ -34,6 +34,7 @@ export interface PoolData { zIndex?: number; width?: number; height?: number; + backgroundColor?: string; color?: string; opacity?: number; transition?: string; @@ -45,7 +46,7 @@ export interface PoolData { export interface ViewDefResult { ele: JSX.Element; bounds?: ViewDefBounds; - inkMask?: boolean; + inkMask?: number; //sort elements into either the mask layer (which has a mixedBlendMode appropriate for transparent masks), or the regular documents layer; -1 = no mask, 0 = mask layer but stroke is transprent (hidden), >0 = mask layer and not hidden } function toLabel(target: FieldResult<Field>) { if (typeof target === 'number' || Number(target)) { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index 010132aa5..d80fcdfc3 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -20,15 +20,20 @@ pointer-events: none; } +.collectionfreeformview-mask-empty, .collectionfreeformview-mask { - mix-blend-mode: multiply; z-index: 5000; width: $INK_MASK_SIZE; height: $INK_MASK_SIZE; transform: translate($INK_MASK_SIZE_HALF, $INK_MASK_SIZE_HALF); - background-color: rgba(0, 0, 0, 0.7); pointer-events: none; position: absolute; + background-color: transparent; + transition: background-color 1s ease 0s; +} +.collectionfreeformview-mask { + mix-blend-mode: multiply; + background-color: rgba(0, 0, 0, 0.7); } .collectionfreeformview-viewdef { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 0fd326091..052cbd3bb 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -115,13 +115,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @observable _timelineRef = React.createRef<Timeline>(); @observable _marqueeRef = React.createRef<HTMLDivElement>(); @observable _marqueeViewRef = React.createRef<MarqueeView>(); - @observable _keyframeEditing = false; @observable ChildDrag: DocumentView | undefined; // child document view being dragged. needed to update drop areas of groups when a group item is dragged. @computed get views() { - const viewsMask = this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z && ele.inkMask).map(ele => ele.ele); - const renderableEles = this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z && !ele.inkMask).map(ele => ele.ele); - if (viewsMask.length) renderableEles.push(<div className="collectionfreeformview-mask">{viewsMask}</div>); + const viewsMask = this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z && ele.inkMask !== -1).map(ele => ele.ele); + const renderableEles = this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z && ele.inkMask === -1).map(ele => ele.ele); + if (viewsMask.length) renderableEles.push(<div className={`collectionfreeformview-mask${this._layoutElements.some(ele => (ele.inkMask ?? 0) > 0) ? '' : '-empty'}`}>{viewsMask}</div>); return renderableEles; } @computed get fitToContentVals() { @@ -185,8 +184,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this.Document.lastFrame = Math.max(NumCast(this.Document._currentFrame), NumCast(this.Document.lastFrame)); } }; - @action setKeyFrameEditing = (set: boolean) => (this._keyframeEditing = set); - getKeyFrameEditing = () => this._keyframeEditing; onBrowseClickHandler = () => this.props.onBrowseClick?.() || ScriptCast(this.layoutDoc.onBrowseClick); onChildClickHandler = () => this.props.childClickScript || ScriptCast(this.Document.onChildClick); onChildDoubleClickHandler = () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); @@ -251,7 +248,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; isCurrent(doc: Doc) { - if (doc.isInkMask && doc.opacity === 0) return false; // bcz: hacky --- allows inkMasks to be "turned off" in a presentation without removing them from the collection. otherwise, they still render a gray background.. need to come back to this and fix. const dispTime = NumCast(doc._timecodeToShow, -1); const endTime = NumCast(doc._timecodeToHide, dispTime + 1.5); const curTime = NumCast(this.Document._currentTimecode, -1); @@ -273,15 +269,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection .sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex)); zsorted.forEach((doc, index) => (doc.zIndex = doc.isInkMask ? 5000 : index + 1)); const dvals = CollectionFreeFormDocumentView.getValues(refDoc, NumCast(refDoc.activeFrame, 1000)); - const dropPos = this.Document._currentFrame !== undefined ? [dvals.x || 0, dvals.y || 0] : [NumCast(refDoc.x), NumCast(refDoc.y)]; + const dropPos = this.Document._currentFrame !== undefined ? [NumCast(dvals.x), NumCast(dvals.y)] : [NumCast(refDoc.x), NumCast(refDoc.y)]; for (let i = 0; i < docDragData.droppedDocuments.length; i++) { const d = docDragData.droppedDocuments[i]; const layoutDoc = Doc.Layout(d); if (this.Document._currentFrame !== undefined) { CollectionFreeFormDocumentView.setupKeyframes([d], NumCast(this.Document._currentFrame), false); const vals = CollectionFreeFormDocumentView.getValues(d, NumCast(d.activeFrame, 1000)); - vals.x = x + (vals.x || 0) - dropPos[0]; - vals.y = y + (vals.y || 0) - dropPos[1]; + vals.x = x + NumCast(vals.x) - dropPos[0]; + vals.y = y + NumCast(vals.y) - dropPos[1]; vals._scrollTop = this.Document.editScrollProgressivize ? vals._scrollTop : undefined; CollectionFreeFormDocumentView.setValues(NumCast(this.Document._currentFrame), d, vals); } else { @@ -1318,22 +1314,23 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }); getCalculatedPositions(params: { pair: { layout: Doc; data?: Doc }; index: number; collection: Doc }): PoolData { - const layoutDoc = Doc.Layout(params.pair.layout); - const { z, color, zIndex } = params.pair.layout; - const { x, y, opacity } = - this.Document._currentFrame === undefined - ? { x: params.pair.layout.x, y: params.pair.layout.y, opacity: this.props.childOpacity ? this.props.childOpacity() : this.props.styleProvider?.(params.pair.layout, this.props, StyleProp.Opacity) } - : CollectionFreeFormDocumentView.getValues(params.pair.layout, NumCast(this.Document._currentFrame)); + const curFrame = Cast(this.Document._currentFrame, 'number'); + const childDoc = params.pair.layout; + const childDocLayout = Doc.Layout(childDoc); + const { z, zIndex } = childDoc; + const { backgroundColor, color } = curFrame === undefined ? { backgroundColor: undefined, color: undefined } : CollectionFreeFormDocumentView.getStringValues(childDoc, curFrame); + const { x, y, opacity } = curFrame === undefined ? { x: childDoc.x, y: childDoc.y, opacity: this.props.childOpacity?.() } : CollectionFreeFormDocumentView.getValues(childDoc, curFrame); return { x: NumCast(x), y: NumCast(y), z: Cast(z, 'number'), - color: StrCast(color), + color: Cast(color, 'string') ? StrCast(color) : this.props.styleProvider?.(childDoc, this.props, StyleProp.Color), + backgroundColor: Cast(backgroundColor, 'string') ? StrCast(backgroundColor) : this.props.styleProvider?.(childDoc, this.props, StyleProp.BackgroundColor), + opacity: Cast(opacity, 'number') ?? this.props.styleProvider?.(childDoc, this.props, StyleProp.Opacity), zIndex: Cast(zIndex, 'number'), - transition: StrCast(layoutDoc.dataTransition), - opacity: this._keyframeEditing ? 1 : Cast(opacity, 'number', null), - width: Cast(layoutDoc._width, 'number'), - height: Cast(layoutDoc._height, 'number'), + width: Cast(childDocLayout._width, 'number'), + height: Cast(childDocLayout._height, 'number'), + transition: StrCast(childDocLayout.dataTransition), pair: params.pair, replica: '', }; @@ -1448,7 +1445,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection for (const entry of array) { const lastPos = this._cachedPool.get(entry[0]); // last computed pos const newPos = entry[1]; - if (!lastPos || newPos.opacity !== lastPos.opacity || newPos.x !== lastPos.x || newPos.y !== lastPos.y || newPos.z !== lastPos.z || newPos.zIndex !== lastPos.zIndex) { + 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.zIndex !== lastPos.zIndex + ) { this._layoutPoolData.set(entry[0], newPos); } if (!lastPos || newPos.height !== lastPos.height || newPos.width !== lastPos.width) { @@ -1465,7 +1471,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection elements.push({ ele: this.getChildDocView(entry[1]), bounds: this.childDataProvider(entry[1].pair.layout, entry[1].replica), - inkMask: BoolCast(entry[1].pair.layout.isInkMask), + inkMask: BoolCast(entry[1].pair.layout.isInkMask) ? NumCast(entry[1].pair.layout.opacity) : -1, }) ); @@ -2207,3 +2213,6 @@ ScriptingGlobals.add(function nextKeyFrame(readOnly: boolean) { ScriptingGlobals.add(function prevKeyFrame(readOnly: boolean) { !readOnly && (SelectionManager.Views()[0].ComponentView as CollectionFreeFormView)?.changeKeyFrame(true); }); +ScriptingGlobals.add(function pinWithView(readOnly: boolean) { + !readOnly && SelectionManager.Views().forEach(view => TabDocView.PinDoc(view.rootDoc, { pinDocView: true, panelWidth: view.props.PanelWidth(), panelHeight: view.props.PanelHeight() })); +}); diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index e19e2d525..86566ac6a 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -16,9 +16,14 @@ import { StyleProp } from '../StyleProvider'; import './CollectionFreeFormDocumentView.scss'; import { DocumentView, DocumentViewProps } from './DocumentView'; import React = require('react'); +import { InkField } from '../../../fields/InkField'; +import { DocumentType } from '../../documents/DocumentTypes'; +import { Field } from '../../util/ProsemirrorCopy/prompt'; +import { RefField } from '../../../fields/RefField'; +import { ObjectField } from '../../../fields/ObjectField'; export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { - dataProvider?: (doc: Doc, replica: string) => { x: number; y: number; zIndex?: number; opacity?: number; highlight?: boolean; z: number; transition?: string } | undefined; + dataProvider?: (doc: Doc, replica: string) => { x: number; y: number; zIndex?: number; color?: string; backgroundColor?: string; opacity?: number; highlight?: boolean; z: number; transition?: string } | undefined; sizeProvider?: (doc: Doc, replica: string) => { width: number; height: number } | undefined; renderCutoffProvider: (doc: Doc) => boolean; zIndex?: number; @@ -32,32 +37,39 @@ export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { @observer export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps>() { public static animFields = ['_height', '_width', 'x', 'y', '_scrollTop', 'opacity']; // fields that are configured to be animatable using animation frames + public static animStringFields = ['backgroundColor', 'color']; // fields that are configured to be animatable using animation frames + public static animDataFields = ['data', 'text']; // fields that are configured to be animatable using animation frames @observable _animPos: number[] | undefined = undefined; @observable _contentView: DocumentView | undefined | null; get displayName() { + // this makes mobx trace() statements more descriptive return 'CollectionFreeFormDocumentView(' + this.rootDoc.title + ')'; - } // this makes mobx trace() statements more descriptive + } + get transform() { return `translate(${this.X}px, ${this.Y}px) rotate(${NumCast(this.Document.jitterRotation, this.props.jitterRotation)}deg)`; } get X() { - return this.dataProvider ? this.dataProvider.x : NumCast(this.Document.x); + return this.dataProvider?.x ?? NumCast(this.Document.x); } get Y() { - return this.dataProvider ? this.dataProvider.y : NumCast(this.Document.y); + return this.dataProvider?.y ?? NumCast(this.Document.y); } get ZInd() { - return this.dataProvider ? this.dataProvider.zIndex : NumCast(this.Document.zIndex); + return this.dataProvider?.zIndex ?? NumCast(this.Document.zIndex); } get Opacity() { - return this.dataProvider ? this.dataProvider.opacity : undefined; + return this.dataProvider?.opacity; + } + get BackgroundColor() { + return this.dataProvider?.backgroundColor; + } + get Color() { + return this.dataProvider?.color; } get Highlight() { return this.dataProvider?.highlight; } - @computed get ShowTitle() { - return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.ShowTitle) as Opt<string>; - } @computed get dataProvider() { return this.props.dataProvider?.(this.props.Document, this.props.replica); } @@ -66,7 +78,13 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF } styleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string) => { - if (property === StyleProp.Opacity && doc === this.layoutDoc) return this.Opacity; // only change the opacity for this specific document, not its children + if (doc === this.layoutDoc) + // prettier-ignore + switch (property) { + case StyleProp.Opacity: return this.Opacity; // only change the opacity for this specific document, not its children + case StyleProp.BackgroundColor: return this.BackgroundColor; + case StyleProp.Color: return this.Color; + } return this.props.styleProvider?.(doc, props, property); }; @@ -77,6 +95,13 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF }, {} as { [val: string]: Opt<number> }); } + public static getStringValues(doc: Doc, time: number) { + return CollectionFreeFormDocumentView.animStringFields.reduce((p, val) => { + p[val] = Cast(`${val}-indexed`, listSpec('string'), [StrCast(doc[val])]).reduce((p, v, i) => ((i <= Math.round(time) && v !== undefined) || p === undefined ? v : p), undefined as any as string); + return p; + }, {} as { [val: string]: Opt<string> }); + } + public static setValues(time: number, d: Doc, vals: { [val: string]: Opt<number> }) { const timecode = Math.round(time); Object.keys(vals).forEach(val => { @@ -95,6 +120,14 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF const findexed = Cast(doc[`${val}-indexed`], listSpec('number'), null); findexed?.length <= timecode + 1 && findexed.push(undefined as any as number); }); + CollectionFreeFormDocumentView.animStringFields.forEach(val => { + const findexed = Cast(doc[`${val}-indexed`], listSpec('string'), null); + findexed?.length <= timecode + 1 && findexed.push(undefined as any as string); + }); + CollectionFreeFormDocumentView.animDataFields.forEach(val => { + const findexed = Cast(doc[`${val}-indexed`], listSpec(InkField), null); + findexed?.length <= timecode + 1 && findexed.push(undefined as any); + }); }) ); setTimeout( @@ -141,9 +174,13 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF // opacity is unlike other fields because it's value should not be undefined before it appears to enable it to fade-in doc['opacity-indexed'] = new List<number>(numberRange(currTimecode + 1).map(t => (!doc.z && makeAppear && t < NumCast(doc.appearFrame) ? 0 : 1))); } - CollectionFreeFormDocumentView.animFields.forEach(val => (doc[val] = ComputedField.MakeInterpolated(val, 'activeFrame', doc, currTimecode))); - doc.activeFrame = ComputedField.MakeFunction('self.context?._currentFrame||0'); - doc.dataTransition = 'inherit'; + CollectionFreeFormDocumentView.animFields.forEach(val => (doc[val] = ComputedField.MakeInterpolatedNumber(val, 'activeFrame', doc, currTimecode))); + CollectionFreeFormDocumentView.animStringFields.forEach(val => (doc[val] = ComputedField.MakeInterpolatedString(val, 'activeFrame', doc, currTimecode))); + CollectionFreeFormDocumentView.animDataFields.forEach(val => (Doc.GetProto(doc)[val] = ComputedField.MakeInterpolatedDataField(val, 'activeFrame', Doc.GetProto(doc), currTimecode))); + const targetDoc = doc.type === DocumentType.RTF ? Doc.GetProto(doc) : doc; // data fields, like rtf 'text' exist on the data doc, so + doc !== targetDoc && (targetDoc.context = doc.context); // the computed fields don't see the layout doc -- need to copy the context to the data doc (HACK!!!) and set the activeFrame on the data doc (HACK!!!) + targetDoc.activeFrame = ComputedField.MakeFunction('self.context?._currentFrame||0'); + targetDoc.dataTransition = 'inherit'; }); } @@ -189,7 +226,6 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF PanelWidth: this.panelWidth, PanelHeight: this.panelHeight, }; - const background = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); const mixBlendMode = undefined; // (StrCast(this.layoutDoc.mixBlendMode) as any) || (typeof background === 'string' && background && !background.startsWith('linear') && DashColor(background).alpha() !== 1 ? 'multiply' : undefined); return ( <div diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 381436a56..d065c62fb 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -25,7 +25,7 @@ import './DocumentView.scss'; import { EquationBox } from './EquationBox'; import { FieldView, FieldViewProps } from './FieldView'; import { FilterBox } from './FilterBox'; -import { FormattedTextBox, FormattedTextBoxProps } from './formattedText/FormattedTextBox'; +import { FormattedTextBox } from './formattedText/FormattedTextBox'; import { FunctionPlotBox } from './FunctionPlotBox'; import { ImageBox } from './ImageBox'; import { KeyValueBox } from './KeyValueBox'; @@ -114,14 +114,13 @@ export class HTMLtag extends React.Component<HTMLtagProps> { @observer export class DocumentContentsView extends React.Component< - DocumentViewProps & - FormattedTextBoxProps & { - isSelected: (outsideReaction: boolean) => boolean; - select: (ctrl: boolean) => void; - NativeDimScaling?: () => number; - setHeight?: (height: number) => void; - layoutKey: string; - } + DocumentViewProps & { + isSelected: (outsideReaction: boolean) => boolean; + select: (ctrl: boolean) => void; + NativeDimScaling?: () => number; + setHeight?: (height: number) => void; + layoutKey: string; + } > { @computed get layout(): string { TraceMobx(); diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index 6ea697a2f..9aaaf1e68 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -99,6 +99,7 @@ border-radius: inherit; width: 100%; height: 100%; + transition: inherit; .sharingIndicator { height: 30px; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 172adcafe..74143a731 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1656,7 +1656,6 @@ export class DocumentView extends React.Component<DocumentViewProps> { ref={this.ContentRef} style={{ transition: this.props.dataTransition, - //position: this.props.Document.isInkMask ? 'absolute' : undefined, transform: isButton ? undefined : `translate(${this.centeringX}px, ${this.centeringY}px)`, width: isButton || isPresTreeElement ? '100%' : xshift() ?? `${(100 * (this.props.PanelWidth() - this.Xshift * 2)) / this.props.PanelWidth()}%`, height: diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index dd2c13391..77aaa4441 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -26,16 +26,21 @@ export interface FieldViewProps extends DocumentViewSharedProps { NativeDimScaling?: () => number; // scaling the DocumentView does to transform its contents into its panel & needed by ScreenToLocal NOTE: Must also be added to DocumentViewInternalsProps onBrowseClick?: () => ScriptField | undefined; onKey?: (e: React.KeyboardEvent, fieldProps: FieldViewProps) => boolean | undefined; + pointerEvents?: () => Opt<string>; // properties intended to be used from within layout strings (otherwise use the function equivalents that work more efficiently with React) - pointerEvents?: () => Opt<string>; + // See currentUserUtils headerTemplate for examples of creating text boxes from html which set some of these fields + // Also, see InkingStroke for examples of creating text boxes from render() methods which set some of these fields + backgroundColor?: string; + color?: string; fontSize?: number; height?: number; width?: number; - background?: string; - color?: string; xPadding?: number; yPadding?: number; + noSidebar?: boolean; + dontScale?: boolean; + dontSelectOnLoad?: boolean; // suppress selecting (e.g.,. text box) when loaded (and mark as not being associated with scrollTop document field) } @observer diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index 4b1fbaf7d..d9f46509e 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -1,34 +1,37 @@ - -import { action, computed, observable } from "mobx"; -import { observer } from "mobx-react"; -import { Doc, Field, FieldResult } from "../../../fields/Doc"; -import { List } from "../../../fields/List"; -import { RichTextField } from "../../../fields/RichTextField"; -import { listSpec } from "../../../fields/Schema"; -import { ComputedField, ScriptField } from "../../../fields/ScriptField"; -import { Cast, FieldValue, NumCast } from "../../../fields/Types"; -import { ImageField } from "../../../fields/URLField"; -import { Docs } from "../../documents/Documents"; -import { SetupDrag } from "../../util/DragManager"; -import { CompiledScript, CompileScript, ScriptOptions } from "../../util/Scripting"; -import { undoBatch } from "../../util/UndoManager"; +import { action, computed, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import { Doc, Field, FieldResult } from '../../../fields/Doc'; +import { List } from '../../../fields/List'; +import { RichTextField } from '../../../fields/RichTextField'; +import { listSpec } from '../../../fields/Schema'; +import { ComputedField, ScriptField } from '../../../fields/ScriptField'; +import { Cast, DocCast, FieldValue, NumCast } from '../../../fields/Types'; +import { ImageField } from '../../../fields/URLField'; +import { Docs } from '../../documents/Documents'; +import { SetupDrag } from '../../util/DragManager'; +import { CompiledScript, CompileScript, ScriptOptions } from '../../util/Scripting'; +import { undoBatch } from '../../util/UndoManager'; import { FieldView, FieldViewProps } from './FieldView'; -import "./KeyValueBox.scss"; -import { KeyValuePair } from "./KeyValuePair"; -import React = require("react"); -import { ContextMenu } from "../ContextMenu"; -import { ContextMenuProps } from "../ContextMenuItem"; -import e = require("express"); +import './KeyValueBox.scss'; +import { KeyValuePair } from './KeyValuePair'; +import React = require('react'); +import { ContextMenu } from '../ContextMenu'; +import { ContextMenuProps } from '../ContextMenuItem'; +import e = require('express'); +import { FormattedTextBox } from './formattedText/FormattedTextBox'; +import { ImageBox } from './ImageBox'; export type KVPScript = { script: CompiledScript; - type: "computed" | "script" | false; + type: 'computed' | 'script' | false; onDelegate: boolean; }; @observer export class KeyValueBox extends React.Component<FieldViewProps> { - public static LayoutString(fieldStr: string) { return FieldView.LayoutString(KeyValueBox, fieldStr); } + public static LayoutString(fieldStr: string) { + return FieldView.LayoutString(KeyValueBox, fieldStr); + } private _mainCont = React.createRef<HTMLDivElement>(); private _keyHeader = React.createRef<HTMLTableHeaderCellElement>(); @@ -37,8 +40,12 @@ export class KeyValueBox extends React.Component<FieldViewProps> { @observable private rows: KeyValuePair[] = []; - @computed get splitPercentage() { return NumCast(this.props.Document.schemaSplitPercentage, 50); } - get fieldDocToLayout() { return this.props.fieldKey ? Cast(this.props.Document[this.props.fieldKey], Doc, null) : this.props.Document; } + @computed get splitPercentage() { + return NumCast(this.props.Document.schemaSplitPercentage, 50); + } + get fieldDocToLayout() { + return this.props.fieldKey ? Cast(this.props.Document[this.props.fieldKey], Doc, null) : this.props.Document; + } @action onEnterKey = (e: React.KeyboardEvent): void => { @@ -46,19 +53,19 @@ export class KeyValueBox extends React.Component<FieldViewProps> { e.stopPropagation(); if (this._keyInput.current?.value && this._valInput.current?.value && this.fieldDocToLayout) { if (KeyValueBox.SetField(this.fieldDocToLayout, this._keyInput.current.value, this._valInput.current.value)) { - this._keyInput.current.value = ""; - this._valInput.current.value = ""; + this._keyInput.current.value = ''; + this._valInput.current.value = ''; document.body.focus(); } } } - } + }; public static CompileKVPScript(value: string): KVPScript | undefined { - const eq = value.startsWith("="); + const eq = value.startsWith('='); value = eq ? value.substr(1) : value; - const dubEq = value.startsWith(":=") ? "computed" : value.startsWith(";=") ? "script" : false; + const dubEq = value.startsWith(':=') ? 'computed' : value.startsWith(';=') ? 'script' : false; value = dubEq ? value.substr(2) : value; - const options: ScriptOptions = { addReturn: true, params: { this: Doc.name, self: Doc.name, _last_: "any", _readOnly_: "boolean" }, editable: false }; + const options: ScriptOptions = { addReturn: true, params: { this: Doc.name, self: Doc.name, _last_: 'any', _readOnly_: 'boolean' }, editable: false }; if (dubEq) options.typecheck = false; const script = CompileScript(value, options); return !script.compiled ? undefined : { script, type: dubEq, onDelegate: eq }; @@ -67,11 +74,11 @@ export class KeyValueBox extends React.Component<FieldViewProps> { public static ApplyKVPScript(doc: Doc, key: string, kvpScript: KVPScript, forceOnDelegate?: boolean): boolean { const { script, type, onDelegate } = kvpScript; //const target = onDelegate ? Doc.Layout(doc.layout) : Doc.GetProto(doc); // bcz: TODO need to be able to set fields on layout templates - const target = forceOnDelegate || onDelegate || key.startsWith("_") ? doc : doc.proto || doc; + const target = forceOnDelegate || onDelegate || key.startsWith('_') ? doc : doc.proto || doc; let field: Field; - if (type === "computed") { + if (type === 'computed') { field = new ComputedField(script); - } else if (type === "script") { + } else if (type === 'script') { field = new ScriptField(script); } else { const res = script.run({ this: target }, console.log); @@ -96,7 +103,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { if (e.buttons === 1 && this.props.isSelected(true)) { e.stopPropagation(); } - } + }; onPointerWheel = (e: React.WheelEvent): void => e.stopPropagation(); rowHeight = () => 30; @@ -104,7 +111,11 @@ export class KeyValueBox extends React.Component<FieldViewProps> { @computed get createTable() { const doc = this.fieldDocToLayout; if (!doc) { - return <tr><td>Loading...</td></tr>; + return ( + <tr> + <td>Loading...</td> + </tr> + ); } const realDoc = doc; @@ -122,83 +133,102 @@ export class KeyValueBox extends React.Component<FieldViewProps> { let i = 0; const self = this; for (const key of Object.keys(ids).slice().sort()) { - rows.push(<KeyValuePair doc={realDoc} addDocTab={this.props.addDocTab} PanelWidth={this.props.PanelWidth} PanelHeight={this.rowHeight} - ref={(function () { - let oldEl: KeyValuePair | undefined; - return (el: KeyValuePair) => { - if (oldEl) self.rows.splice(self.rows.indexOf(oldEl), 1); - oldEl = el; - if (el) self.rows.push(el); - }; - })()} keyWidth={100 - this.splitPercentage} rowStyle={"keyValueBox-" + (i++ % 2 ? "oddRow" : "evenRow")} key={key} keyName={key} />); + rows.push( + <KeyValuePair + doc={realDoc} + addDocTab={this.props.addDocTab} + PanelWidth={this.props.PanelWidth} + PanelHeight={this.rowHeight} + ref={(function () { + let oldEl: KeyValuePair | undefined; + return (el: KeyValuePair) => { + if (oldEl) self.rows.splice(self.rows.indexOf(oldEl), 1); + oldEl = el; + if (el) self.rows.push(el); + }; + })()} + keyWidth={100 - this.splitPercentage} + rowStyle={'keyValueBox-' + (i++ % 2 ? 'oddRow' : 'evenRow')} + key={key} + keyName={key} + /> + ); } return rows; } @computed get newKeyValue() { - return <tr className="keyValueBox-valueRow"> - <td className="keyValueBox-td-key" onClick={(e) => { this._keyInput.current!.select(); e.stopPropagation(); }} style={{ width: `${100 - this.splitPercentage}%` }}> - <input style={{ width: "100%" }} ref={this._keyInput} type="text" placeholder="Key" /> - </td> - <td className="keyValueBox-td-value" onClick={(e) => { this._valInput.current!.select(); e.stopPropagation(); }} style={{ width: `${this.splitPercentage}%` }}> - <input style={{ width: "100%" }} ref={this._valInput} type="text" placeholder="Value" onKeyDown={this.onEnterKey} /> - </td> - </tr>; + return ( + <tr className="keyValueBox-valueRow"> + <td + className="keyValueBox-td-key" + onClick={e => { + this._keyInput.current!.select(); + e.stopPropagation(); + }} + style={{ width: `${100 - this.splitPercentage}%` }}> + <input style={{ width: '100%' }} ref={this._keyInput} type="text" placeholder="Key" /> + </td> + <td + className="keyValueBox-td-value" + onClick={e => { + this._valInput.current!.select(); + e.stopPropagation(); + }} + style={{ width: `${this.splitPercentage}%` }}> + <input style={{ width: '100%' }} ref={this._valInput} type="text" placeholder="Value" onKeyDown={this.onEnterKey} /> + </td> + </tr> + ); } @action onDividerMove = (e: PointerEvent): void => { const nativeWidth = this._mainCont.current!.getBoundingClientRect(); - this.props.Document.schemaSplitPercentage = Math.max(0, 100 - Math.round((e.clientX - nativeWidth.left) / nativeWidth.width * 100)); - } + this.props.Document.schemaSplitPercentage = Math.max(0, 100 - Math.round(((e.clientX - nativeWidth.left) / nativeWidth.width) * 100)); + }; @action onDividerUp = (e: PointerEvent): void => { - document.removeEventListener("pointermove", this.onDividerMove); + document.removeEventListener('pointermove', this.onDividerMove); document.removeEventListener('pointerup', this.onDividerUp); - } + }; onDividerDown = (e: React.PointerEvent) => { e.stopPropagation(); e.preventDefault(); - document.addEventListener("pointermove", this.onDividerMove); + document.addEventListener('pointermove', this.onDividerMove); document.addEventListener('pointerup', this.onDividerUp); - } + }; - getTemplate = async () => { - const parent = Docs.Create.StackingDocument([], { _width: 800, _height: 800, title: "Template", _chromeHidden: true }); - parent._columnWidth = 100; - for (const row of this.rows.filter(row => row.isChecked)) { - await this.createTemplateField(parent, row); - row.uncheck(); + getFieldView = async () => { + const rows = this.rows.filter(row => row.isChecked); + if (rows.length > 1) { + const parent = Docs.Create.StackingDocument([], { _autoHeight: true, _width: 300, title: `field views for ${DocCast(this.props.Document.data).title}`, _chromeHidden: true }); + for (const row of rows) { + const field = this.createFieldView(DocCast(this.props.Document.data), row); + field && Doc.AddDocToList(parent, 'data', field); + row.uncheck(); + } + return parent; } - return parent; - } + return this.createFieldView(DocCast(this.props.Document.data), rows.lastElement()); + }; - createTemplateField = async (parentStackingDoc: Doc, row: KeyValuePair) => { + createFieldView = (templateDoc: Doc, row: KeyValuePair) => { const metaKey = row.props.keyName; - const sourceDoc = await Cast(this.props.Document.data, Doc); - if (!sourceDoc) { - return; - } + const fieldTemplate = Doc.MakeAlias(templateDoc); + fieldTemplate.title = metaKey; + fieldTemplate.layout = this.inferType(templateDoc[metaKey], metaKey); + return fieldTemplate; + }; - const fieldTemplate = await this.inferType(sourceDoc[metaKey], metaKey); - if (!fieldTemplate) { - return; - } - const previousViewType = fieldTemplate._viewType; - Doc.MakeMetadataFieldTemplate(fieldTemplate, Doc.GetProto(parentStackingDoc)); - previousViewType && (fieldTemplate._viewType = previousViewType); - - Cast(parentStackingDoc.data, listSpec(Doc))!.push(fieldTemplate); - } - - inferType = async (data: FieldResult, metaKey: string) => { + inferType = (data: FieldResult, metaKey: string) => { const options = { _width: 300, _height: 300, title: metaKey }; - if (data instanceof RichTextField || typeof data === "string" || typeof data === "number") { - return Docs.Create.TextDocument("", options); + if (data instanceof RichTextField || typeof data === 'string' || typeof data === 'number') { + return FormattedTextBox.LayoutString(metaKey); } else if (data instanceof List) { if (data.length === 0) { return Docs.Create.StackingDocument([], options); } - const first = await Cast(data[0], Doc); + const first = DocCast(data[0]); if (!first || !first.data) { return Docs.Create.StackingDocument([], options); } @@ -212,44 +242,52 @@ export class KeyValueBox extends React.Component<FieldViewProps> { return undefined; } } else if (data instanceof ImageField) { - return Docs.Create.ImageDocument("https://image.flaticon.com/icons/png/512/23/23765.png", options); + return ImageBox.LayoutString(metaKey); } - return new Doc; - } + return new Doc(); + }; specificContextMenu = (e: React.MouseEvent): void => { const cm = ContextMenu.Instance; - const open = cm.findByDescription("Change Perspective..."); - const openItems: ContextMenuProps[] = open && "subitems" in open ? open.subitems : []; + const open = cm.findByDescription('Change Perspective...'); + const openItems: ContextMenuProps[] = open && 'subitems' in open ? open.subitems : []; openItems.push({ - description: "Default Perspective", event: () => { - this.props.addDocTab(this.props.Document, "close"); - this.props.addDocTab(this.fieldDocToLayout, "add:right"); - }, icon: "image" + description: 'Default Perspective', + event: () => { + this.props.addDocTab(this.props.Document, 'close'); + this.props.addDocTab(this.fieldDocToLayout, 'add:right'); + }, + icon: 'image', }); - !open && cm.addItem({ description: "Change Perspective...", subitems: openItems, icon: "external-link-alt" }); - } + !open && cm.addItem({ description: 'Change Perspective...', subitems: openItems, icon: 'external-link-alt' }); + }; render() { - const dividerDragger = this.splitPercentage === 0 ? (null) : - <div className="keyValueBox-dividerDragger" style={{ transform: `translate(calc(${100 - this.splitPercentage}% - 5px), 0px)` }}> - <div className="keyValueBox-dividerDraggerThumb" onPointerDown={this.onDividerDown} /> - </div>; - - return (<div className="keyValueBox-cont" onWheel={this.onPointerWheel} onContextMenu={this.specificContextMenu} ref={this._mainCont}> - <table className="keyValueBox-table"> - <tbody className="keyValueBox-tbody"> - <tr className="keyValueBox-header"> - <th className="keyValueBox-key" style={{ width: `${100 - this.splitPercentage}%` }} ref={this._keyHeader} - onPointerDown={SetupDrag(this._keyHeader, this.getTemplate)} - >Key</th> - <th className="keyValueBox-fields" style={{ width: `${this.splitPercentage}%` }}>Fields</th> - </tr> - {this.createTable} - {this.newKeyValue} - </tbody> - </table> - {dividerDragger} - </div>); + const dividerDragger = + this.splitPercentage === 0 ? null : ( + <div className="keyValueBox-dividerDragger" style={{ transform: `translate(calc(${100 - this.splitPercentage}% - 5px), 0px)` }}> + <div className="keyValueBox-dividerDraggerThumb" onPointerDown={this.onDividerDown} /> + </div> + ); + + return ( + <div className="keyValueBox-cont" onWheel={this.onPointerWheel} onContextMenu={this.specificContextMenu} ref={this._mainCont}> + <table className="keyValueBox-table"> + <tbody className="keyValueBox-tbody"> + <tr className="keyValueBox-header"> + <th className="keyValueBox-key" style={{ width: `${100 - this.splitPercentage}%` }} ref={this._keyHeader} onPointerDown={SetupDrag(this._keyHeader, this.getFieldView)}> + Key + </th> + <th className="keyValueBox-fields" style={{ width: `${this.splitPercentage}%` }}> + Fields + </th> + </tr> + {this.createTable} + {this.newKeyValue} + </tbody> + </table> + {dividerDragger} + </div> + ); } } diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx index cb68c1ac3..c72b5ca9b 100644 --- a/src/client/views/nodes/button/FontIconBox.tsx +++ b/src/client/views/nodes/button/FontIconBox.tsx @@ -90,9 +90,11 @@ export class FontIconBox extends DocComponent<ButtonProps>() { @computed get label() { return StrCast(this.rootDoc.label, StrCast(this.rootDoc.title)); } - @computed get icon() { - return StrCast(this.dataDoc.icon, 'user') as any; - } + Icon = (color: string) => { + const icon = StrCast(this.dataDoc.icon, 'user') as any; + const trailsIcon = () => <img src={`/assets/${'presTrails.png'}`} style={{ width: 30, height: 30, filter: `invert(${color === Colors.DARK_GRAY ? '0%' : '100%'})` }} />; + return !icon ? null : icon === 'pres-trail' ? trailsIcon() : <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={icon} color={color} />; + }; @computed get dropdown() { return BoolCast(this.rootDoc.dropDownOpen); } @@ -228,7 +230,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { className={`menuButton ${this.type} ${active}`} style={{ color: color, backgroundColor: backgroundColor, borderBottomLeftRadius: this.dropdown ? 0 : undefined }} onClick={action(() => (this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen))}> - <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} /> + {this.Icon(color)} {!this.label || !FontIconBox.GetShowLabels() ? null : ( <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor }}> {' '} @@ -252,16 +254,13 @@ export class FontIconBox extends DocComponent<ButtonProps>() { const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); const script = ScriptCast(this.rootDoc.script); - if (!script) { - return null; - } let noviceList: string[] = []; let text: string | undefined; let dropdown = true; let icon: IconProp = 'caret-down'; try { - if (script.script.originalScript.startsWith('setView')) { + if (script?.script.originalScript.startsWith('setView')) { const selected = SelectionManager.Docs().lastElement(); if (selected) { if (StrCast(selected.type) === DocumentType.COL) { @@ -277,7 +276,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { text = 'User Default'; } noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Stacking]; - } else if (script.script.originalScript.startsWith('setFont')) { + } else if (script?.script.originalScript.startsWith('setFont')) { const editorView = RichTextMenu.Instance?.TextView?.EditorView; text = StrCast((editorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily); noviceList = ['Roboto', 'Times New Roman', 'Arial', 'Georgia', 'Comic Sans MS', 'Tahoma', 'Impact', 'Crimson Text']; @@ -287,11 +286,9 @@ export class FontIconBox extends DocComponent<ButtonProps>() { } // Get items to place into the list - const list = this.buttonList.map(value => { - if (Doc.noviceMode && !noviceList.includes(value)) { - return; - } - return ( + const list = this.buttonList + .filter(value => !Doc.noviceMode || noviceList.includes(value)) + .map(value => ( <div className="list-item" key={`${value}`} @@ -302,8 +299,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { onClick={() => script.script.run({ value }).result}> {value[0].toUpperCase() + value.slice(1)} </div> - ); - }); + )); const label = !this.label || !FontIconBox.GetShowLabels() ? null : ( @@ -384,7 +380,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { style={{ color: color, borderBottomLeftRadius: this.dropdown ? 0 : undefined }} onClick={action(() => (this.colorPickerClosed = !this.colorPickerClosed))} onPointerDown={e => e.stopPropagation()}> - <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} /> + {this.Icon(color)} <div className="colorButton-color" style={{ backgroundColor: curColor }} /> {label} {/* {dropdownCaret} */} @@ -436,7 +432,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { } else { return ( <div className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? 'Label' : '')}`} style={{ opacity: 1, backgroundColor, color }}> - <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} /> + {this.Icon(color)} {label} </div> ); @@ -449,11 +445,10 @@ export class FontIconBox extends DocComponent<ButtonProps>() { @computed get defaultButton() { const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); - const active: string = StrCast(this.rootDoc.dropDownOpen); return ( <div className={`menuButton ${this.type}`} onContextMenu={this.specificContextMenu} style={{ backgroundColor: 'transparent', borderBottomLeftRadius: this.dropdown ? 0 : undefined }}> <div className="menuButton-wrap"> - <FontAwesomeIcon className={`menuButton-icon-${this.type}`} icon={this.icon} color={'black'} size={'sm'} /> + {this.Icon(color)} {!this.label || !FontIconBox.GetShowLabels() ? null : ( <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor }}> {' '} @@ -485,90 +480,50 @@ export class FontIconBox extends DocComponent<ButtonProps>() { render() { const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); - const label = - !this.label || !FontIconBox.GetShowLabels() ? null : ( - <div className="fontIconBox-label" style={{ color, backgroundColor }}> - {this.label} - </div> - ); - - const menuLabel = + const label = (noBackground: boolean = false) => !this.label || !FontIconBox.GetShowLabels() ? null : ( - <div className="fontIconBox-label" style={{ color, backgroundColor: 'transparent' }}> + <div className="fontIconBox-label" style={{ color, backgroundColor: noBackground ? 'transparent' : backgroundColor }}> {this.label} </div> ); - // TODO:glr Add label of button type - let button: JSX.Element | null = this.defaultButton; + let button: JSX.Element = this.defaultButton; + // prettier-ignore switch (this.type) { - case ButtonType.TextButton: - button = ( + case ButtonType.DropdownList: return this.dropdownListButton; + case ButtonType.ColorButton: return this.colorButton; + case ButtonType.NumberButton: return this.numberButton; + case ButtonType.EditableText: return this.editableText; + case ButtonType.DropdownButton: button = this.dropdownButton; break; + case ButtonType.ToggleButton: button = this.toggleButton; break; + case ButtonType.TextButton: button = ( <div className={`menuButton ${this.type}`} style={{ color, backgroundColor, opacity: 1, gridAutoColumns: `${NumCast(this.rootDoc._height)} auto` }}> - <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} /> + {this.Icon(color)} {StrCast(this.rootDoc.buttonText) ? <div className="button-text">{StrCast(this.rootDoc.buttonText)}</div> : null} - {label} + {label()} </div> ); - // button = <TextButton {...buttonProps}></TextButton> - break; - case ButtonType.EditableText: - button = this.editableText; - break; - case ButtonType.NumberButton: - button = this.numberButton; - break; - case ButtonType.DropdownButton: - button = this.dropdownButton; - break; - case ButtonType.DropdownList: - button = this.dropdownListButton; - break; - case ButtonType.ColorButton: - button = this.colorButton; - break; - case ButtonType.ToolButton: - button = ( - <div className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? 'Label' : '')}`} style={{ opacity: 1, backgroundColor, color }}> - <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} /> - {label} - </div> - ); - break; - case ButtonType.ToggleButton: - button = this.toggleButton; - // button = <ToggleButton {...buttonProps}></ToggleButton> break; case ButtonType.ClickButton: - button = ( - <div className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? 'Label' : '')}`} style={{ color, backgroundColor, opacity: 1 }}> - <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} /> - {label} + case ButtonType.ToolButton: button = ( + <div className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? 'Label' : '')}`} style={{ backgroundColor, color, opacity: 1 }}> + {this.Icon(color)} + {label()} </div> ); break; - case ButtonType.MenuButton: - const trailsIcon = <img src={`/assets/${'presTrails.png'}`} style={{ width: 30, height: 30, filter: `invert(${color === Colors.DARK_GRAY ? '0%' : '100%'})` }} />; - button = ( + case ButtonType.MenuButton: button = ( <div className={`menuButton ${this.type}`} style={{ color, backgroundColor }}> - {this.icon === 'pres-trail' ? trailsIcon : <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />} - {menuLabel} + {this.Icon(color)} + {label(true)} <FontIconBadge value={Cast(this.Document.badgeValue, 'string', null)} /> </div> ); break; - default: - break; } - const retval = - !this.layoutDoc.toolTip || this.type === ButtonType.DropdownList || this.type === ButtonType.ColorButton || this.type === ButtonType.NumberButton || this.type === ButtonType.EditableText ? ( - button - ) : button !== null ? ( - <Tooltip title={<div className="dash-tooltip">{StrCast(this.layoutDoc.toolTip)}</div>}>{button}</Tooltip> - ) : null; - return retval; + return !this.layoutDoc.toolTip ? button : <Tooltip title={<div className="dash-tooltip">{StrCast(this.layoutDoc.toolTip)}</div>}>{button}</Tooltip>; } } @@ -697,7 +652,7 @@ ScriptingGlobals.add(function setFontHighlight(color?: string, checkResult?: boo ScriptingGlobals.add(function setFontSize(size: string | number, checkResult?: boolean) { const editorView = RichTextMenu.Instance?.TextView?.EditorView; if (checkResult) { - return RichTextMenu.Instance.fontSize.replace('px', ''); + return RichTextMenu.Instance?.fontSize.replace('px', ''); } if (typeof size === 'number') size = size.toString(); if (size && Number(size).toString() === size) size += 'px'; diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index f61533619..81ac45521 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -11,7 +11,7 @@ import { Fragment, Mark, Node, Slice } from 'prosemirror-model'; import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; import { DateField } from '../../../../fields/DateField'; -import { AclAdmin, AclAugment, AclEdit, AclReadonly, AclSelfEdit, DataSym, Doc, DocListCast, DocListCastAsync, Field, ForceServerWrite, HeightSym, Opt, UpdatingFromServer, WidthSym } from '../../../../fields/Doc'; +import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DataSym, Doc, DocListCast, DocListCastAsync, Field, ForceServerWrite, HeightSym, Opt, UpdatingFromServer, WidthSym } from '../../../../fields/Doc'; import { Id } from '../../../../fields/FieldSymbols'; import { InkTool } from '../../../../fields/InkField'; import { PrefetchProxy } from '../../../../fields/Proxy'; @@ -35,6 +35,7 @@ import { SnappingManager } from '../../../util/SnappingManager'; import { undoBatch, UndoManager } from '../../../util/UndoManager'; import { CollectionFreeFormView } from '../../collections/collectionFreeForm/CollectionFreeFormView'; import { CollectionStackingView } from '../../collections/CollectionStackingView'; +import { CollectionTreeView } from '../../collections/CollectionTreeView'; import { ContextMenu } from '../../ContextMenu'; import { ContextMenuProps } from '../../ContextMenuItem'; import { ViewBoxAnnotatableComponent } from '../../DocComponent'; @@ -44,6 +45,7 @@ import { LightboxView } from '../../LightboxView'; import { AnchorMenu } from '../../pdf/AnchorMenu'; import { SidebarAnnos } from '../../SidebarAnnos'; import { StyleProp } from '../../StyleProvider'; +import { DocumentViewInternal } from '../DocumentView'; import { FieldView, FieldViewProps } from '../FieldView'; import { LinkDocPreview } from '../LinkDocPreview'; import { DashDocCommentView } from './DashDocCommentView'; @@ -61,25 +63,14 @@ import { schema } from './schema_rts'; import { SummaryView } from './SummaryView'; import applyDevTools = require('prosemirror-dev-tools'); import React = require('react'); -import { text } from 'body-parser'; -import { CollectionTreeView } from '../../collections/CollectionTreeView'; -import { DocumentViewInternal } from '../DocumentView'; const translateGoogleApi = require('translate-google-api'); -export interface FormattedTextBoxProps { - makeLink?: () => Opt<Doc>; // bcz: hack: notifies the text document when the container has made a link. allows the text doc to react and setup a hyeprlink for any selected text - xPadding?: number; // used to override document's settings for xMargin --- see CollectionCarouselView - yPadding?: number; - noSidebar?: boolean; - dontScale?: boolean; - dontSelectOnLoad?: boolean; // suppress selecting the text box when loaded (and mark as not being associated with scrollTop document field) -} export const GoogleRef = 'googleDocId'; type PullHandler = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => void; @observer -export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps & FormattedTextBoxProps>() { +export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { public static LayoutString(fieldStr: string) { return FieldView.LayoutString(FormattedTextBox, fieldStr); } @@ -1843,18 +1834,27 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const paddingY = NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0); const selPad = (selected && !this.layoutDoc._singleLine) || minimal ? Math.min(paddingY, Math.min(paddingX, 10)) : 0; const selPaddingClass = selected && !this.layoutDoc._singleLine && paddingY >= 10 ? '-selected' : ''; - const styleFromString = this.styleFromLayoutString(scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._headerHeight}px' > - return styleFromString?.height === '0px' ? null : ( + const styleFromLayoutString = Doc.styleFromLayoutString(this.rootDoc, this.layoutDoc, this.props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._headerHeight}px' > + return styleFromLayoutString?.height === '0px' ? null : ( <div className="formattedTextBox-cont" onWheel={e => this.props.isContentActive() && e.stopPropagation()} style={{ - transform: this.props.dontScale ? undefined : `scale(${scale})`, - transformOrigin: this.props.dontScale ? undefined : 'top left', - width: this.props.dontScale ? undefined : `${100 / scale}%`, - height: this.props.dontScale ? undefined : `${100 / scale}%`, + ...(this.props.dontScale + ? {} + : { + transform: `scale(${scale})`, + transformOrigin: 'top left', + width: `${100 / scale}%`, + height: `${100 / scale}%`, + }), + transition: 'inherit', // overflowY: this.layoutDoc._autoHeight ? "hidden" : undefined, - ...styleFromString, + color: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color), + fontSize: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontSize), + fontFamily: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontFamily), + fontWeight: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontWeight), + ...styleFromLayoutString, }}> <div className={`formattedTextBox-cont`} @@ -1862,11 +1862,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps style={{ overflow: this.autoHeight ? 'hidden' : undefined, height: this.props.height || (this.autoHeight && this.props.renderDepth && !this.props.suppressSetHeight ? 'max-content' : undefined), - background: this.props.background ? this.props.background : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor), - color: this.props.color ? this.props.color : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color), - fontSize: this.props.fontSize ? this.props.fontSize : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontSize), - fontWeight: Cast(this.layoutDoc._fontWeight, 'string', null) as any, - fontFamily: StrCast(this.layoutDoc._fontFamily, 'inherit'), pointerEvents: interactive ? undefined : 'none', }} onContextMenu={this.specificContextMenu} diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 2a77210ae..0cbe60c0c 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -16,7 +16,7 @@ import { SelectionManager } from '../../../util/SelectionManager'; import { undoBatch, UndoManager } from '../../../util/UndoManager'; import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu'; import { FieldViewProps } from '../FieldView'; -import { FormattedTextBox, FormattedTextBoxProps } from './FormattedTextBox'; +import { FormattedTextBox } from './FormattedTextBox'; import { updateBullets } from './ProsemirrorExampleTransfer'; import './RichTextMenu.scss'; import { schema } from './schema_rts'; @@ -29,7 +29,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { private _linkToRef = React.createRef<HTMLInputElement>(); @observable public view?: EditorView; - public editorProps: (FieldViewProps & FormattedTextBoxProps) | undefined; + public editorProps: FieldViewProps | undefined; public _brushMap: Map<string, Set<Mark>> = new Map(); diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index 05e09361b..eb40089ec 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -358,20 +358,24 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (bestTarget) this._navTimer = PresBox.navigateToDoc(bestTarget, activeItem, false); }; + static pinDataTypes(target: Doc) { + const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(target.type as any) || target._viewType === CollectionViewType.Stacking; + const pannable = [DocumentType.IMG].includes(target.type as any) || (target.type === DocumentType.COL && target._viewType === CollectionViewType.Freeform); + const temporal = [DocumentType.AUDIO, DocumentType.VID].includes(target.type as any); + const clippable = [DocumentType.COMPARISON].includes(target.type as any); + return { scrollable, pannable, temporal, clippable }; + } // navigates to the bestTarget document by making sure it is on screen, // then it applies the view specs stored in activeItem to @action static navigateToDoc(bestTarget: Doc, activeItem: Doc, jumpToDoc: boolean) { - if (bestTarget.type === DocumentType.PDF || bestTarget.type === DocumentType.WEB || bestTarget.type === DocumentType.RTF || bestTarget._viewType === CollectionViewType.Stacking) { - bestTarget._viewTransition = activeItem.presTransition ? `transform ${activeItem.presTransition}ms` : 'all 0.5s'; - bestTarget._scrollTop = activeItem.presPinViewScroll; - } else if (bestTarget.type === DocumentType.COMPARISON) { - bestTarget._clipWidth = activeItem.presPinClipWidth; - } else if ([DocumentType.AUDIO, DocumentType.VID].includes(bestTarget.type as any)) { - bestTarget._currentTimecode = activeItem.presStartTime; - } else { + bestTarget._viewTransition = activeItem.presTransition ? `transform ${activeItem.presTransition}ms` : 'all 0.5s'; + const { scrollable, pannable, temporal, clippable } = this.pinDataTypes(bestTarget); + if (clippable) bestTarget._clipWidth = activeItem.presPinClipWidth; + if (temporal) bestTarget._currentTimecode = activeItem.presStartTime; + if (scrollable) bestTarget._scrollTop = activeItem.presPinViewScroll; + if (pannable) { const contentBounds = Cast(activeItem.contentBounds, listSpec('number')); - bestTarget._viewTransition = activeItem.presTransition ? `transform ${activeItem.presTransition}ms` : 'all 0.5s'; if (contentBounds) { bestTarget._panX = (contentBounds[0] + contentBounds[2]) / 2; bestTarget._panY = (contentBounds[1] + contentBounds[3]) / 2; @@ -388,6 +392,43 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { return setTimeout(() => (bestTarget._viewTransition = undefined), activeItem.presTransition ? NumCast(activeItem.presTransition) + 10 : 510); } + /// copies values from the targetDoc (which is the prototype of the pinDoc) to + /// reserved fields on the pinDoc so that those values can be restored to the + /// target doc when navigating to it. + @action + static pinDocView(pinDoc: Doc, pinProps: PinProps | undefined) { + if (pinProps?.pinWithView) { + // If pinWithView option set then update scale and x / y props of slide + const bounds = pinProps.pinWithView.bounds; + pinDoc.presPinView = true; + pinDoc.presPinViewX = bounds.left + bounds.width / 2; + pinDoc.presPinViewY = bounds.top + bounds.height / 2; + pinDoc.presPinViewScale = pinProps.pinWithView.scale; + pinDoc.contentBounds = new List<number>([bounds.left, bounds.top, bounds.left + bounds.width, bounds.top + bounds.height]); + } + if (pinProps?.pinDocView) { + const { scrollable, pannable, temporal, clippable } = this.pinDataTypes(pinDoc); + pinDoc.presPinView = (pinProps?.pinWithView ? true : false) || scrollable || temporal || pannable || clippable; + + if (scrollable) pinDoc.presPinViewScroll = pinDoc._scrollTop; + else if (clippable) pinDoc.presPinClipWidth = pinDoc._clipWidth; + else if (temporal) pinDoc.presEndTime = NumCast((pinDoc.presStartTime = pinDoc._currentTimecode)) + 0.1; + else if (pannable) { + const panX = NumCast(pinDoc._panX); + const panY = NumCast(pinDoc._panY); + const pw = NumCast(pinProps.panelWidth); + const ph = NumCast(pinProps.panelHeight); + const ps = NumCast(pinDoc._viewScale); + if (pw && ph && ps) { + pinDoc.contentBounds = new List<number>([panX - pw / 2 / ps, panY - ph / 2 / ps, panX + pw / 2 / ps, panY + ph / 2 / ps]); + } + pinDoc.presPinViewX = panX; + pinDoc.presPinViewY = panY; + pinDoc.presPinViewScale = ps; + } + } + } + /** * This method makes sure that cursor navigates to the element that * has the option open and last in the group. @@ -1540,26 +1581,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } } - @computed get effectDirection(): string { - let effect = ''; + @computed get effectDirection() { + // prettier-ignore switch (this.activeItem.presEffectDirection) { - case 'left': - effect = 'Enter from left'; - break; - case 'right': - effect = 'Enter from right'; - break; - case 'top': - effect = 'Enter from top'; - break; - case 'bottom': - effect = 'Enter from bottom'; - break; - default: - effect = 'Enter from center'; - break; + case 'left': return 'Enter from left'; + case 'right': return 'Enter from right'; + case 'top': return'Enter from top'; + case 'bottom': return 'Enter from bottom'; } - return effect; + return 'Enter from center'; } @undoBatch @@ -1582,179 +1612,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }); }; - @computed get presPinViewOptionsDropdown() { - const activeItem: Doc = this.activeItem; - const targetDoc: Doc = this.targetDoc; - const presPinWithViewIcon = <img src="/assets/pinWithView.png" style={{ margin: 'auto', width: 16, filter: 'invert(1)' }} />; - return ( - <> - {this.panable || this.scrollable || this.targetDoc.type === DocumentType.COMPARISON ? 'Pinned view' : null} - <div className="ribbon-doubleButton"> - <Tooltip - title={ - <> - <div className="dash-tooltip">{activeItem.presPinView ? 'Turn off pin with view' : 'Turn on pin with view'}</div> - </> - }> - <div - className="ribbon-toggle" - style={{ width: 20, padding: 0, backgroundColor: activeItem.presPinView ? Colors.LIGHT_BLUE : '' }} - onClick={() => { - activeItem.presPinView = !activeItem.presPinView; - targetDoc.presPinView = activeItem.presPinView; - if (activeItem.presPinView) { - if (targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.RTF || targetDoc.type === DocumentType.WEB || targetDoc._viewType === CollectionViewType.Stacking) { - const scroll = targetDoc._scrollTop; - activeItem.presPinView = true; - activeItem.presPinViewScroll = scroll; - } else if ([DocumentType.AUDIO, DocumentType.VID].includes(targetDoc.type as any)) { - activeItem.presStartTime = targetDoc._currentTimecode; - activeItem.presEndTime = NumCast(targetDoc._currentTimecode) + 0.1; - } else if ((targetDoc.type === DocumentType.COL && targetDoc._viewType === CollectionViewType.Freeform) || targetDoc.type === DocumentType.IMG) { - const x = targetDoc._panX; - const y = targetDoc._panY; - const scale = targetDoc._viewScale; - activeItem.presPinView = true; - activeItem.presPinViewX = x; - activeItem.presPinViewY = y; - activeItem.presPinViewScale = scale; - } else if (targetDoc.type === DocumentType.COMPARISON) { - const width = targetDoc._clipWidth; - activeItem.presPinClipWidth = width; - activeItem.presPinView = true; - } - } - }}> - {presPinWithViewIcon} - </div> - </Tooltip> - {activeItem.presPinView ? ( - <Tooltip - title={ - <> - <div className="dash-tooltip">{'Update the pinned view with the view of the selected document'}</div> - </> - }> - <div - className="ribbon-button" - onClick={() => { - if (targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.WEB || targetDoc.type === DocumentType.RTF) { - const scroll = targetDoc._scrollTop; - activeItem.presPinViewScroll = scroll; - } else if ([DocumentType.AUDIO, DocumentType.VID].includes(targetDoc.type as any)) { - activeItem.presStartTime = targetDoc._currentTimecode; - activeItem.presStartTime = NumCast(targetDoc._currentTimecode) + 0.1; - } else if (targetDoc.type === DocumentType.COMPARISON) { - const clipWidth = targetDoc._clipWidth; - activeItem.presPinClipWidth = clipWidth; - } else { - const x = targetDoc._panX; - const y = targetDoc._panY; - const scale = targetDoc._viewScale; - activeItem.presPinViewX = x; - activeItem.presPinViewY = y; - activeItem.presPinViewScale = scale; - } - }}> - Update - </div> - </Tooltip> - ) : null} - </div> - </> - ); - } - - @computed get panOptionsDropdown() { - const activeItem: Doc = this.activeItem; - const targetDoc: Doc = this.targetDoc; - return ( - <> - {this.panable ? ( - <div style={{ display: activeItem.presPinView ? 'block' : 'none' }}> - <div className="ribbon-doubleButton" style={{ marginRight: 10 }}> - <div className="presBox-subheading">Pan X</div> - <div className="ribbon-property" style={{ paddingRight: 0, paddingLeft: 0 }}> - <input - className="presBox-input" - style={{ textAlign: 'left', width: 50 }} - type="number" - value={NumCast(activeItem.presPinViewX)} - onKeyDown={e => e.stopPropagation()} - onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { - const val = e.target.value; - activeItem.presPinViewX = Number(val); - })} - /> - </div> - </div> - <div className="ribbon-doubleButton" style={{ marginRight: 10 }}> - <div className="presBox-subheading">Pan Y</div> - <div className="ribbon-property" style={{ paddingRight: 0, paddingLeft: 0 }}> - <input - className="presBox-input" - style={{ textAlign: 'left', width: 50 }} - type="number" - value={NumCast(activeItem.presPinViewY)} - onKeyDown={e => e.stopPropagation()} - onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { - const val = e.target.value; - activeItem.presPinViewY = Number(val); - })} - /> - </div> - </div> - <div className="ribbon-doubleButton" style={{ marginRight: 10 }}> - <div className="presBox-subheading">Scale</div> - <div className="ribbon-property" style={{ paddingRight: 0, paddingLeft: 0 }}> - <input - className="presBox-input" - style={{ textAlign: 'left', width: 50 }} - type="number" - value={NumCast(activeItem.presPinViewScale)} - onKeyDown={e => e.stopPropagation()} - onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { - const val = e.target.value; - activeItem.presPinViewScale = Number(val); - })} - /> - </div> - </div> - </div> - ) : null} - </> - ); - } - - @computed get scrollOptionsDropdown() { - const activeItem: Doc = this.activeItem; - const targetDoc: Doc = this.targetDoc; - return ( - <> - {this.scrollable ? ( - <div style={{ display: activeItem.presPinView ? 'block' : 'none' }}> - <div className="ribbon-doubleButton" style={{ marginRight: 10 }}> - <div className="presBox-subheading">Scroll</div> - <div className="ribbon-property" style={{ paddingRight: 0, paddingLeft: 0 }}> - <input - className="presBox-input" - style={{ textAlign: 'left', width: 50 }} - type="number" - value={NumCast(activeItem.presPinViewScroll)} - onKeyDown={e => e.stopPropagation()} - onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { - const val = e.target.value; - activeItem.presPinViewScroll = Number(val); - })} - /> - </div> - </div> - </div> - ) : null} - </> - ); - } - @computed get mediaStopSlides() { const activeItem: Doc = this.activeItem; const list = this.childDocs.map((doc, i) => { @@ -2102,42 +1959,23 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }; createTemplate = (layout: string, input?: string) => { - const activeItem: Doc = this.activeItem; - const targetDoc: Doc = this.targetDoc; - let x = 0; - let y = 0; - if (activeItem && targetDoc) { - x = NumCast(targetDoc.x); - y = NumCast(targetDoc.y) + NumCast(targetDoc._height) + 20; - } - let doc = undefined; - const title = Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 58, _fontSize: '24pt' }); - const subtitle = Docs.Create.TextDocument('Click to change subtitle', { title: 'Slide subtitle', _width: 380, _height: 50, x: 10, y: 118, _fontSize: '16pt' }); - const header = Docs.Create.TextDocument('Click to change header', { title: 'Slide header', _width: 380, _height: 65, x: 10, y: 80, _fontSize: '20pt' }); - const contentTitle = Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 10, _fontSize: '24pt' }); - const content = Docs.Create.TextDocument('Click to change text', { title: 'Slide text', _width: 380, _height: 145, x: 10, y: 70, _fontSize: '14pt' }); - const content1 = Docs.Create.TextDocument('Click to change text', { title: 'Column 1', _width: 185, _height: 140, x: 10, y: 80, _fontSize: '14pt' }); - const content2 = Docs.Create.TextDocument('Click to change text', { title: 'Column 2', _width: 185, _height: 140, x: 205, y: 80, _fontSize: '14pt' }); + const x = this.activeItem && this.targetDoc ? NumCast(this.targetDoc.x) : 0; + const y = this.activeItem && this.targetDoc ? NumCast(this.targetDoc.y) + NumCast(this.targetDoc._height) + 20 : 0; + const title = () => Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 58, _fontSize: '24pt' }); + const subtitle = () => Docs.Create.TextDocument('Click to change subtitle', { title: 'Slide subtitle', _width: 380, _height: 50, x: 10, y: 118, _fontSize: '16pt' }); + const header = () => Docs.Create.TextDocument('Click to change header', { title: 'Slide header', _width: 380, _height: 65, x: 10, y: 80, _fontSize: '20pt' }); + const contentTitle = () => Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 10, _fontSize: '24pt' }); + const content = () => Docs.Create.TextDocument('Click to change text', { title: 'Slide text', _width: 380, _height: 145, x: 10, y: 70, _fontSize: '14pt' }); + const content1 = () => Docs.Create.TextDocument('Click to change text', { title: 'Column 1', _width: 185, _height: 140, x: 10, y: 80, _fontSize: '14pt' }); + const content2 = () => Docs.Create.TextDocument('Click to change text', { title: 'Column 2', _width: 185, _height: 140, x: 205, y: 80, _fontSize: '14pt' }); + // prettier-ignore switch (layout) { - case 'blank': - doc = Docs.Create.FreeformDocument([], { title: input ? input : 'Blank slide', _width: 400, _height: 225, x: x, y: y }); - break; - case 'title': - doc = Docs.Create.FreeformDocument([title, subtitle], { title: input ? input : 'Title slide', _width: 400, _height: 225, _fitContentsToBox: true, x: x, y: y }); - break; - case 'header': - doc = Docs.Create.FreeformDocument([header], { title: input ? input : 'Section header', _width: 400, _height: 225, _fitContentsToBox: true, x: x, y: y }); - break; - case 'content': - doc = Docs.Create.FreeformDocument([contentTitle, content], { title: input ? input : 'Title and content', _width: 400, _height: 225, _fitContentsToBox: true, x: x, y: y }); - break; - case 'twoColumns': - doc = Docs.Create.FreeformDocument([contentTitle, content1, content2], { title: input ? input : 'Title and two columns', _width: 400, _height: 225, _fitContentsToBox: true, x: x, y: y }); - break; - default: - break; + case 'blank': return Docs.Create.FreeformDocument([], { title: input ? input : 'Blank slide', _width: 400, _height: 225, x, y }); + case 'title': return Docs.Create.FreeformDocument([title(), subtitle()], { title: input ? input : 'Title slide', _width: 400, _height: 225, _fitContentsToBox: true, x, y }); + case 'header': return Docs.Create.FreeformDocument([header()], { title: input ? input : 'Section header', _width: 400, _height: 225, _fitContentsToBox: true, x, y }); + case 'content': return Docs.Create.FreeformDocument([contentTitle(), content()], { title: input ? input : 'Title and content', _width: 400, _height: 225, _fitContentsToBox: true, x, y }); + case 'twoColumns': return Docs.Create.FreeformDocument([contentTitle(), content1(), content2()], { title: input ? input : 'Title and two columns', _width: 400, _height: 225, _fitContentsToBox: true, x, y }) } - return doc; }; // Dropdown that appears when the user wants to begin presenting (either minimize or sidebar view) @@ -2201,42 +2039,22 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { /** * Returns the collection type as a string for headers */ - @computed get stringType(): string { - const activeItem: Doc = this.activeItem; - const targetDoc: Doc = this.targetDoc; - let type: string = ''; - if (activeItem) { - switch (targetDoc.type) { - case DocumentType.PDF: - type = 'PDF'; - break; - case DocumentType.RTF: - type = 'Text node'; - break; - case DocumentType.COL: - type = 'Collection'; - break; - case DocumentType.AUDIO: - type = 'Audio'; - break; - case DocumentType.VID: - type = 'Video'; - break; - case DocumentType.IMG: - type = 'Image'; - break; - case DocumentType.WEB: - type = 'Web page'; - break; - case DocumentType.MAP: - type = 'Map'; - break; - default: - type = 'Other node'; - break; + @computed get stringType() { + if (this.activeItem) { + // prettier-ignore + switch (this.targetDoc.type) { + case DocumentType.PDF: return 'PDF'; + case DocumentType.RTF: return 'Text node'; + case DocumentType.COL: return 'Collection'; + case DocumentType.AUDIO: return 'Audio'; + case DocumentType.VID: return 'Video'; + case DocumentType.IMG: return 'Image'; + case DocumentType.WEB: return 'Web page'; + case DocumentType.MAP: return 'Map'; + default: return 'Other node'; } } - return type; + return ''; } @observable private openActiveColorPicker: boolean = false; @@ -2845,10 +2663,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { case DocumentType.PDF || DocumentType.RTF || DocumentType.WEB: this.updateList(activeItem.frameList); break; - case DocumentType.COL: - break; - default: - break; } }; diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx index 3af8cad9a..91196ca21 100644 --- a/src/client/views/nodes/trails/PresElementBox.tsx +++ b/src/client/views/nodes/trails/PresElementBox.tsx @@ -256,10 +256,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { onPointerMove = (e: PointerEvent) => { const slide = this._itemRef.current!; - let dragIsPresItem: boolean = DragManager.docsBeingDragged.length > 0 ? true : false; - for (const doc of DragManager.docsBeingDragged) { - if (!doc.presentationTargetDoc) dragIsPresItem = false; - } + const dragIsPresItem = DragManager.docsBeingDragged.some(d => d.presentationTargetDoc); if (slide && dragIsPresItem) { const rect = slide.getBoundingClientRect(); const y = e.clientY - rect.top; //y position within the element. @@ -501,7 +498,13 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { backgroundColor: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor), boxShadow: presBoxColor && presBoxColor !== 'white' && presBoxColor !== 'transparent' ? (isSelected ? '0 0 0px 1.5px' + presBoxColor : undefined) : undefined, }}> - <div className="presItem-name" style={{ maxWidth: showMore ? toolbarWidth - 195 : toolbarWidth - 105, cursor: isSelected ? 'text' : 'grab' }}> + <div + className="presItem-name" + style={{ + pointerEvents: isSelected ? undefined : 'none', + maxWidth: showMore ? toolbarWidth - 195 : toolbarWidth - 105, + cursor: isSelected ? 'text' : 'grab', + }}> <div>{`${this.indexInPres + 1}. `}</div> <EditableView ref={this._titleRef} editing={!isSelected ? false : undefined} contents={activeItem.title} overflow={'ellipsis'} GetValue={() => StrCast(activeItem.title)} SetValue={this.onSetValue} /> </div> diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx index 3fc0a237e..cbcfed06f 100644 --- a/src/client/views/topbar/TopBar.tsx +++ b/src/client/views/topbar/TopBar.tsx @@ -1,5 +1,6 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; +import { MdBugReport } from 'react-icons/md'; import { action } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -18,6 +19,7 @@ import { DashboardView } from '../DashboardView'; import { Borders, Colors } from '../global/globalEnums'; import { MainView } from '../MainView'; import './TopBar.scss'; +import { ReportManager } from '../../util/ReportManager'; /** * ABOUT: This is the topbar in Dash, which included the current Dashboard as well as access to information on the user @@ -31,6 +33,7 @@ export class TopBar extends React.Component { DashboardView.closeActiveDashboard(); // bcz: if we do this, we need some other way to keep track, for user convenience, of the last dashboard in use }); }; + render() { const activeDashboard = Doc.ActiveDashboard; return ( @@ -92,6 +95,9 @@ export class TopBar extends React.Component { {GetEffectiveAcl(Doc.GetProto(Doc.ActiveDashboard)) === AclAdmin ? 'Share' : 'view original'} </div> ) : null} + <div className="topbar-button-icon" onClick={() => ReportManager.Instance.open()}> + <MdBugReport /> + </div> <div className="topbar-button-icon" onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/', '_blank')}> <FontAwesomeIcon icon="question-circle" /> </div> diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 8d56ebf8c..612fc7fb8 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -1389,6 +1389,20 @@ export namespace Doc { return !curPres ? false : DocListCast(curPres.data).findIndex(val => Doc.AreProtosEqual(val, doc)) !== -1; } + export function styleFromLayoutString(rootDoc: Doc, layoutDoc: Doc, props: any, scale: number) { + const style: { [key: string]: any } = {}; + const divKeys = ['width', 'height', 'fontSize', 'transform', 'left', 'backgroundColor', 'left', 'right', 'top', 'bottom', 'pointerEvents', 'position']; + const replacer = (match: any, expr: string, offset: any, string: any) => { + // bcz: this executes a script to convert a property expression string: { script } into a value + return ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name, scale: 'number' })?.script.run({ self: rootDoc, this: layoutDoc, scale }).result?.toString() ?? ''; + }; + divKeys.map((prop: string) => { + const p = props[prop]; + typeof p === 'string' && (style[prop] = p?.replace(/{([^.'][^}']+)}/g, replacer)); + }); + return style; + } + // prettier-ignore export function toIcon(doc?: Doc, isOpen?: boolean) { switch (StrCast(doc?.type)) { diff --git a/src/fields/SchemaHeaderField.ts b/src/fields/SchemaHeaderField.ts index 3b02d0cfe..1321bc327 100644 --- a/src/fields/SchemaHeaderField.ts +++ b/src/fields/SchemaHeaderField.ts @@ -1,60 +1,60 @@ -import { Deserializable } from "../client/util/SerializationHelper"; -import { serializable, primitive } from "serializr"; -import { ObjectField } from "./ObjectField"; -import { Copy, ToScriptString, ToString, OnUpdate } from "./FieldSymbols"; -import { scriptingGlobal } from "../client/util/ScriptingGlobals"; -import { ColumnType } from "../client/views/collections/collectionSchema/CollectionSchemaView"; +import { Deserializable } from '../client/util/SerializationHelper'; +import { serializable, primitive } from 'serializr'; +import { ObjectField } from './ObjectField'; +import { Copy, ToScriptString, ToString, OnUpdate } from './FieldSymbols'; +import { scriptingGlobal } from '../client/util/ScriptingGlobals'; +import { ColumnType } from '../client/views/collections/collectionSchema/CollectionSchemaView'; export const PastelSchemaPalette = new Map<string, string>([ // ["pink1", "#FFB4E8"], - ["pink2", "#ff9cee"], - ["pink3", "#ffccf9"], - ["pink4", "#fcc2ff"], - ["pink5", "#f6a6ff"], - ["purple1", "#b28dff"], - ["purple2", "#c5a3ff"], - ["purple3", "#d5aaff"], - ["purple4", "#ecd4ff"], + ['pink2', '#ff9cee'], + ['pink3', '#ffccf9'], + ['pink4', '#fcc2ff'], + ['pink5', '#f6a6ff'], + ['purple1', '#b28dff'], + ['purple2', '#c5a3ff'], + ['purple3', '#d5aaff'], + ['purple4', '#ecd4ff'], // ["purple5", "#fb34ff"], - ["purple6", "#dcd3ff"], - ["purple7", "#a79aff"], - ["purple8", "#b5b9ff"], - ["purple9", "#97a2ff"], - ["bluegreen1", "#afcbff"], - ["bluegreen2", "#aff8db"], - ["bluegreen3", "#c4faf8"], - ["bluegreen4", "#85e3ff"], - ["bluegreen5", "#ace7ff"], + ['purple6', '#dcd3ff'], + ['purple7', '#a79aff'], + ['purple8', '#b5b9ff'], + ['purple9', '#97a2ff'], + ['bluegreen1', '#afcbff'], + ['bluegreen2', '#aff8db'], + ['bluegreen3', '#c4faf8'], + ['bluegreen4', '#85e3ff'], + ['bluegreen5', '#ace7ff'], // ["bluegreen6", "#6eb5ff"], - ["bluegreen7", "#bffcc6"], - ["bluegreen8", "#dbffd6"], - ["yellow1", "#f3ffe3"], - ["yellow2", "#e7ffac"], - ["yellow3", "#ffffd1"], - ["yellow4", "#fff5ba"], + ['bluegreen7', '#bffcc6'], + ['bluegreen8', '#dbffd6'], + ['yellow1', '#f3ffe3'], + ['yellow2', '#e7ffac'], + ['yellow3', '#ffffd1'], + ['yellow4', '#fff5ba'], // ["red1", "#ffc9de"], - ["red2", "#ffabab"], - ["red3", "#ffbebc"], - ["red4", "#ffcbc1"], - ["orange1", "#ffd5b3"], - ["gray", "#f1efeb"] + ['red2', '#ffabab'], + ['red3', '#ffbebc'], + ['red4', '#ffcbc1'], + ['orange1', '#ffd5b3'], + ['gray', '#f1efeb'], ]); export const RandomPastel = () => Array.from(PastelSchemaPalette.values())[Math.floor(Math.random() * PastelSchemaPalette.size)]; export const DarkPastelSchemaPalette = new Map<string, string>([ - ["pink2", "#c932b0"], - ["purple4", "#913ad6"], - ["bluegreen1", "#3978ed"], - ["bluegreen7", "#2adb3e"], - ["bluegreen5", "#21b0eb"], - ["yellow4", "#edcc0c"], - ["red2", "#eb3636"], - ["orange1", "#f2740f"], + ['pink2', '#c932b0'], + ['purple4', '#913ad6'], + ['bluegreen1', '#3978ed'], + ['bluegreen7', '#2adb3e'], + ['bluegreen5', '#21b0eb'], + ['yellow4', '#edcc0c'], + ['red2', '#eb3636'], + ['orange1', '#f2740f'], ]); @scriptingGlobal -@Deserializable("schemaheader") +@Deserializable('schemaheader') export class SchemaHeaderField extends ObjectField { @serializable(primitive()) heading: string; @@ -69,7 +69,7 @@ export class SchemaHeaderField extends ObjectField { @serializable(primitive()) desc: boolean | undefined; // boolean determines sort order, undefined when no sort - constructor(heading: string = "", color: string = RandomPastel(), type?: ColumnType, width?: number, desc?: boolean, collapsed?: boolean) { + constructor(heading: string = '', color: string = RandomPastel(), type?: ColumnType, width?: number, desc?: boolean, collapsed?: boolean) { super(); this.heading = heading; @@ -111,7 +111,7 @@ export class SchemaHeaderField extends ObjectField { } [Copy]() { - return new SchemaHeaderField(this.heading, this.color, this.type); + return new SchemaHeaderField(this.heading, this.color, this.type, this.width, this.desc, this.collapsed); } [ToScriptString]() { @@ -120,4 +120,4 @@ export class SchemaHeaderField extends ObjectField { [ToString]() { return `SchemaHeaderField`; } -}
\ No newline at end of file +} diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts index 68fb45987..48d5c5563 100644 --- a/src/fields/ScriptField.ts +++ b/src/fields/ScriptField.ts @@ -9,7 +9,7 @@ import { Doc, Field, Opt } from './Doc'; import { Copy, Id, ToScriptString, ToString } from './FieldSymbols'; import { List } from './List'; import { ObjectField } from './ObjectField'; -import { Cast, NumCast } from './Types'; +import { Cast, NumCast, StrCast } from './Types'; import { Plugins } from './util'; function optional(propSchema: PropSchema) { @@ -189,7 +189,7 @@ export class ComputedField extends ScriptField { const compiled = ScriptField.CompileScript(script, params, true, capturedVariables); return compiled.compiled ? new ComputedField(compiled) : undefined; } - public static MakeInterpolated(fieldKey: string, interpolatorKey: string, doc: Doc, curTimecode: number) { + public static MakeInterpolatedNumber(fieldKey: string, interpolatorKey: string, doc: Doc, curTimecode: number) { if (!doc[`${fieldKey}-indexed`]) { const flist = new List<number>(numberRange(curTimecode + 1).map(i => undefined) as any as number[]); flist[curTimecode] = NumCast(doc[fieldKey]); @@ -199,6 +199,26 @@ export class ComputedField extends ScriptField { const setField = ScriptField.CompileScript(`setIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey}, value)`, { value: 'any' }, true, {}); return getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined; } + public static MakeInterpolatedString(fieldKey: string, interpolatorKey: string, doc: Doc, curTimecode: number) { + if (!doc[`${fieldKey}-indexed`]) { + const flist = new List<string>(numberRange(curTimecode + 1).map(i => undefined) as any as string[]); + flist[curTimecode] = StrCast(doc[fieldKey]); + doc[`${fieldKey}-indexed`] = flist; + } + const getField = ScriptField.CompileScript(`getIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey})`, {}, true, {}); + const setField = ScriptField.CompileScript(`setIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey}, value)`, { value: 'any' }, true, {}); + return getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined; + } + public static MakeInterpolatedDataField(fieldKey: string, interpolatorKey: string, doc: Doc, curTimecode: number) { + if (!doc[`${fieldKey}-indexed`]) { + const flist = new List<Field>(numberRange(curTimecode + 1).map(i => undefined) as any as Field[]); + flist[curTimecode] = Field.Copy(doc[fieldKey]); + doc[`${fieldKey}-indexed`] = flist; + } + const getField = ScriptField.CompileScript(`getIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey})`, {}, true, {}); + const setField = ScriptField.CompileScript(`setIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey}, value)`, { value: 'any' }, true, {}); + return getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined; + } } export namespace ComputedField { let useComputed = true; diff --git a/src/fields/util.ts b/src/fields/util.ts index d87bb6656..b3cbbe241 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -445,8 +445,13 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any undo: action(() => { // console.log("undo $add: " + prop, diff.items) // bcz: uncomment to log undo diff.items.forEach((item: any) => { - const ind = receiver[prop].indexOf(item.value ? item.value() : item); - ind !== -1 && receiver[prop].splice(ind, 1); + if (item instanceof SchemaHeaderField) { + const ind = receiver[prop].findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading); + ind !== -1 && receiver[prop].splice(ind, 1); + } else { + const ind = receiver[prop].indexOf(item.value ? item.value() : item); + ind !== -1 && receiver[prop].splice(ind, 1); + } }); lastValue = ObjectField.MakeCopy(receiver[prop]); }), |