diff options
Diffstat (limited to 'src')
77 files changed, 1373 insertions, 1147 deletions
diff --git a/src/Utils.ts b/src/Utils.ts index e8bd35ac4..38325a463 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -902,6 +902,13 @@ export function setupMoveUpEvents( document.addEventListener('click', _clickEvent, true); } +export function DivHeight(ele: HTMLElement): number { + return Number(getComputedStyle(ele).height.replace('px', '')); +} +export function DivWidth(ele: HTMLElement): number { + return Number(getComputedStyle(ele).width.replace('px', '')); +} + export function dateRangeStrToDates(dateStr: string) { // dateStr in yyyy-mm-dd format const dateRangeParts = dateStr.split('|'); // splits into from and to date diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index f334e958a..2d2d13519 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -3,7 +3,7 @@ import { action, reaction, runInAction } from 'mobx'; import { basename } from 'path'; import { DateField } from '../../fields/DateField'; import { Doc, DocListCast, Field, LinkedTo, Opt, StrListCast, updateCachedAcls } from '../../fields/Doc'; -import { Initializing } from '../../fields/DocSymbols'; +import { DocData, Initializing } from '../../fields/DocSymbols'; import { Id } from '../../fields/FieldSymbols'; import { HtmlField } from '../../fields/HtmlField'; import { InkField, PointData } from '../../fields/InkField'; @@ -68,13 +68,24 @@ class EmptyBox { return ''; } } + +export enum FInfoFieldType { + string = 'string', + boolean = 'boolean', + number = 'number', + Doc = 'Doc', + enumeration = 'enum', + date = 'date', + list = 'list', + rtf = 'rich text', +} export class FInfo { description: string = ''; readOnly: boolean = false; - fieldType?: string = ''; + fieldType?: FInfoFieldType; values?: Field[]; - filterable?: boolean = true; + filterable?: boolean = true; // can be used as a Filter in FilterPanel // format?: string; // format to display values (e.g, decimal places, $, etc) // parse?: ScriptField; // parse a value from a string constructor(d: string, readOnly?: boolean) { @@ -84,7 +95,7 @@ export class FInfo { searchable = () => true; } class BoolInfo extends FInfo { - fieldType? = 'boolean'; + fieldType? = FInfoFieldType.boolean; values?: boolean[] = [true, false]; constructor(d: string, filterable?: boolean) { super(d); @@ -93,7 +104,7 @@ class BoolInfo extends FInfo { override searchable = () => false; } class NumInfo extends FInfo { - fieldType? = 'number'; + fieldType? = FInfoFieldType.number; values?: number[] = []; constructor(d: string, filterable?: boolean, readOnly?: boolean, values?: number[]) { super(d, readOnly); @@ -103,7 +114,7 @@ class NumInfo extends FInfo { override searchable = () => false; } class StrInfo extends FInfo { - fieldType? = 'string'; + fieldType? = FInfoFieldType.string; values?: string[] = []; constructor(d: string, filterable?: boolean, readOnly?: boolean, values?: string[]) { super(d, readOnly); @@ -112,7 +123,7 @@ class StrInfo extends FInfo { } } class DocInfo extends FInfo { - fieldType? = 'Doc'; + fieldType? = FInfoFieldType.Doc; values?: Doc[] = []; constructor(d: string, filterable?: boolean, values?: Doc[]) { super(d, true); @@ -122,45 +133,55 @@ class DocInfo extends FInfo { override searchable = () => false; } class DimInfo extends FInfo { - fieldType? = 'enumeration'; + fieldType? = FInfoFieldType.enumeration; values? = [DimUnit.Pixel, DimUnit.Ratio]; readOnly = false; filterable = false; override searchable = () => false; } class PEInfo extends FInfo { - fieldType? = 'enumeration'; + fieldType? = FInfoFieldType.enumeration; values? = ['all', 'none']; readOnly = false; filterable = false; override searchable = () => false; } class DAInfo extends FInfo { - fieldType? = 'enumeration'; - values? = ['embed', 'copy', 'move', 'same', 'proto', 'none']; + fieldType? = FInfoFieldType.enumeration; + values? = ['embed', 'copy', 'move', 'same', 'add', 'inSame', 'proto']; readOnly = false; filterable = false; override searchable = () => false; } class CTypeInfo extends FInfo { - fieldType? = 'enumeration'; + fieldType? = FInfoFieldType.enumeration; values? = Array.from(Object.keys(CollectionViewType)); readOnly = false; filterable = false; override searchable = () => false; } class DTypeInfo extends FInfo { - fieldType? = 'enumeration'; + fieldType? = FInfoFieldType.enumeration; values? = Array.from(Object.keys(DocumentType)); override searchable = () => false; } class DateInfo extends FInfo { - fieldType? = 'date'; + constructor(d: string, filterable?: boolean) { + super(d, true); + this.filterable = filterable; + } + fieldType? = FInfoFieldType.date; values?: DateField[] = []; - filterable = true; +} +class RtfInfo extends FInfo { + constructor(d: string, filterable?: boolean) { + super(d, true); + this.filterable = filterable; + } + fieldType? = FInfoFieldType.rtf; } class ListInfo extends FInfo { - fieldType? = 'list'; + fieldType? = FInfoFieldType.list; values?: List<any>[] = []; } type BOOLt = BoolInfo | boolean; @@ -168,6 +189,7 @@ type NUMt = NumInfo | number; type STRt = StrInfo | string; type LISTt = ListInfo | List<any>; type DOCt = DocInfo | Doc; +type RTFt = RtfInfo | RichTextField; type DIMt = DimInfo | typeof DimUnit.Pixel | typeof DimUnit.Ratio; type PEVt = PEInfo | 'none' | 'all'; type COLLt = CTypeInfo | CollectionViewType; @@ -176,19 +198,20 @@ type DATEt = DateInfo | number; type DTYPEt = DTypeInfo | string; export class DocumentOptions { // coordinate and dimensions depending on view - x?: NUMt = new NumInfo('x coordinate of document in a freeform view', false); - y?: NUMt = new NumInfo('y coordinate of document in a freeform view', false); + x?: NUMt = new NumInfo('horizontal coordinate in freeform view', false); + y?: NUMt = new NumInfo('vertical coordinate in freeform view', false); z?: NUMt = new NumInfo('whether document is in overlay (1) or not (0)', false, false, [1, 0]); - overlayX?: NUMt = new NumInfo('x coordinate of document in a overlay view', false); - overlayY?: NUMt = new NumInfo('y coordinate of document in a overlay view', false); + overlayX?: NUMt = new NumInfo('horizontal coordinate in overlay view', false); + overlayY?: NUMt = new NumInfo('vertical coordinate in overlay view', false); + text?: RTFt = new RtfInfo('plain or rich text', true); _dimMagnitude?: NUMt = new NumInfo("magnitude of collectionMulti{row,col} element's width or height", false); _dimUnit?: DIMt = new DimInfo("units of collectionMulti{row,col} element's width or height - 'px' or '*' for pixels or relative units"); - latitude?: NUMt = new NumInfo('latitude coordinate for map views', false); - longitude?: NUMt = new NumInfo('longitude coordinate for map views', false); + latitude?: NUMt = new NumInfo('latitude coordinate', false); + longitude?: NUMt = new NumInfo('longitude coordinate', false); routeCoordinates?: STRt = new StrInfo("stores a route's/direction's coordinates (stringified version)"); // for a route document, this stores the route's coordinates - markerType?: STRt = new StrInfo('Defines the marker type for a pushpin document'); - markerColor?: STRt = new StrInfo('Defines the marker color for a pushpin document'); - map?: STRt = new StrInfo('text location of map'); + markerType?: STRt = new StrInfo('marker type for a pushpin document'); + markerColor?: STRt = new StrInfo('marker color for a pushpin document'); + map?: STRt = new StrInfo('map location name'); map_type?: STRt = new StrInfo('type of map view', false); map_zoom?: NUMt = new NumInfo('zoom of a map view', false); map_pitch?: NUMt = new NumInfo('pitch of a map view', false); @@ -198,11 +221,11 @@ export class DocumentOptions { date_range?: STRt = new StrInfo('date range for calendar', false); wikiData?: STRt = new StrInfo('WikiData ID related to map location'); - description?: STRt = new StrInfo('A description of the document'); - _timecodeToShow?: NUMt = new NumInfo('the time that a document should be displayed (e.g., when an annotation shows up as a video plays)', false); - _timecodeToHide?: NUMt = new NumInfo('the time that a document should be hidden', false); - _width?: NUMt = new NumInfo('displayed width of a document'); - _height?: NUMt = new NumInfo('displayed height of document'); + description?: STRt = new StrInfo('description of document'); + _timecodeToShow?: NUMt = new NumInfo('media timecode when document should appear (e.g., when an annotation shows up as a video plays)', false); + _timecodeToHide?: NUMt = new NumInfo('media timecode when document should disappear', false); + _width?: NUMt = new NumInfo("width of document in container's coordinates"); + _height?: NUMt = new NumInfo("height of document in container's coordiantes"); data_nativeWidth?: NUMt = new NumInfo('native width of data field contents (e.g., the pixel width of an image)', false); data_nativeHeight?: NUMt = new NumInfo('native height of data field contents (e.g., the pixel height of an image)', false); linearBtnWidth?: NUMt = new NumInfo('unexpanded width of a linear menu button (button "width" changes when it expands)', false); @@ -250,19 +273,19 @@ export class DocumentOptions { layout_hideResizeHandles?: BOOLt = new BoolInfo('whether to hide the resize handles when selected'); layout_hideLinkButton?: BOOLt = new BoolInfo('whether the blue link counter button should be hidden'); layout_hideDecorationTitle?: BOOLt = new BoolInfo('whether to suppress the document decortations title when selected'); - _layout_hideContextMenu?: BOOLt = new BoolInfo('whether the context menu can be shown'); + layout_hideContextMenu?: BOOLt = new BoolInfo('whether the context menu can be shown'); layout_borderRounding?: string; _layout_modificationDate?: DATEt = new DateInfo('last modification date of doc layout', false); _layout_nativeDimEditable?: BOOLt = new BoolInfo('native dimensions can be modified using document decoration reizers', false); - _layout_reflowVertical?: BOOLt = new BoolInfo('native height can be changed independent of width by dragging decoration resizers'); - _layout_reflowHorizontal?: BOOLt = new BoolInfo('whether a doc with a native size can be horizonally resized, causing some form of reflow'); + _layout_reflowVertical?: BOOLt = new BoolInfo('permit vertical resizing with content "reflow"'); + _layout_reflowHorizontal?: BOOLt = new BoolInfo('permit horizontal resizing with content reflow'); layout_boxShadow?: string; // box-shadow css string OR "standard" to use dash standard box shadow + layout_maxShown?: NUMt = new NumInfo('maximum number of children to display at one time (see multicolumnview)'); _layout_autoHeight?: BOOLt = new BoolInfo('whether document automatically resizes vertically to display contents'); _layout_curPage?: NUMt = new NumInfo('current page of a PDF or other? paginated document', false); _layout_currentTimecode?: NUMt = new NumInfo('the current timecode of a time-based document (e.g., current time of a video) value is in seconds', false); _layout_centered?: BOOLt = new BoolInfo('whether text should be vertically centered in Doc'); _layout_fitWidth?: BOOLt = new BoolInfo('whether document should scale its contents to fit its rendered width or not (e.g., for PDFviews)'); - _layout_fitContentsToBox?: BOOLt = new BoolInfo('whether a freeformview should zoom/scale to create a shrinkwrapped view of its content'); _layout_fieldKey?: STRt = new StrInfo('the field key containing the current layout definition', false); _layout_enableAltContentUI?: BOOLt = new BoolInfo('whether to show alternate content button'); _layout_isFlashcard?: BOOLt = new BoolInfo('whether comparison node should be displayed as a flashcard'); @@ -295,7 +318,6 @@ export class DocumentOptions { _label_minFontSize?: NUMt = new NumInfo('minimum font size for labelBoxes', false); _label_maxFontSize?: NUMt = new NumInfo('maximum font size for labelBoxes', false); stroke_width?: NUMt = new NumInfo('width of an ink stroke', false); - icon_label?: STRt = new StrInfo('label to use for a fontIcon doc (otherwise, the title is used)', false); mediaState?: STRt = new StrInfo(`status of audio/video media document: ${media_state.PendingRecording}, ${media_state.Recording}, ${media_state.Paused}, ${media_state.Playing}`, false); recording?: BOOLt = new BoolInfo('whether WebCam is recording or not'); slides?: DOCt = new DocInfo('presentation slide associated with video recording (bcz: should be renamed!!)'); @@ -374,6 +396,7 @@ export class DocumentOptions { _freeform_panY?: NUMt = new NumInfo('vertical pan location of a freeform view'); _freeform_noAutoPan?: BOOLt = new BoolInfo('disables autopanning when this item is dragged'); _freeform_noZoom?: BOOLt = new BoolInfo('disables zooming (used by Pile docs)'); + _freeform_fitContentsToBox?: BOOLt = new BoolInfo('whether a freeformview should zoom/scale to create a shrinkwrapped view of its content'); //BUTTONS buttonText?: string; @@ -460,7 +483,6 @@ export class DocumentOptions { sidebar_type_collection?: string; // collection type of text sidebar data_dashboards?: List<any>; // list of dashboards used in shareddocs; - text?: string; textTransform?: string; letterSpacing?: string; iconTemplate?: string; // name of icon template style @@ -630,8 +652,8 @@ export namespace Docs { [ DocumentType.LABEL, { - layout: { view: LabelBox, dataField: defaultDataKey }, - options: { _singleLine: true }, + layout: { view: LabelBox, dataField: 'title' }, + options: { _singleLine: true, layout_nativeDimEditable: true, layout_reflowHorizontal: true, layout_reflowVertical: true }, }, ], [ @@ -662,8 +684,8 @@ export namespace Docs { [ DocumentType.BUTTON, { - layout: { view: LabelBox, dataField: 'onClick' }, - options: {}, + layout: { view: LabelBox, dataField: 'title' }, + options: { layout_nativeDimEditable: true, layout_reflowHorizontal: true, layout_reflowVertical: true }, }, ], [ @@ -677,7 +699,7 @@ export namespace Docs { DocumentType.FONTICON, { layout: { view: FontIconBox, dataField: 'icon' }, - options: { defaultDoubleClick: 'ignore', waitForDoubleClickToClick: 'never', layout_hideLinkButton: true, _width: 40, _height: 40 }, + options: { defaultDoubleClick: 'ignore', waitForDoubleClickToClick: 'never', layout_hideContextMenu: true, layout_hideLinkButton: true, _width: 40, _height: 40 }, }, ], [ @@ -729,16 +751,7 @@ export namespace Docs { { data: '', layout: { view: ComparisonBox, dataField: defaultDataKey }, - options: { - _layout_isFlashcard: false, - backgroundColor: 'gray', - dropAction: 'move', - waitForDoubleClickToClick: 'always', - layout_reflowHorizontal: true, - layout_reflowVertical: true, - layout_nativeDimEditable: true, - systemIcon: 'BsLayoutSplit', - }, + options: { backgroundColor: 'gray', dropAction: dropActionType.move, waitForDoubleClickToClick: 'always', layout_reflowHorizontal: true, layout_reflowVertical: true, layout_nativeDimEditable: true, systemIcon: 'BsLayoutSplit' }, }, ], [ @@ -1012,6 +1025,15 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.PRES), new List<Doc>(), options); } + /** + * Creates a Doc to edit a script and write the compiled script into the specified field. + * Typically, this would be used to create a template that can then be applied to some other Doc + * in order to customize a behavior, such as onClick. + * @param script + * @param options + * @param fieldKey the field that the compiled script is written into. + * @returns the Scripting Doc + */ export function ScriptingDocument(script: Opt<ScriptField> | null, options: DocumentOptions = {}, fieldKey?: string) { return InstanceFromProto(Prototypes.get(DocumentType.SCRIPTING), script ? script : undefined, { ...options, layout: fieldKey ? ScriptingBox.LayoutString(fieldKey) : undefined }); } @@ -1192,7 +1214,7 @@ export namespace Docs { return InstanceFromProto( Prototypes.get(DocumentType.COL), new List(documents), - { backgroundColor: 'transparent', dropAction: 'move', _forceActive: true, _freeform_noZoom: true, _freeform_noAutoPan: true, ...options, _type_collection: CollectionViewType.Pile }, + { backgroundColor: 'transparent', dropAction: dropActionType.move, _forceActive: true, _freeform_noZoom: true, _freeform_noAutoPan: true, ...options, _type_collection: CollectionViewType.Pile }, id ); } @@ -1868,8 +1890,7 @@ export namespace DocUtils { const hasContextAnchor = LinkManager.Links(doc).some(l => (l.link_anchor_2 === doc && Cast(l.link_anchor_1, Doc, null)?.annotationOn === context) || (l.link_anchor_1 === doc && Cast(l.link_anchor_2, Doc, null)?.annotationOn === context)); if (context && !hasContextAnchor && (context.type === DocumentType.VID || context.type === DocumentType.WEB || context.type === DocumentType.PDF || context.type === DocumentType.IMG)) { const pushpin = Docs.Create.FontIconDocument({ - title: 'pushpin', - icon_label: '', + title: '', annotationOn: Cast(doc.annotationOn, Doc, null), followLinkToggle: true, icon: 'map-pin', @@ -1961,27 +1982,30 @@ export namespace DocUtils { } } - export function GetNewTextDoc(title: string, x: number, y: number, width?: number, height?: number, noMargins?: boolean, annotationOn?: Doc, backgroundColor?: string) { + export function GetNewTextDoc(title: string, x: number, y: number, width?: number, height?: number, annotationOn?: Doc, backgroundColor?: string) { + const defaultTextTemplate = DocCast(Doc.UserDoc().defaultTextLayout); const tbox = Docs.Create.TextDocument('', { - _xMargin: noMargins ? 0 : undefined, - _yMargin: noMargins ? 0 : undefined, annotationOn, backgroundColor, - _width: width || 200, - _height: 35, - x: x, - y: y, - _layout_centered: BoolCast(Doc.UserDoc().layout_centered), - _layout_fitWidth: true, - _layout_autoHeight: true, - _layout_enableAltContentUI: BoolCast(Doc.UserDoc().defaultToFlashcards), + x, + y, title, + ...(defaultTextTemplate + ? {} // if the new doc will inherit from a template, don't set any layout fields since that would block the inheritance + : { + _width: width || 200, + _height: 35, + _layout_centered: BoolCast(Doc.UserDoc()._layout_centered), + _layout_fitWidth: true, + _layout_autoHeight: true, + _layout_enableAltContentUI: BoolCast(Doc.UserDoc().defaultToFlashcards), + }), }); - const template = Doc.UserDoc().defaultTextLayout; - if (template instanceof Doc) { - tbox._width = NumCast(template._width); - tbox.layout_fieldKey = 'layout_' + StrCast(template.title); - Doc.GetProto(tbox)[StrCast(tbox.layout_fieldKey)] = template; + + if (defaultTextTemplate) { + tbox.layout_fieldKey = 'layout_' + StrCast(defaultTextTemplate.title); + Doc.GetProto(tbox)[StrCast(tbox.layout_fieldKey)] = defaultTextTemplate; // set the text doc's layout to render with the text template + tbox[DocData].proto = defaultTextTemplate; // and also set the text doc to inherit from the template (this allows the template to specify default field values) } return tbox; } diff --git a/src/client/goldenLayout.js b/src/client/goldenLayout.js index 2b94d35ee..cb1dfd76a 100644 --- a/src/client/goldenLayout.js +++ b/src/client/goldenLayout.js @@ -4727,7 +4727,7 @@ */ } else { type = isVertical ? 'column' : 'row'; - if (this.parent.contentItems.length === 1) { + if (this.parent.contentItems.length === 1 && this.contentItems.length === 1) { let grandparent = this.parent.parent; let correctRowOrCol = this.layoutManager.createContentItem({ type: type }, this); grandparent.replaceChild(this.parent, correctRowOrCol); diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 95e7841bf..958240f94 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -2,6 +2,7 @@ import { observable, reaction, runInAction } from "mobx"; import * as rp from 'request-promise'; import { OmitKeys, Utils } from "../../Utils"; import { Doc, DocListCast, DocListCastAsync, Opt } from "../../fields/Doc"; +import { DocData } from "../../fields/DocSymbols"; import { InkTool } from "../../fields/InkField"; import { List } from "../../fields/List"; import { PrefetchProxy } from "../../fields/Proxy"; @@ -14,14 +15,14 @@ import { SetCachedGroups, SharingPermissions } from "../../fields/util"; import { GestureUtils } from "../../pen-gestures/GestureUtils"; import { DocServer } from "../DocServer"; import { CollectionViewType, DocumentType } from "../documents/DocumentTypes"; -import { DocUtils, Docs, DocumentOptions, FInfo } from "../documents/Documents"; +import { DocUtils, Docs, DocumentOptions, FInfo, FInfoFieldType } from "../documents/Documents"; import { DashboardView } from "../views/DashboardView"; import { OverlayView } from "../views/OverlayView"; import { CollectionTreeView, TreeViewType } from "../views/collections/CollectionTreeView"; import { Colors } from "../views/global/globalEnums"; import { media_state } from "../views/nodes/AudioBox"; import { OpenWhere } from "../views/nodes/DocumentView"; -import { ButtonType } from "../views/nodes/FontIconBox/FontIconBox"; +import { ButtonType, FontIconBox } from "../views/nodes/FontIconBox/FontIconBox"; import { ImportElementBox } from "../views/nodes/importBox/ImportElementBox"; import { DragManager, dropActionType } from "./DragManager"; import { MakeTemplate } from "./DropConverter"; @@ -31,6 +32,8 @@ import { ScriptingGlobals } from "./ScriptingGlobals"; import { ColorScheme } from "./SettingsManager"; import { SnappingManager } from "./SnappingManager"; import { UndoManager } from "./UndoManager"; +import { LabelBox } from "../views/nodes/LabelBox"; +import { ImageBox } from "../views/nodes/ImageBox"; interface Button { // DocumentOptions fields a button can set @@ -65,7 +68,7 @@ export class CurrentUserUtils { const userTemplates = DocListCast(userDocTemplates?.data).filter(doc => !Doc.IsSystem(doc)); const reqdOpts:DocumentOptions = { title: "User Tools", _xMargin: 0, _layout_showTitle: "title", _chromeHidden: true, hidden: false, - _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, isSystem: true, _forceActive: true, + _dragOnlyWithinContainer: true, layout_hideContextMenu: true, isSystem: true, _forceActive: true, _layout_autoHeight: true, _width: 500, _height: 300, _layout_fitWidth: true, _columnWidth: 35, ignoreClick: true, _lockedPosition: true, }; const reqdScripts = { dropConverter : "convertToButtons(dragData)" }; @@ -102,9 +105,12 @@ export class CurrentUserUtils { { opts: { title: "onCheckedClick"}, script: "console.log(heading, checked, containingTreeView)"}, ]; const reqdClickList = reqdTempOpts.map(opts => { + const title = opts.opts.title?.toString(); const allOpts = {...reqdClickOpts, ...opts.opts}; - const clickDoc = tempClicks ? DocListCast(tempClicks.data).find(doc => doc.title === opts.opts.title): undefined; - return DocUtils.AssignOpts(clickDoc, allOpts) ?? MakeTemplate(Docs.Create.ScriptingDocument(ScriptField.MakeScript(opts.script, {heading:Doc.name, checked:"boolean", containingTreeView:Doc.name}), allOpts), true, opts.opts.title?.toString()); + const clickDoc = tempClicks ? DocListCast(tempClicks.data).find(doc => doc.title === title): undefined; + const script = ScriptField.MakeScript(opts.script, {heading:Doc.name, checked:"boolean", containingTreeView:Doc.name}); + const scriptDoc = Docs.Create.ScriptingDocument(script, allOpts, title) + return DocUtils.AssignOpts(clickDoc, allOpts) ?? MakeTemplate(scriptDoc); }); const reqdOpts:DocumentOptions = {title: "click editor templates", _height:75, isSystem: true}; @@ -115,14 +121,14 @@ export class CurrentUserUtils { static setupNoteTemplates(doc: Doc, field="template_notes") { const tempNotes = DocCast(doc[field]); const reqdTempOpts:DocumentOptions[] = [ - { noteType: "Postit", backgroundColor: "yellow", icon: "sticky-note"}, - { noteType: "Idea", backgroundColor: "pink", icon: "lightbulb" }, - { noteType: "Topic", backgroundColor: "lightblue", icon: "book-open" }]; - const reqdNoteList = reqdTempOpts.map(opts => { - const reqdOpts = {...opts, isSystem:true, title: "text", width:200, layout_autoHeight: true, layout_fitWidth: true}; - const noteType = tempNotes ? DocListCast(tempNotes.data).find(doc => doc.noteType === opts.noteType): undefined; - return DocUtils.AssignOpts(noteType, reqdOpts) ?? MakeTemplate(Docs.Create.TextDocument("",reqdOpts), true, opts.noteType??"Note"); - }); + { title: "Postit", backgroundColor: "yellow", icon: "sticky-note"}, + { title: "Idea", backgroundColor: "pink", icon: "lightbulb" }, + { title: "Topic", backgroundColor: "lightblue", icon: "book-open" }]; + const reqdNoteList = [...reqdTempOpts.map(opts => { + const reqdOpts = {...opts, isSystem:true, width:200, layout_autoHeight: true, layout_fitWidth: true}; + const noteTemp = tempNotes ? DocListCast(tempNotes.data).find(doc => doc.title === opts.title): undefined; + return DocUtils.AssignOpts(noteTemp, reqdOpts) ?? MakeTemplate(Docs.Create.TextDocument("",reqdOpts)); + }), ... DocListCast(tempNotes?.data).filter(note => !reqdTempOpts.find(reqd => reqd.title === note.title))]; const reqdOpts:DocumentOptions = { title: "Note Layouts", _height: 75, isSystem: true }; return DocUtils.AssignOpts(tempNotes, reqdOpts, reqdNoteList) ?? (doc[field] = Docs.Create.TreeDocument(reqdNoteList, reqdOpts)); @@ -146,23 +152,23 @@ export class CurrentUserUtils { const templateIconsDoc = DocUtils.AssignOpts(DocCast(doc[field]), reqdOpts) ?? (doc[field] = Docs.Create.TreeDocument([], reqdOpts)); const makeIconTemplate = (type: DocumentType | undefined, templateField: string, opts:DocumentOptions) => { - const iconFieldName = "icon" + (type ? "_" + type : ""); - const curIcon = DocCast(templateIconsDoc[iconFieldName]); + const title = "icon" + (type ? "_" + type : ""); + const curIcon = DocCast(templateIconsDoc[title]); let creator = labelBox; switch (opts.iconTemplate) { case DocumentType.IMG : creator = imageBox; break; case DocumentType.FONTICON: creator = fontBox; break; } - const allopts = {isSystem: true, onClickScriptDisable: "never", ...opts}; + const allopts = {isSystem: true, onClickScriptDisable: "never", ...opts, title}; return DocUtils.AssignScripts( (curIcon?.iconTemplate === opts.iconTemplate ? - DocUtils.AssignOpts(curIcon, allopts):undefined) ?? ((templateIconsDoc[iconFieldName] = MakeTemplate(creator(allopts), true, iconFieldName, templateField))), + DocUtils.AssignOpts(curIcon, allopts):undefined) ?? ((templateIconsDoc[title] = MakeTemplate(creator(allopts, templateField)))), {onClick:"deiconifyView(documentView)", onDoubleClick: "deiconifyViewToLightbox(documentView)", }); }; - const labelBox = (opts: DocumentOptions, data?:string) => Docs.Create.LabelDocument({ - textTransform: "unset", letterSpacing: "unset", _singleLine: false, _label_minFontSize: 14, _label_maxFontSize: 14, layout_borderRounding: "5px", _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, ...opts + const labelBox = (opts: DocumentOptions, fieldKey:string) => Docs.Create.LabelDocument({ + layout: LabelBox.LayoutString(fieldKey), textTransform: "unset", letterSpacing: "unset", _singleLine: false, _label_minFontSize: 14, _label_maxFontSize: 14, layout_borderRounding: "5px", _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, ...opts }); - const imageBox = (opts: DocumentOptions, url?:string) => Docs.Create.ImageDocument(url ?? "http://www.cs.brown.edu/~bcz/noImage.png", { "icon_nativeWidth": 360 / 4, "icon_nativeHeight": 270 / 4, iconTemplate:DocumentType.IMG, _width: 360 / 4, _height: 270 / 4, _layout_showTitle: "title", ...opts }); - const fontBox = (opts:DocumentOptions, data?:string) => Docs.Create.FontIconDocument({ _nativeHeight: 30, _nativeWidth: 30, _width: 30, _height: 30, ...opts }); + const imageBox = (opts: DocumentOptions, fieldKey:string) => Docs.Create.ImageDocument( "http://www.cs.brown.edu/~bcz/noImage.png", { layout:ImageBox.LayoutString(fieldKey), "icon_nativeWidth": 360 / 4, "icon_nativeHeight": 270 / 4, iconTemplate:DocumentType.IMG, _width: 360 / 4, _height: 270 / 4, _layout_showTitle: "title", ...opts }); + const fontBox = (opts:DocumentOptions, fieldKey:string) => Docs.Create.FontIconDocument({ layout:FontIconBox.LayoutString(fieldKey), _nativeHeight: 30, _nativeWidth: 30, _width: 30, _height: 30, ...opts }); const iconTemplates = [ makeIconTemplate(undefined, "title", { iconTemplate:DocumentType.LABEL, backgroundColor: "dimgray"}), makeIconTemplate(DocumentType.AUDIO, "title", { iconTemplate:DocumentType.LABEL, backgroundColor: "lightgreen"}), @@ -173,7 +179,7 @@ export class CurrentUserUtils { makeIconTemplate(DocumentType.COL, "icon", { iconTemplate:DocumentType.IMG}), makeIconTemplate(DocumentType.COL, "icon", { iconTemplate:DocumentType.IMG}), makeIconTemplate(DocumentType.VID, "icon", { iconTemplate:DocumentType.IMG}), - makeIconTemplate(DocumentType.BUTTON,"data", { iconTemplate:DocumentType.FONTICON}), + makeIconTemplate(DocumentType.BUTTON,"title", { iconTemplate:DocumentType.FONTICON}), //nasty hack .. templates are looked up exclusively by type -- but we want a template for a document with a certain field (transcription) .. so this hack and the companion hack in createCustomView does this for now makeIconTemplate("transcription" as any, "transcription", { iconTemplate:DocumentType.LABEL, backgroundColor: "orange" }), //makeIconTemplate(DocumentType.PDF, "icon", {iconTemplate:DocumentType.IMG}, (opts) => imageBox("http://www.cs.brown.edu/~bcz/noImage.png", opts)) @@ -208,7 +214,7 @@ export class CurrentUserUtils { }; const headerBtnHgt = 10; const headerTemplate = (opts:DocumentOptions) => { - const header = Docs.Create.RTFDocument(new RichTextField(JSON.stringify(json), ""), { ...opts, title: "text", + const header = Docs.Create.RTFDocument(new RichTextField(JSON.stringify(json), ""), { ...opts, title: "Untitled Header", 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)'/>` + @@ -221,7 +227,7 @@ export class CurrentUserUtils { // " <FormattedTextBox {...props} fieldKey={'header'} dontSelectOnLoad={'true'} ignoreAutoHeight={'true'} pointerEvents='{this._headerPointerEvents||`none`}' fontSize='{this._headerFontSize}px' height='{this._headerHeight}px' background='{this._headerColor||this.target.mySharedDocs.userColor}' />" + // " <FormattedTextBox {...props} fieldKey={'text'} position='absolute' top='{(this._headerHeight)*scale}px' height='calc({100/scale}% - {this._headerHeight}px)'/>" + // "</div>"; - MakeTemplate(Doc.GetProto(header), true, "Untitled Header"); + MakeTemplate(Doc.GetProto(header)); return header; } const slideView = (opts:DocumentOptions) => { @@ -229,11 +235,105 @@ export class CurrentUserUtils { [ Docs.Create.MulticolumnDocument([], { title: "hero", _height: 200, isSystem: true }), Docs.Create.TextDocument("", { title: "text", _layout_fitWidth:true, _height: 100, isSystem: true, _text_fontFamily: StrCast(Doc.UserDoc().fontFamily), _text_fontSize: StrCast(Doc.UserDoc().fontSize) }) - ], opts); + ], {...opts, title: "Untitled Slide View"}); - MakeTemplate(Doc.GetProto(slide), true, "Untitled Slide View"); + MakeTemplate(Doc.GetProto(slide)); + return slide; + } + const plotlyApi = () => { + var plotly = Doc.MyPublishedDocs.find(doc => doc.title === "@plotly"); + if (!plotly) { + const plotly = Docs.Create.TextDocument( + `await import("https://cdn.plot.ly/plotly-2.27.0.min.js"); + Plotly.newPlot(dashDiv.id, [ --DOCDATA-- ])` + , {title: "@plotly", title_custom: true, _layout_showTitle:"title", _width:300,_height:400}); + Doc.AddToMyPublished(plotly); + } + } + const plotlyView = (opts:DocumentOptions) => { + const rtfield = new RichTextField(JSON.stringify( + {doc: {type:"doc",content:[ + {type:"code_block",content:[ + {type:"text",text:"^@plotly"}, + {type:"text",text:"\n"}, + {type:"text",text:"\n{"}, + {type:"text",text:"\n x: [1,2,3,5,19],"}, + {type:"text",text:"\n y: [1, 9, 15, 12,3],"}, + {type:"text",text:"\n mode: 'lines+markers', "}, + {type:"text",text:"\n type: 'scatter'"}, + {type:"text",text:"\n}"} + ]} + ]}, + selection:{type:"text",anchor:2,head:2}}), + `^@plotly + { + x: [1,2,3,5,19], + y: [1, 9, 15, 12,3], + mode: 'lines+markers', + type: 'scatter' + }`); + const slide = Docs.Create.TextDocument("", opts); + slide[DocData].text = rtfield; + slide[DocData].layout_textPainted = `<CollectionView {...props} fieldKey={'text'}/>`; + slide[DocData]._type_collection = CollectionViewType.Freeform; + slide.onPaint = ScriptField.MakeScript(`toggleDetail(documentView, "textPainted")`, {documentView:"any"}); + return slide; + } + const mermaidsApi = () => { + var mermaids = Doc.MyPublishedDocs.find(doc => doc.title === "@mermaids"); + if (!mermaids) { + const mermaids = Docs.Create.TextDocument( + `const mdef = (await import("https://cdn.jsdelivr.net/npm/mermaid\@10.8.0/dist/mermaid.esm.min.mjs")).default; + window["callb"] = (x) => { + alert(x); + } + mdef.initialize({ + securityLevel : "loose", + startOnLoad: true, + flowchart: { useMaxWidth: true, htmlLabels: true, curve: 'cardinal' }, + }); + const mermaid = async (str) => (await mdef.render("graph"+Date.now(),str)); + const {svg, bindFunctions} = await mermaid(\`--DOCDATA--\`); + dashDiv.innerHTML = svg; + if (bindFunctions) { + bindFunctions(dashDiv); + }` + , {title: "@mermaids", title_custom: true, _layout_showTitle:"title", _width:300,_height:400}); + Doc.AddToMyPublished(mermaids); + } + } + const mermaidsView = (opts:DocumentOptions) => { + const rtfield = new RichTextField(JSON.stringify( + {doc: {type:"doc",content:[ + {type:"code_block",content:[ + {type:"text",text:"^@mermaids"}, + {type:"text",text:"\n\n"}, + {type:"text",text:"pie "}, + {type:"text",text:"title"}, + {type:"text",text:" "}, + {type:"text",text:"Minerals in my tap water"}, + {type:"text",text:"\n \"Calcium\" : "}, + {type:"dashField",attrs:{fieldKey:"calcium",docId:"","hideKey":false,editable:true}}, + {type:"text",text:"\n \"Potassium\" : "}, + {type:"dashField",attrs:{fieldKey:"pot",docId:"",hideKey:false,editable:true}}, + {type:"text",text:"\n \"Magnesium\" : 10.01"} + ]} + ]}, + selection:{type:"text",anchor:109,head:109} + }), + `^@mermaids +pie title Minerals in my tap water + "Calcium" : 42.96 + "Potassium" : 50 + "Magnesium" : 10.01`); + const slide = Docs.Create.TextDocument("", opts); + slide[DocData].text = rtfield; + slide[DocData].layout_textPainted = `<CollectionView {...props} fieldKey={'text'}/>`; + slide[DocData]._type_collection = CollectionViewType.Freeform; + slide.onPaint = ScriptField.MakeScript(`toggleDetail(documentView, "textPainted")`, {documentView:"any"}); return slide; } + const apis = [plotlyApi(), mermaidsApi()] const emptyThings:{key:string, // the field name where the empty thing will be stored opts:DocumentOptions, // the document options that are required for the empty thing funcs?:{[key:string]: any}, // computed fields that are rquired for the empth thing @@ -258,14 +358,16 @@ export class CurrentUserUtils { {key: "DataViz", creator: opts => Docs.Create.DataVizDocument("/users/rz/Downloads/addresses.csv", opts), opts: { _width: 300, _height: 300 }}, {key: "Header", creator: headerTemplate, opts: { _width: 300, _height: 70, _headerPointerEvents: "all", _headerHeight: 12, _headerFontSize: 9, _layout_autoHeight: true, treeView_HideUnrendered: true}}, {key: "ViewSlide", creator: slideView, opts: { _width: 400, _height: 300, _xMargin: 3, _yMargin: 3,}}, - {key: "Trail", creator: Docs.Create.PresDocument, opts: { _width: 400, _height: 30, _type_collection: CollectionViewType.Stacking, dropAction: "embed" as dropActionType, treeView_HideTitle: true, _layout_fitWidth:true, layout_boxShadow: "0 0" }}, + {key: "Trail", creator: Docs.Create.PresDocument, opts: { _width: 400, _height: 30, _type_collection: CollectionViewType.Stacking, dropAction: dropActionType.embed, treeView_HideTitle: true, _layout_fitWidth:true, layout_boxShadow: "0 0" }}, {key: "Tab", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 500, _height: 800, _layout_fitWidth: true, _freeform_backgroundGrid: true, }}, {key: "Slide", creator: opts => Docs.Create.TreeDocument([], opts), opts: { _width: 300, _height: 200, _type_collection: CollectionViewType.Tree, treeView_HasOverlay: true, _text_fontSize: "20px", _layout_autoHeight: true, - dropAction:'move', treeView_Type: TreeViewType.outline, + dropAction:dropActionType.move, treeView_Type: TreeViewType.outline, backgroundColor: "white", _xMargin: 0, _yMargin: 0, _createDocOnCR: true }, funcs: {title: 'this.text?.Text'}}, - ]; + {key: "Mermaids", creator: mermaidsView, opts: { _width: 300, _height: 300, }}, + {key: "Plotly", creator: plotlyView, opts: { _width: 300, _height: 300, }}, + ]; emptyThings.forEach(thing => DocUtils.AssignDocField(doc, "empty"+thing.key, (opts) => thing.creator(opts), {...standardOps(thing.key), ...thing.opts}, undefined, thing.scripts, thing.funcs)); @@ -273,8 +375,10 @@ export class CurrentUserUtils { { toolTip: "Tap or drag to create a note", title: "Note", icon: "sticky-note", dragFactory: doc.emptyNote as Doc, clickFactory: DocCast(doc.emptyNote)}, { toolTip: "Tap or drag to create a flashcard", title: "Flashcard", icon: "id-card", dragFactory: doc.emptyFlashcard as Doc, clickFactory: DocCast(doc.emptyFlashcard)}, { toolTip: "Tap or drag to create an equation", title: "Math", icon: "calculator", dragFactory: doc.emptyEquation as Doc, clickFactory: DocCast(doc.emptyEquation)}, + { toolTip: "Tap or drag to create a mermaid node", title: "Mermaids", icon: "rocket", dragFactory: doc.emptyMermaids as Doc, clickFactory: DocCast(doc.emptyMermaids)}, + { toolTip: "Tap or drag to create a plotly node", title: "Plotly", icon: "rocket", dragFactory: doc.emptyPlotly as Doc, clickFactory: DocCast(doc.emptyMermaids)}, { toolTip: "Tap or drag to create a physics simulation",title: "Simulation", icon: "rocket",dragFactory: doc.emptySimulation as Doc, clickFactory: DocCast(doc.emptySimulation), funcs: { hidden: "IsNoviceMode()"}}, - { toolTip: "Tap or drag to create a note board", title: "Notes", icon: "folder", dragFactory: doc.emptyNoteboard as Doc, clickFactory: DocCast(doc.emptyNoteboard)}, + { toolTip: "Tap or drag to create a note board", title: "Notes", icon: "book", dragFactory: doc.emptyNoteboard as Doc, clickFactory: DocCast(doc.emptyNoteboard)}, { toolTip: "Tap or drag to create a collection", title: "Col", icon: "folder", dragFactory: doc.emptyCollection as Doc, clickFactory: DocCast(doc.emptyTab)}, { toolTip: "Tap or drag to create a webpage", title: "Web", icon: "globe-asia", dragFactory: doc.emptyWebpage as Doc, clickFactory: DocCast(doc.emptyWebpage)}, { toolTip: "Tap or drag to create a comparison box", title: "Compare", icon: "columns", dragFactory: doc.emptyComparison as Doc, clickFactory: DocCast(doc.emptyComparison)}, @@ -282,10 +386,10 @@ export class CurrentUserUtils { { toolTip: "Tap or drag to create a map", title: "Map", icon: "map-marker-alt", dragFactory: doc.emptyMap as Doc, clickFactory: DocCast(doc.emptyMap)}, { toolTip: "Tap or drag to create a screen grabber", title: "Grab", icon: "photo-video", dragFactory: doc.emptyScreengrab as Doc, clickFactory: DocCast(doc.emptyScreengrab), openFactoryLocation: OpenWhere.overlay, funcs: { hidden: "IsNoviceMode()"}}, { toolTip: "Tap or drag to create a WebCam recorder", title: "WebCam", icon: "photo-video", dragFactory: doc.emptyWebCam as Doc, clickFactory: DocCast(doc.emptyWebCam), openFactoryLocation: OpenWhere.overlay, funcs: { hidden: "IsNoviceMode()"}}, - { toolTip: "Tap or drag to create a button", title: "Button", icon: "bolt", dragFactory: doc.emptyButton as Doc, clickFactory: DocCast(doc.emptyButton)}, + { toolTip: "Tap or drag to create a button", title: "Button", icon: "circle", dragFactory: doc.emptyButton as Doc, clickFactory: DocCast(doc.emptyButton)}, { toolTip: "Tap or drag to create a scripting box", title: "Script", icon: "terminal", dragFactory: doc.emptyScript as Doc, clickFactory: DocCast(doc.emptyScript), funcs: { hidden: "IsNoviceMode()"}}, { toolTip: "Tap or drag to create a data viz node", title: "DataViz", icon: "chart-bar", dragFactory: doc.emptyDataViz as Doc, clickFactory: DocCast(doc.emptyDataViz)}, - { toolTip: "Tap or drag to create a bullet slide", title: "PPT Slide", icon: "file", dragFactory: doc.emptySlide as Doc, clickFactory: DocCast(doc.emptySlide), openFactoryLocation: OpenWhere.overlay, funcs: { hidden: "IsNoviceMode()"}}, + { toolTip: "Tap or drag to create a bullet slide", title: "PPT Slide", icon: "person-chalkboard", dragFactory: doc.emptySlide as Doc, clickFactory: DocCast(doc.emptySlide), openFactoryLocation: OpenWhere.overlay, funcs: { hidden: "IsNoviceMode()"}}, { toolTip: "Tap or drag to create a view slide", title: "View Slide", icon: "address-card", dragFactory: doc.emptyViewSlide as Doc,clickFactory: DocCast(doc.emptyViewSlide),openFactoryLocation: OpenWhere.overlay,funcs: { hidden: "IsNoviceMode()"}}, { toolTip: "Tap or drag to create a data note", title: "DataNote", icon: "window-maximize",dragFactory: doc.emptyHeader as Doc,clickFactory: DocCast(doc.emptyHeader), openFactoryAsDelegate: true, funcs: { hidden: "IsNoviceMode()"} }, { toolTip: "Toggle a Calculator REPL", title: "replviewer", icon: "calculator", clickFactory: '<ScriptingRepl />' as any, openFactoryLocation: OpenWhere.overlay}, // hack: clickFactory is not a Doc but will get interpreted as a custom UI by the openDoc() onClick script @@ -303,16 +407,16 @@ export class CurrentUserUtils { const creatorBtns = CurrentUserUtils.creatorBtnDescriptors(doc).map((reqdOpts) => { const btn = dragCreatorDoc ? DocListCast(dragCreatorDoc.data).find(doc => doc.title === reqdOpts.title): undefined; const opts:DocumentOptions = {...OmitKeys(reqdOpts, ["funcs", "scripts", "backgroundColor"]).omit, - _width: 60, _height: 60, _layout_hideContextMenu: true, _dragOnlyWithinContainer: true, + _width: 60, _height: 60, _dragOnlyWithinContainer: true, btnType: ButtonType.ToolButton, backgroundColor: reqdOpts.backgroundColor ?? Colors.DARK_GRAY, color: Colors.WHITE, isSystem: true, }; return DocUtils.AssignScripts(DocUtils.AssignOpts(btn, opts) ?? Docs.Create.FontIconDocument(opts), reqdOpts.scripts, reqdOpts.funcs); }); const reqdOpts:DocumentOptions = { - title: "Document Creators", _layout_showTitle: "title", _xMargin: 0, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, _chromeHidden: true, isSystem: true, + title: "Document Creators", _layout_showTitle: "title", _xMargin: 0, _dragOnlyWithinContainer: true, layout_hideContextMenu: true, _chromeHidden: true, isSystem: true, _layout_autoHeight: true, _width: 500, _height: 300, _layout_fitWidth: true, _columnWidth: 40, ignoreClick: true, _lockedPosition: true, _forceActive: true, - childDragAction: 'embed' + childDragAction: dropActionType.embed }; const reqdScripts = { dropConverter: "convertToButtons(dragData)" }; return DocUtils.AssignScripts(DocUtils.AssignOpts(dragCreatorDoc, reqdOpts, creatorBtns) ?? Docs.Create.MasonryDocument(creatorBtns, reqdOpts), reqdScripts); @@ -348,13 +452,13 @@ export class CurrentUserUtils { const btnDoc = myLeftSidebarMenu ? DocListCast(myLeftSidebarMenu.data).find(doc => doc.title === title) : undefined; const reqdBtnOpts:DocumentOptions = { title, icon, target, toolTip, hidden, btnType: ButtonType.MenuButton, isSystem: true, undoIgnoreFields: new List<string>(['height', 'data_columnHeaders']), dontRegisterView: true, - _width: 60, _height: 60, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, + _width: 60, _height: 60, _dragOnlyWithinContainer: true, }; return DocUtils.AssignScripts(DocUtils.AssignOpts(btnDoc, reqdBtnOpts) ?? Docs.Create.FontIconDocument(reqdBtnOpts), scripts, funcs); }); const reqdStackOpts:DocumentOptions ={ - title: "menuItemPanel", childDragAction: "same", layout_boxShadow: "rgba(0,0,0,0)", dontRegisterView: true, ignoreClick: true, + title: "menuItemPanel", childDragAction: dropActionType.same, layout_boxShadow: "rgba(0,0,0,0)", dontRegisterView: true, ignoreClick: true, _chromeHidden: true, _gridGap: 0, _yMargin: 0, _xMargin: 0, _layout_autoHeight: false, _width: 60, _columnWidth: 60, _lockedPosition: true, isSystem: true, }; return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(items??[], opts), reqdStackOpts, menuBtns, { dropConverter: "convertToButtons(dragData)" }); @@ -437,14 +541,13 @@ export class CurrentUserUtils { /// Search option on the left side button panel static setupSearcher(doc: Doc, field:string) { return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.SearchDocument(opts), { - dontRegisterView: true, backgroundColor: "dimgray", ignoreClick: true, title: "Search Panel", isSystem: true, childDragAction: "embed", + dontRegisterView: true, backgroundColor: "dimgray", ignoreClick: true, title: "Search Panel", isSystem: true, childDragAction: dropActionType.embed, _lockedPosition: true, _type_collection: CollectionViewType.Schema }); } /// Initializes the panel of draggable tools that is opened from the left sidebar. static setupToolsBtnPanel(doc: Doc, field:string) { - const myTools = DocCast(doc[field]); - const allTools = DocListCast(myTools?.data); + const allTools = DocListCast(DocCast(doc[field])?.data); const creatorBtns = CurrentUserUtils.setupCreatorButtons(doc, allTools?.length ? allTools[0]:undefined); const userTools = allTools && allTools?.length > 1 ? allTools[1]:undefined; const userBtns = CurrentUserUtils.setupUserDocumentCreatorButtons(doc, userTools); @@ -452,7 +555,7 @@ export class CurrentUserUtils { const reqdToolOps:DocumentOptions = { title: "My Tools", isSystem: true, ignoreClick: true, layout_boxShadow: "0 0", layout_explainer: "This is a palette of documents that can be created.", - _layout_showTitle: "title", _width: 500, _yMargin: 20, _lockedPosition: true, _forceActive: true, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, _chromeHidden: true, + _layout_showTitle: "title", _width: 500, _yMargin: 20, _lockedPosition: true, _forceActive: true, _dragOnlyWithinContainer: true, layout_hideContextMenu: true, _chromeHidden: true, }; return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(items??[], opts), reqdToolOps, [creatorBtns, userBtns]); } @@ -463,7 +566,7 @@ export class CurrentUserUtils { const newDashboard = `createNewDashboard()`; - const reqdBtnOpts:DocumentOptions = { _forceActive: true, _width: 30, _height: 30, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, + const reqdBtnOpts:DocumentOptions = { _forceActive: true, _width: 30, _height: 30, _dragOnlyWithinContainer: true, title: "new Dash", btnType: ButtonType.ClickButton, toolTip: "Create new dashboard", buttonText: "New trail", icon: "plus", isSystem: true }; const reqdBtnScript = {onClick: newDashboard,} const newDashboardButton = DocUtils.AssignScripts(DocUtils.AssignOpts(DocCast(myDashboards?.layout_headerButton), reqdBtnOpts) ?? Docs.Create.FontIconDocument(reqdBtnOpts), reqdBtnScript); @@ -477,8 +580,8 @@ export class CurrentUserUtils { const childContextMenuIcons = ["tv", "camera", "users", "times", "trash"]; // entries must be kept in synch with childContextMenuScripts, childContextMenuLabels, and childContextMenuFilters const reqdOpts:DocumentOptions = { title: "My Dashboards", childHideLinkButton: true, treeView_FreezeChildren: "remove|add", treeView_HideTitle: true, layout_boxShadow: "0 0", childDontRegisterViews: true, - dropAction: "inSame", treeView_Type: TreeViewType.fileSystem, isFolder: true, isSystem: true, treeView_TruncateTitleWidth: 350, ignoreClick: true, - layout_headerButton: newDashboardButton, childDragAction: "inSame", + dropAction: dropActionType.inPlace, treeView_Type: TreeViewType.fileSystem, isFolder: true, isSystem: true, treeView_TruncateTitleWidth: 350, ignoreClick: true, + layout_headerButton: newDashboardButton, childDragAction: dropActionType.inPlace, _layout_showTitle: "title", _height: 400, _gridGap: 5, _forceActive: true, _lockedPosition: true, contextMenuLabels:new List<string>(contextMenuLabels), contextMenuIcons:new List<string>(contextMenuIcons), @@ -504,16 +607,16 @@ export class CurrentUserUtils { var myFilesystem = DocCast(doc[field]); const newFolderOpts: DocumentOptions = { - _forceActive: true, _dragOnlyWithinContainer: true, _embedContainer: Doc.MyFilesystem, _layout_hideContextMenu: true, _width: 30, _height: 30, undoIgnoreFields:new List<string>(['treeView_SortCriterion']), + _forceActive: true, _dragOnlyWithinContainer: true, _embedContainer: Doc.MyFilesystem, _width: 30, _height: 30, undoIgnoreFields:new List<string>(['treeView_SortCriterion']), title: "New folder", color: Colors.BLACK, btnType: ButtonType.ClickButton, toolTip: "Create new folder", buttonText: "New folder", icon: "folder-plus", isSystem: true }; const newFolderScript = { onClick: CollectionTreeView.AddTreeFunc}; const newFolderButton = DocUtils.AssignScripts(DocUtils.AssignOpts(DocCast(myFilesystem?.layout_headerButton), newFolderOpts) ?? Docs.Create.FontIconDocument(newFolderOpts), newFolderScript); const reqdOpts:DocumentOptions = { _layout_showTitle: "title", _height: 100, _forceActive: true, - title: "My Documents", layout_headerButton: newFolderButton, treeView_HideTitle: true, dropAction: 'add', isSystem: true, + title: "My Documents", layout_headerButton: newFolderButton, treeView_HideTitle: true, dropAction: dropActionType.add, isSystem: true, isFolder: true, treeView_Type: TreeViewType.fileSystem, childHideLinkButton: true, layout_boxShadow: "0 0", childDontRegisterViews: true, - treeView_TruncateTitleWidth: 350, ignoreClick: true, childDragAction: "embed", + treeView_TruncateTitleWidth: 350, ignoreClick: true, childDragAction: dropActionType.embed, layout_explainer: "This is your file manager where you can create folders to keep track of documents independently of your dashboard." }; const fileFolders = new Set(DocListCast(DocCast(doc[field])?.data)); @@ -523,8 +626,8 @@ export class CurrentUserUtils { /// initializes the panel displaying docs that have been recently closed static setupRecentlyClosed(doc: Doc, field:string) { const reqdOpts:DocumentOptions = { _layout_showTitle: "title", _lockedPosition: true, _gridGap: 5, _forceActive: true, isFolder: true, - title: "My Recently Closed", childHideLinkButton: true, treeView_HideTitle: true, childDragAction: "move", isSystem: true, - treeView_TruncateTitleWidth: 350, ignoreClick: true, layout_boxShadow: "0 0", childDontRegisterViews: true, dropAction: "same", + title: "My Recently Closed", childHideLinkButton: true, treeView_HideTitle: true, childDragAction: dropActionType.move, isSystem: true, + treeView_TruncateTitleWidth: 350, ignoreClick: true, layout_boxShadow: "0 0", childDontRegisterViews: true, dropAction: dropActionType.same, contextMenuLabels: new List<string>(["Empty recently closed"]), contextMenuIcons:new List<string>(["trash"]), layout_explainer: "Recently closed documents appear in this menu. They will only be deleted if you explicity empty this list." @@ -532,7 +635,7 @@ export class CurrentUserUtils { const recentlyClosed = DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.TreeDocument([], opts), reqdOpts); const clearAll = (target:string) => `getProto(${target}).data = new List([])`; - const clearBtnsOpts:DocumentOptions = { _width: 30, _height: 30, _forceActive: true, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, + const clearBtnsOpts:DocumentOptions = { _width: 30, _height: 30, _forceActive: true, _dragOnlyWithinContainer: true, layout_hideContextMenu: true, title: "Empty", target: recentlyClosed, btnType: ButtonType.ClickButton, color: Colors.BLACK, buttonText: "Empty", icon: "trash", isSystem: true, toolTip: "Empty recently closed",}; DocUtils.AssignDocField(recentlyClosed, "layout_headerButton", (opts) => Docs.Create.FontIconDocument(opts), clearBtnsOpts, undefined, {onClick: clearAll("this.target")}); @@ -547,7 +650,7 @@ export class CurrentUserUtils { static setupUserDocView(doc: Doc, field:string) { const reqdOpts:DocumentOptions = { _lockedPosition: true, _gridGap: 5, _forceActive: true, title: Doc.CurrentUserEmail +"-view", - layout_boxShadow: "0 0", childDontRegisterViews: true, dropAction: "same", ignoreClick: true, isSystem: true, + layout_boxShadow: "0 0", childDontRegisterViews: true, dropAction: dropActionType.same, ignoreClick: true, isSystem: true, treeView_HideTitle: true, treeView_TruncateTitleWidth: 350 }; if (!doc[field]) DocUtils.AssignOpts(doc, {treeView_Open: true, treeView_ExpandedView: "fields" }); @@ -566,8 +669,7 @@ export class CurrentUserUtils { }) static createToolButton = (opts: DocumentOptions) => Docs.Create.FontIconDocument({ - btnType: ButtonType.ToolButton, _layout_hideContextMenu: true, - _dropPropertiesToRemove: new List<string>([ "_layout_hideContextMenu"]), + btnType: ButtonType.ToolButton, _dropPropertiesToRemove: new List<string>([ "layout_hideContextMenu"]), /*_nativeWidth: 40, _nativeHeight: 40, */ _width: 40, _height: 40, isSystem: true, ...opts, }) @@ -588,7 +690,7 @@ export class CurrentUserUtils { ]; const btns = btnDescs.map(desc => dockBtn({_width: 30, _height: 30, defaultDoubleClick: 'ignore', undoIgnoreFields: new List<string>(['opacity']), _dragOnlyWithinContainer: true, ...desc.opts}, desc.scripts)); const dockBtnsReqdOpts:DocumentOptions = { - title: "docked buttons", _height: 40, flexGap: 0, layout_boxShadow: "standard", childDragAction: 'move', + title: "docked buttons", _height: 40, flexGap: 0, layout_boxShadow: "standard", childDragAction: dropActionType.move, childDontRegisterViews: true, linearView_IsOpen: true, linearView_Expandable: true, ignoreClick: true }; reaction(() => UndoManager.redoStack.slice(), () => Doc.GetProto(btns.find(btn => btn.title === "Redo")!).opacity = UndoManager.CanRedo() ? 1 : 0.4, { fireImmediately: true }); @@ -612,9 +714,7 @@ export class CurrentUserUtils { return [ { title: "Snap", icon: "th", toolTip: "Show Snap Lines", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"snaplines", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform { title: "Grid", icon: "border-all", toolTip: "Show Grid", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"grid", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform - { title: "View All", icon: "object-group", toolTip: "Keep all Docs in View",btnType: ButtonType.ToggleButton, ignoreClick:true, expertMode: false, toolType:"viewAll", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform - // want the same style as toggle button, but don't want it to act as an actual toggle, so set disableToggle to true, - { title: "Fit All", icon: "arrows-left-right", toolTip: "Fit Docs to View (once)",btnType: ButtonType.ClickButton,ignoreClick:false,expertMode: false, toolType:"fitOnce", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform + { title: "Fit All", icon: "object-group", toolTip: "Fit Docs to View (double click to make sticky)",btnType: ButtonType.ToggleButton, ignoreClick:true, expertMode: false, toolType:"viewAll", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}', onDoubleClick: '{ return showFreeform(this.toolType, _readOnly_, true);}'}}, // Only when floating document is selected in freeform { title: "Clusters", icon: "braille", toolTip: "Show Doc Clusters", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"clusters", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform { title: "Cards", icon: "brain", toolTip: "Flashcards", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"flashcards", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform { title: "Arrange", icon:"arrow-down-short-wide",toolTip:"Toggle Auto Arrange", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"arrange", funcs: {hidden: 'IsNoviceMode()'}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform @@ -689,7 +789,8 @@ export class CurrentUserUtils { CollectionViewType.Grid, CollectionViewType.NoteTaking]), title: "Perspective", toolTip: "View", btnType: ButtonType.DropdownList, ignoreClick: true, width: 100, scripts: { script: 'setView(value, _readOnly_)'}}, { title: "Pin", icon: "map-pin", toolTip: "Pin View to Trail", btnType: ButtonType.ClickButton, expertMode: false, width: 30, scripts: { onClick: 'pinWithView(altKey)'}, funcs: {hidden: "IsNoneSelected()"}}, - { title: "Header", icon: "heading", toolTip: "Doc Titlebar Color", btnType: ButtonType.ColorButton, expertMode: true, ignoreClick: true, scripts: { script: 'return setHeaderColor(value, _readOnly_)'} }, + { title: "Header", icon: "heading", toolTip: "Doc Titlebar Color", btnType: ButtonType.ColorButton, expertMode: false, ignoreClick: true, scripts: { script: 'return setHeaderColor(value, _readOnly_)'} }, + { title: "Template",icon: "scroll", toolTip: "Default Note Template",btnType: ButtonType.ToggleButton, expertMode: false, toolType:DocumentType.RTF, scripts: { onClick: '{ return setDefaultTemplate(_readOnly_); }'} }, { title: "Fill", icon: "fill-drip", toolTip: "Fill/Background Color",btnType: ButtonType.ColorButton, expertMode: false, ignoreClick: true, width: 30, scripts: { script: 'return setBackgroundColor(value, _readOnly_)'}, funcs: {hidden: "IsNoneSelected()"}}, // Only when a document is selected { title: "Overlay", icon: "layer-group", toolTip: "Overlay", btnType: ButtonType.ToggleButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(this.toolType, this.expertMode, true)'}, scripts: { onClick: '{ return toggleOverlay(_readOnly_); }'}}, // Only when floating document is selected in freeform { title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(this.toolType, this.expertMode)'}, width: 30, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}}, @@ -714,7 +815,7 @@ export class CurrentUserUtils { _nativeWidth: params.width ?? 30, _width: params.width ?? 30, _height: 30, _nativeHeight: 30, linearBtnWidth: params.linearBtnWidth, toolType: params.toolType, expertMode: params.expertMode, - _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, _lockedPosition: true, + _dragOnlyWithinContainer: true, _lockedPosition: true, }; const reqdFuncs:{[key:string]:any} = { ...params.funcs, @@ -744,7 +845,7 @@ export class CurrentUserUtils { /// Initializes all the default buttons for the top bar context menu static setupContextMenuButtons(doc: Doc, field="myContextMenuBtns") { - const reqdCtxtOpts:DocumentOptions = { title: "context menu buttons", undoIgnoreFields:new List<string>(['width', "linearView_IsOpen"]), flexGap: 0, childDragAction: 'embed', childDontRegisterViews: true, linearView_IsOpen: true, ignoreClick: true, linearView_Expandable: false, _height: 35 }; + const reqdCtxtOpts:DocumentOptions = { title: "context menu buttons", undoIgnoreFields:new List<string>(['width', "linearView_IsOpen"]), flexGap: 0, childDragAction: dropActionType.embed, childDontRegisterViews: true, linearView_IsOpen: true, ignoreClick: true, linearView_Expandable: false, _height: 35 }; const ctxtMenuBtnsDoc = DocUtils.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), reqdCtxtOpts, undefined); const ctxtMenuBtns = CurrentUserUtils.contextMenuTools().map(params => this.setupContextMenuBtn(params, ctxtMenuBtnsDoc) ); return DocUtils.AssignOpts(ctxtMenuBtnsDoc, reqdCtxtOpts, ctxtMenuBtns); @@ -770,7 +871,7 @@ export class CurrentUserUtils { ]; const btns = btnDescs.map(desc => dockBtn({_width: desc.opts.width??30, _height: 30, defaultDoubleClick: 'ignore', undoIgnoreFields: new List<string>(['opacity']), _dragOnlyWithinContainer: true, ...desc.opts}, desc.scripts, desc.funcs)); const dockBtnsReqdOpts:DocumentOptions = { - title: "docked buttons", _height: 40, flexGap: 0, layout_boxShadow: "standard", childDragAction: 'move', + title: "docked buttons", _height: 40, flexGap: 0, layout_boxShadow: "standard", childDragAction: dropActionType.move, childDontRegisterViews: true, linearView_IsOpen: true, linearView_Expandable: false, ignoreClick: true }; return DocUtils.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), dockBtnsReqdOpts, btns); @@ -810,7 +911,7 @@ export class CurrentUserUtils { // childContextMenuLabels: new List<string>(["Add to Dashboards",]), // childContextMenuIcons: new List<string>(["user-plus",]), "acl-Guest": SharingPermissions.Augment, "_acl-Guest": SharingPermissions.Augment, - childDragAction: "embed", isSystem: true, childContentPointerEvents: "none", childLimitHeight: 0, _yMargin: 0, _gridGap: 15, childDontRegisterViews:true, + childDragAction: dropActionType.embed, isSystem: true, childContentPointerEvents: "none", childLimitHeight: 0, _yMargin: 0, _gridGap: 15, childDontRegisterViews:true, // NOTE: treeView_HideTitle & _layout_showTitle is for a TreeView's editable title, _layout_showTitle is for DocumentViews title bar _layout_showTitle: "title", treeView_HideTitle: true, ignoreClick: true, _lockedPosition: true, layout_boxShadow: "0 0", _chromeHidden: true, dontRegisterView: true, layout_explainer: "This is where documents or dashboards that other users have shared with you will appear. To share a document or dashboard right click and select 'Share'" @@ -824,14 +925,14 @@ export class CurrentUserUtils { static setupImportSidebar(doc: Doc, field:string) { const reqdOpts:DocumentOptions = { title: "My Imports", _forceActive: true, _layout_showTitle: "title", childLayoutString: ImportElementBox.LayoutString('data'), - _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, childLimitHeight: 0, onClickScriptDisable:"never", - childDragAction: "copy", _layout_autoHeight: true, _yMargin: 50, _gridGap: 15, layout_boxShadow: "0 0", _lockedPosition: true, isSystem: true, _chromeHidden: true, + _dragOnlyWithinContainer: true, layout_hideContextMenu: true, childLimitHeight: 0, onClickScriptDisable:"never", + childDragAction: dropActionType.copy, _layout_autoHeight: true, _yMargin: 50, _gridGap: 15, layout_boxShadow: "0 0", _lockedPosition: true, isSystem: true, _chromeHidden: true, dontRegisterView: true, layout_explainer: "This is where documents that are Imported into Dash will go." }; const myImports = DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.MasonryDocument([], opts), reqdOpts, undefined, {onClick: "deselectAll()"}); const reqdBtnOpts:DocumentOptions = { _forceActive: true, toolTip: "Import from computer", - _width: 30, _height: 30, color: Colors.BLACK, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, title: "Import", btnType: ButtonType.ClickButton, + _width: 30, _height: 30, color: Colors.BLACK, _dragOnlyWithinContainer: true, title: "Import", btnType: ButtonType.ClickButton, buttonText: "Import", icon: "upload", isSystem: true }; DocUtils.AssignDocField(myImports, "layout_headerButton", (opts) => Docs.Create.FontIconDocument(opts), reqdBtnOpts, undefined, { onClick: "importDocument()" }); return myImports; @@ -887,7 +988,7 @@ export class CurrentUserUtils { this.setupDocTemplates(doc); // sets up the template menu of templates //this.setupFieldInfos(doc); // sets up the collection of field info descriptions for each possible DocumentOption DocUtils.AssignDocField(doc, "globalScriptDatabase", (opts) => Docs.Prototypes.MainScriptDocument(), {}); - DocUtils.AssignDocField(doc, "myHeaderBar", (opts) => Docs.Create.MulticolumnDocument([], opts), { title: "My Header Bar", isSystem: true, _chromeHidden:true, childLayoutFitWidth:false, childDocumentsActive:false, dropAction: 'move'}); // drop down panel at top of dashboard for stashing documents + DocUtils.AssignDocField(doc, "myHeaderBar", (opts) => Docs.Create.MulticolumnDocument([], opts), { title: "My Header Bar", isSystem: true, _chromeHidden:true, layout_maxShown: 10, childLayoutFitWidth:false, childDocumentsActive:false, dropAction: dropActionType.move}); // drop down panel at top of dashboard for stashing documents Doc.AddDocToList(Doc.MyFilesystem, undefined, Doc.MyDashboards) Doc.AddDocToList(Doc.MyFilesystem, undefined, Doc.MySharedDocs) @@ -910,9 +1011,9 @@ export class CurrentUserUtils { const options = pair[1] as FInfo; const opts:DocumentOptions = { isSystem: true, title: pair[0], ...OmitKeys(options, ["values"]).omit, fieldIsLayout: pair[0].startsWith("_")}; switch (options.fieldType) { - case "boolean": opts.fieldValues = new List<boolean>(options.values as any); break; - case "number": opts.fieldValues = new List<number>(options.values as any); break; - case Doc.name: opts.fieldValues = new List<Doc>(options.values as any); break; + case FInfoFieldType.boolean: opts.fieldValues = new List<boolean>(options.values as any); break; + case FInfoFieldType.number: opts.fieldValues = new List<number>(options.values as any); break; + case FInfoFieldType.Doc: opts.fieldValues = new List<Doc>(options.values as any); break; default: opts.fieldValues = new List<string>(options.values as any); break;// string, pointerEvents, dimUnit, dropActionType } DocUtils.AssignDocField(infos, pair[0], opts => Doc.assign(new Doc(), OmitKeys(opts,["values"]).omit), opts); diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 7407fa2b3..a38a330da 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -248,6 +248,11 @@ export class DocumentManager { Doc.RemoveDocFromList(Doc.MyRecentlyClosed, undefined, targetDoc); const docContextPath = DocumentManager.GetContextPath(targetDoc, true); if (docContextPath.some(doc => doc.hidden)) options.toggleTarget = false; + const tabView = Array.from(TabDocView._allTabs).find(view => view._document === docContextPath[0]); + if (!tabView?._activated && tabView?._document) { + options.toggleTarget = false; + TabDocView.Activate(tabView?._document); + } let rootContextView = docContextPath.length && (await new Promise<DocumentView>(res => { diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 1f093a33c..aa0f77c72 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -1,10 +1,24 @@ +/** + * The DragManager handles all dragging interactions that occur entirely within Dash (as opposed to external drag operations from the file system, etc) + * + * Events are generated for + * a pause in the drag movement (dashDragMovePause) as a Doc(s) is dragged, + * just before (dashPreDrop) a Doc(s) is dropped, + * and just after (dashDropEvent) a Doc(s) is dropped + * If the document is dragged and paused over the golden layout header tabs, the + * drag interaction will switch to a golden layout tab drag. + * + * All drag operations can be aborted by hitting the Esc key + * + */ + import { action, observable, runInAction } from 'mobx'; import { DateField } from '../../fields/DateField'; import { Doc, Field, Opt, StrListCast } from '../../fields/Doc'; import { List } from '../../fields/List'; import { PrefetchProxy } from '../../fields/Proxy'; import { ScriptField } from '../../fields/ScriptField'; -import { ScriptCast, StrCast } from '../../fields/Types'; +import { ScriptCast } from '../../fields/Types'; import { emptyFunction, Utils } from '../../Utils'; import { Docs, DocUtils } from '../documents/Documents'; import { CollectionFreeFormDocumentView } from '../views/nodes/CollectionFreeFormDocumentView'; @@ -16,7 +30,15 @@ import { UndoManager } from './UndoManager'; import { DocData } from '../../fields/DocSymbols'; const { default : { contextMenuZindex } } = require('../views/global/globalCssVariables.module.scss'); // prettier-ignore -export type dropActionType = 'embed' | 'copy' | 'move' | 'add' | 'same' | 'inSame' | 'proto' | 'none' | undefined; // undefined = move, "same" = move but don't call dropPropertiesToRemove +export enum dropActionType { + embed = 'embed', // create a new embedding of the dragged document for the new location + copy = 'copy', // copy the dragged document + move = 'move', // move the dragged document to the drop location after removing it from where it was + add = 'add', // add the dragged document to the drop location without removing it from where it was + same = 'same', // only allow drop within same collection (or same hierarchical tree collection) + inPlace = 'inSame', // keep document in place (unless overridden by a drag modifier) + proto = 'proto', +} // undefined = move, same = move but doesn't call dropPropertiesToRemove /** * Initialize drag @@ -79,8 +101,6 @@ export namespace DragManager { export interface DragOptions { dragComplete?: (e: DragCompleteEvent) => void; // function to invoke when drag has completed hideSource?: boolean; // hide source document during drag - offsetX?: number; // offset of top left of source drag visual from cursor - offsetY?: number; noAutoscroll?: boolean; } @@ -129,9 +149,9 @@ export namespace DragManager { treeViewDoc?: Doc; offset: number[]; canEmbed?: boolean; - userDropAction: dropActionType; // the user requested drop action -- this will be honored as specified by modifier keys + userDropAction?: dropActionType; // the user requested drop action -- this will be honored as specified by modifier keys defaultDropAction?: dropActionType; // an optionally specified default drop action when there is no user drop actionl - this will be honored if there is no user drop action - dropAction: dropActionType; // a drop action request by the initiating code. the actual drop action may be different -- eg, if the request is 'embed', but the document is dropped within the same collection, the drop action will be switched to 'move' + dropAction?: dropActionType; // a drop action request by the initiating code. the actual drop action may be different -- eg, if the request is 'embed', but the document is dropped within the same collection, the drop action will be switched to 'move' dropPropertiesToRemove?: string[]; moveDocument?: MoveFunction; removeDocument?: RemoveFunction; @@ -170,8 +190,8 @@ export namespace DragManager { dropDocCreator: (annotationOn: Doc | undefined) => Doc; dropDocument?: Doc; offset: number[]; - dropAction: dropActionType; - userDropAction: dropActionType; + dropAction?: dropActionType; + userDropAction?: dropActionType; } let defaultPreDropFunc = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => { @@ -189,7 +209,7 @@ export namespace DragManager { const handler = (e: Event) => dropFunc(e, (e as CustomEvent<DropEvent>).detail); const preDropHandler = (e: Event) => { const de = (e as CustomEvent<DropEvent>).detail; - (preDropFunc ?? defaultPreDropFunc)(e, de, StrCast(doc.dropAction) as dropActionType); + (preDropFunc ?? defaultPreDropFunc)(e, de, doc.dropAction as any as dropActionType); }; element.addEventListener('dashOnDrop', handler); element.addEventListener('dashPreDrop', preDropHandler); @@ -218,19 +238,19 @@ export namespace DragManager { dragData.draggedDocuments.map(async d => !dragData.isDocDecorationMove && !dragData.userDropAction && ScriptCast(d.onDragStart) ? addAudioTag(ScriptCast(d.onDragStart).script.run({ this: d }).result) - : docDragData.dropAction === 'embed' + : docDragData.dropAction === dropActionType.embed ? Doc.BestEmbedding(d) - : docDragData.dropAction === 'add' + : docDragData.dropAction === dropActionType.add ? d - : docDragData.dropAction === 'proto' + : docDragData.dropAction === dropActionType.proto ? d[DocData] - : docDragData.dropAction === 'copy' + : docDragData.dropAction === dropActionType.copy ? (await Doc.MakeClone(d)).clone : d ) ) ).filter(d => d); - !['same', 'proto'].includes(docDragData.dropAction as any) && + ![dropActionType.same, dropActionType.proto].includes(docDragData.dropAction as any) && docDragData.droppedDocuments // .filter(drop => !drop.dragOnlyWithinContainer || ['embed', 'copy'].includes(docDragData.dropAction as any)) .forEach((drop: Doc, i: number) => { @@ -504,7 +524,7 @@ export namespace DragManager { const moveHandler = (e: PointerEvent) => { e.preventDefault(); // required or dragging text menu link item ends up dragging the link button as native drag/drop if (dragData instanceof DocumentDragData) { - dragData.userDropAction = e.ctrlKey && e.altKey ? 'copy' : e.shiftKey ? 'move' : e.ctrlKey ? 'embed' : dragData.defaultDropAction; + dragData.userDropAction = e.ctrlKey && e.altKey ? dropActionType.copy : e.shiftKey ? dropActionType.move : e.ctrlKey ? dropActionType.embed : dragData.defaultDropAction; } if (['lm_tab', 'lm_title_wrap', 'lm_tabs', 'lm_header'].includes(typeof (e.target as any).className === 'string' ? (e.target as any)?.className : '') && dragData.draggedDocuments.length === 1) { if (!startWindowDragTimer) { @@ -530,7 +550,7 @@ export namespace DragManager { if (target && !Doc.UserDoc()._noAutoscroll && !options?.noAutoscroll && !dragData.draggedDocuments?.some((d: any) => d._freeform_noAutoPan)) { const autoScrollHandler = () => { target.dispatchEvent( - new CustomEvent<React.DragEvent>('dashDragAutoScroll', { + new CustomEvent<React.DragEvent>('dashDragMovePause', { bubbles: true, detail: { shiftKey: e.shiftKey, @@ -552,7 +572,7 @@ export namespace DragManager { screenY: e.screenY, detail: e.detail, view: e.view ? e.view : (new Window() as any), - nativeEvent: new DragEvent('dashDragAutoScroll'), + nativeEvent: new DragEvent('dashDragMovePause'), currentTarget: target, target: target, bubbles: true, @@ -566,7 +586,7 @@ export namespace DragManager { isPropagationStopped: () => ('not implemented for this event' ? false : false), persist: emptyFunction, timeStamp: e.timeStamp, - type: 'dashDragAutoScroll', + type: 'dashDragMovePause', }, }) ); diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts index 54066d267..3df3e36c6 100644 --- a/src/client/util/DropConverter.ts +++ b/src/client/util/DropConverter.ts @@ -12,43 +12,55 @@ import { ButtonType, FontIconBox } from '../views/nodes/FontIconBox/FontIconBox' import { DragManager } from './DragManager'; import { ScriptingGlobals } from './ScriptingGlobals'; -export function MakeTemplate(doc: Doc, first: boolean = true, rename: Opt<string> = undefined, templateField: string = '') { - if (templateField) doc[DocData].title = templateField; /// the title determines which field is being templated - doc.isTemplateDoc = makeTemplate(doc, first, rename); +/** + * Converts a Doc to a render template that can be applied to other Docs to customize how they render while + * still using the other Doc as the backing data store (ie, dataDoc). During rendering, if a layout Doc is provided + * with 'isTemplateDoc' set, then the layout Doc is treated as a template for the rendered Doc. The template Doc is + * "expanded" to create an template instance for the rendered Doc. + * + * + * @param doc the doc to convert to a template + * @returns 'doc' + */ +export function MakeTemplate(doc: Doc) { + doc.isTemplateDoc = makeTemplate(doc, true); 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 -// after it has been converted to -function makeTemplate(doc: Doc, first: boolean = true, rename: Opt<string> = undefined): boolean { + +/** + * Recursively converts 'doc' into a template that can be used to render other documents. + * + * For recurive Docs in the template, their target fieldKey is defined by their title, + * not by whatever fieldKey they used in their layout. + * @param doc + * @param first whether this is the topmost root of the recursive template + * @returns whether a template was successfully created + */ +function makeTemplate(doc: Doc, first: boolean = true): boolean { const layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc; if (layoutDoc.layout instanceof Doc) { - // its already a template - return true; + return true; // its already a template } const layout = StrCast(layoutDoc.layout).match(/fieldKey={'[^']*'}/)![0]; const fieldKey = layout.replace("fieldKey={'", '').replace(/'}$/, ''); const docs = DocListCast(layoutDoc[fieldKey]); - let any = false; + let isTemplate = false; docs.forEach(d => { if (!StrCast(d.title).startsWith('-')) { - any = Doc.MakeMetadataFieldTemplate(d, layoutDoc[DocData]) || any; + isTemplate = Doc.MakeMetadataFieldTemplate(d, layoutDoc[DocData]) || isTemplate; } else if (d.type === DocumentType.COL || d.data instanceof RichTextField) { - any = makeTemplate(d, false) || any; + isTemplate = makeTemplate(d, false) || isTemplate; } }); if (first && !docs.length) { // bcz: feels hacky : if the root level document has items, it's not a field template - any = Doc.MakeMetadataFieldTemplate(doc, layoutDoc[DocData], true) || any; + isTemplate = Doc.MakeMetadataFieldTemplate(doc, layoutDoc[DocData], true) || isTemplate; } else if (layoutDoc[fieldKey] instanceof RichTextField || layoutDoc[fieldKey] instanceof ImageField) { if (!StrCast(layoutDoc.title).startsWith('-')) { - any = Doc.MakeMetadataFieldTemplate(layoutDoc, layoutDoc[DocData], true); + isTemplate = Doc.MakeMetadataFieldTemplate(layoutDoc, layoutDoc[DocData], true); } } - rename && (doc.title = rename); - return any; + return isTemplate; } export function convertDropDataToButtons(data: DragManager.DocumentDragData) { data?.draggedDocuments.map((doc, i) => { @@ -73,7 +85,6 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) { _nativeHeight: 100, _width: 100, _height: 100, - _layout_hideContextMenu: true, backgroundColor: StrCast(doc.backgroundColor), title: StrCast(layoutDoc.title), btnType: ButtonType.ClickButton, diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx index 8c3c9df2e..8f4e43978 100644 --- a/src/client/views/ContextMenu.tsx +++ b/src/client/views/ContextMenu.tsx @@ -7,6 +7,7 @@ import { SettingsManager } from '../util/SettingsManager'; import './ContextMenu.scss'; import { ContextMenuItem, ContextMenuProps, OriginalMenuProps } from './ContextMenuItem'; import { ObservableReactComponent } from './ObservableReactComponent'; +import { DivHeight, DivWidth } from '../../Utils'; @observer export class ContextMenu extends ObservableReactComponent<{}> { @@ -214,8 +215,8 @@ export class ContextMenu extends ObservableReactComponent<{}> { className="contextMenu-cont" ref={action((r: any) => { if (r) { - this._width = Number(getComputedStyle(r).width.replace('px', '')); - this._height = Number(getComputedStyle(r).height.replace('px', '')); + this._width = DivWidth(r); + this._height = DivHeight(r); } })} style={{ diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx index 472d419fc..146ac5b01 100644 --- a/src/client/views/DashboardView.tsx +++ b/src/client/views/DashboardView.tsx @@ -28,6 +28,7 @@ import { Colors } from './global/globalEnums'; import { MainViewModal } from './MainViewModal'; import { ButtonType } from './nodes/FontIconBox/FontIconBox'; import { ObservableReactComponent } from './ObservableReactComponent'; +import { dropActionType } from '../util/DragManager'; enum DashboardGroup { MyDashboards, @@ -403,7 +404,7 @@ export class DashboardView extends ObservableReactComponent<{}> { _layout_fitWidth: true, _gridGap: 5, _forceActive: true, - childDragAction: 'embed', + childDragAction: dropActionType.embed, treeView_TruncateTitleWidth: 150, ignoreClick: true, contextMenuIcons: new List<string>(['plus']), @@ -411,7 +412,7 @@ export class DashboardView extends ObservableReactComponent<{}> { _lockedPosition: true, layout_boxShadow: '0 0', childDontRegisterViews: true, - dropAction: 'same', + dropAction: dropActionType.same, isSystem: true, layout_explainer: 'All of the calendars that you have created will appear here.', }; @@ -427,7 +428,6 @@ export class DashboardView extends ObservableReactComponent<{}> { _width: 30, _height: 30, _dragOnlyWithinContainer: true, - _layout_hideContextMenu: true, title: 'New trail', toolTip: 'Create new trail', color: Colors.BLACK, @@ -450,7 +450,7 @@ export class DashboardView extends ObservableReactComponent<{}> { _layout_fitWidth: true, _gridGap: 5, _forceActive: true, - childDragAction: 'embed', + childDragAction: dropActionType.embed, treeView_TruncateTitleWidth: 150, ignoreClick: true, layout_headerButton: myTrailsBtn, @@ -459,7 +459,7 @@ export class DashboardView extends ObservableReactComponent<{}> { _lockedPosition: true, layout_boxShadow: '0 0', childDontRegisterViews: true, - dropAction: 'same', + dropAction: dropActionType.same, isSystem: true, layout_explainer: 'All of the trails that you have created will appear here.', }; diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index 3d5a5b945..a25a8f77a 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -182,19 +182,19 @@ export function ViewBoxAnnotatableComponent<P extends FieldViewProps>() { const docs = indocs.filter(doc => [AclEdit, AclAdmin].includes(effectiveAcl) || GetEffectiveAcl(doc) === AclAdmin); // docs.forEach(doc => doc.annotationOn === this.Document && Doc.SetInPlace(doc, 'annotationOn', undefined, true)); - const targetDataDoc = this.dataDoc; + const targetDataDoc = this.Document[DocData]; // this.dataDoc; // we want to write to the template, not the actual data doc const value = DocListCast(targetDataDoc[annotationKey ?? this.annotationKey]); const toRemove = value.filter(v => docs.includes(v)); if (toRemove.length !== 0) { - const recent = this.Document !== Doc.MyRecentlyClosed ? Doc.MyRecentlyClosed : undefined; + const recentlyClosed = this.Document !== Doc.MyRecentlyClosed ? Doc.MyRecentlyClosed : undefined; toRemove.forEach(doc => { leavePushpin && DocUtils.LeavePushpin(doc, annotationKey ?? this.annotationKey); Doc.RemoveDocFromList(targetDataDoc, annotationKey ?? this.annotationKey, doc, true); - Doc.RemoveEmbedding(doc, doc); doc.embedContainer = undefined; - if (recent && !dontAddToRemoved) { - doc.type !== DocumentType.LOADING && Doc.AddDocToList(recent, 'data', doc, undefined, true, true); + if (recentlyClosed && !dontAddToRemoved && doc.type !== DocumentType.LOADING) { + Doc.AddDocToList(recentlyClosed, 'data', doc, undefined, true, true); + Doc.RemoveEmbedding(doc, doc); } }); if (targetDataDoc.isGroup && DocListCast(targetDataDoc[annotationKey ?? this.annotationKey]).length < 2) { @@ -228,7 +228,7 @@ export function ViewBoxAnnotatableComponent<P extends FieldViewProps>() { if (this._props.filterAddDocument?.(docs) === false || docs.find(doc => Doc.AreProtosEqual(doc, this.Document) && Doc.LayoutField(doc) === Doc.LayoutField(this.Document))) { return false; } - const targetDataDoc = this.dataDoc; + const targetDataDoc = this.Document[DocData]; // this.dataDoc; // we want to write to the template, not the actual data doc const effectiveAcl = GetEffectiveAcl(targetDataDoc); if (effectiveAcl === AclPrivate || effectiveAcl === AclReadonly) { @@ -245,9 +245,9 @@ export function ViewBoxAnnotatableComponent<P extends FieldViewProps>() { inheritParentAcls(targetDataDoc, doc, true); }); - const annoDocs = Doc.Get(targetDataDoc, annotationKey ?? this.annotationKey, true) as List<Doc>; // get the dataDoc directly ... when using templates there may be some default items already there, but we can't change them. maybe we should copy them over, though... + const annoDocs = Doc.Get(targetDataDoc, annotationKey ?? this.annotationKey, true) as List<Doc>; // get the dataDoc directly ... when using templates there may be some default items already there, but we can't change them, so we copy them below (should really be some kind of inheritance since the template contents could change) if (annoDocs instanceof List) annoDocs.push(...added.filter(add => !annoDocs.includes(add))); - else targetDataDoc[annotationKey ?? this.annotationKey] = new List<Doc>(added); + else targetDataDoc[annotationKey ?? this.annotationKey] = new List<Doc>([...added, ...(annoDocs === undefined ? DocListCast(targetDataDoc[annotationKey ?? this.annotationKey]) : [])]); targetDataDoc[(annotationKey ?? this.annotationKey) + '_modificationDate'] = new DateField(); } } diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index 8a4b42ae0..d65e0b406 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -4,12 +4,12 @@ import { Tooltip } from '@mui/material'; import { action, computed, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { emptyFunction, returnFalse, setupMoveUpEvents, simulateMouseClick } from '../../Utils'; +import { emptyFunction, returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick } from '../../Utils'; import { Doc } from '../../fields/Doc'; import { Cast, DocCast } from '../../fields/Types'; import { DocUtils } from '../documents/Documents'; import { CalendarManager } from '../util/CalendarManager'; -import { DragManager } from '../util/DragManager'; +import { DragManager, dropActionType } from '../util/DragManager'; import { IsFollowLinkScript } from '../util/LinkFollower'; import { SelectionManager } from '../util/SelectionManager'; import { SharingManager } from '../util/SharingManager'; @@ -24,6 +24,9 @@ import { DocumentView, DocumentViewInternal, OpenWhere } from './nodes/DocumentV import { DashFieldView } from './nodes/formattedText/DashFieldView'; import { PinProps } from './nodes/trails'; import { faCalendarDays } from '@fortawesome/free-solid-svg-icons'; +import { Popup } from 'browndash-components'; +import { TemplateMenu } from './TemplateMenu'; +import { FaEdit } from 'react-icons/fa'; @observer export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (DocumentView | undefined)[]; stack?: any }> { @@ -306,7 +309,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( const dragDocView = this.view0!; const dragData = new DragManager.DocumentDragData([dragDocView.Document]); const [left, top] = dragDocView.screenToContentsTransform().inverse().transformPoint(0, 0); - dragData.defaultDropAction = 'embed'; + dragData.defaultDropAction = dropActionType.embed; dragData.canEmbed = true; DragManager.StartDocumentDrag([dragDocView.ContentDiv!], dragData, left, top, { hideSource: false }); return true; @@ -316,35 +319,24 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( _ref = React.createRef<HTMLDivElement>(); @observable _tooltipOpen: boolean = false; + @computed get templateMenu() { + return ( + <div ref={this._ref}> + <TemplateMenu + docViews={this._props + .views() + .filter(v => v) + .map(v => v as DocumentView)} + /> + </div> + ); + } @computed get templateButton() { - const view0 = this.view0; - const views = this._props.views(); - return !view0 ? null : ( + return !this.view0 ? null : ( <Tooltip title={<div className="dash-tooltip">Tap to Customize Layout. Drag an embedding</div>} open={this._tooltipOpen} onClose={action(() => (this._tooltipOpen = false))} placement="bottom"> <div className="documentButtonBar-linkFlyout" ref={this._dragRef} onPointerEnter={action(() => !this._ref.current?.getBoundingClientRect().width && (this._tooltipOpen = true))}> - { - /* <Flyout - anchorPoint={anchorPoints.LEFT_TOP} - onOpen={action(() => (this._embedDown = true))} - onClose={action(() => (this._embedDown = false))} - content={ - !this._embedDown ? null : ( - <div ref={this._ref}> - {' '} - <TemplateMenu docViews={views.filter(v => v).map(v => v as DocumentView)} /> - </div> - ) - }> - <div className={'documentButtonBar-linkButton-empty'} ref={this._dragRef} onPointerDown={this.onTemplateButton}> - <FontAwesomeIcon className="documentdecorations-icon" icon="edit" size="sm" /> - </div> - </Flyout> */ - - <div className={'documentButtonBar-linkButton-empty'} ref={this._dragRef} onPointerDown={this.onTemplateButton}> - <FontAwesomeIcon className="documentdecorations-icon" icon="edit" size="sm" /> - </div> - } + <Popup icon={<FaEdit />} popup={this.templateMenu} popupContainsPt={returnTrue} /> </div> </Tooltip> ); diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index e9c4d9cc5..2fb9f0fc1 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -77,7 +77,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora const {x,y} = Utils.rotPt(e.clientX - center.x, e.clientY - center.y, NumCast(SelectionManager.Views.lastElement()?.screenToViewTransform().Rotate)); - (this._showNothing = !(this.Bounds.x !== Number.MAX_VALUE && // + (this._showNothing = !DocumentButtonBar.Instance?._tooltipOpen && !(this.Bounds.x !== Number.MAX_VALUE && // (this.Bounds.x > center.x+x || this.Bounds.r < center.x+x || this.Bounds.y > center.y+y || this.Bounds.b < center.y+y ))); })); // prettier-ignore @@ -323,7 +323,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora SelectionManager.Docs.map(doc => { const docMax = Math.min(NumCast(doc.width) / 2, NumCast(doc.height) / 2); const radius = Math.min(1, dist / maxDist) * docMax; // set radius based on ratio of drag distance to half diagonal distance of bounding box - doc.layout_borderRounding = `${radius}px`; + doc._layout_borderRounding = `${radius}px`; }); return false; }, @@ -514,7 +514,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora // // determines if anything being dragged directly or via a group has a fixed aspect ratio (in which case we resize uniformly) // - hasFixedAspect = (doc: Doc): boolean => (doc.isGroup ? DocListCast(doc.data).some(this.hasFixedAspect) : !BoolCast(doc.layout_nativeDimEditable)); + hasFixedAspect = (doc: Doc): boolean => (doc.isGroup ? DocListCast(doc.data).some(this.hasFixedAspect) : !BoolCast(doc._layout_nativeDimEditable)); // // resize a single DocumentView about the specified reference point, possibly setting/updating the native dimensions of the Doc @@ -533,9 +533,9 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora const [initWidth, initHeight] = [NumCast(doc._width, 1), NumCast(doc._height)]; const modifyNativeDim = - (opts.ctrlKey && doc.layout_nativeDimEditable) || // e.g., PDF or web page - (doc.layout_reflowHorizontal && opts.dragHdl !== 'bottom' && opts.dragHdl !== 'top') || // eg rtf or some web pages - (doc.layout_reflowVertical && (opts.dragHdl === 'bottom' || opts.dragHdl === 'top' || opts.ctrlKey)); // eg rtf, web, pdf + (opts.ctrlKey && doc._layout_nativeDimEditable) || // e.g., PDF or web page + (doc._layout_reflowHorizontal && opts.dragHdl !== 'bottom' && opts.dragHdl !== 'top') || // eg rtf or some web pages + (doc._layout_reflowVertical && (opts.dragHdl === 'bottom' || opts.dragHdl === 'top' || opts.ctrlKey)); // eg rtf, web, pdf if (nwidth && nheight && !modifyNativeDim) { // eg., dragging right resizer on PDF -- enforce native dimensions because not expliclty overridden with ctrl or bottom resize drag scale.x === 1 ? (scale.x = scale.y) : (scale.y = scale.x); @@ -545,7 +545,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora const setData = Doc.NativeWidth(doc[DocData]) === doc.nativeWidth; doc.nativeWidth = scale.x * Doc.NativeWidth(doc); if (setData) Doc.SetNativeWidth(doc[DocData], NumCast(doc.nativeWidth)); - if (doc.layout_reflowVertical && !NumCast(doc.nativeHeight)) { + if (doc._layout_reflowVertical && !NumCast(doc.nativeHeight)) { doc._nativeHeight = (initHeight / initWidth) * nwidth; // initializes the nativeHeight for a PDF } } @@ -562,7 +562,13 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora doc.y = NumCast(doc.y) + deltaY; doc._layout_modificationDate = new DateField(); - scale.y !== 1 && (doc._layout_autoHeight = undefined); + if (scale.y !== 1) { + docView.layoutDoc._layout_autoHeight = undefined; + if (docView.layoutDoc._layout_autoHeight) { + // if autoHeight is still on because of a prototype + docView.layoutDoc._layout_autoHeight = false; // then don't inherit, but explicitly set it to false + } + } } }; @@ -597,7 +603,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora this._resizeUndo?.end(); // detect layout_autoHeight gesture and apply - SelectionManager.Docs.forEach(doc => NumCast(doc._height) < 20 && (doc._layout_autoHeight = true)); + SelectionManager.Views.forEach(view => NumCast(view.Document._height) < 20 && (view.layoutDoc._layout_autoHeight = true)); //need to change points for resize, or else rotation/control points will fail. this._inkDragDocs .map(oldbds => ({ oldbds, inkPts: Cast(oldbds.doc.data, InkField)?.inkData || [] })) diff --git a/src/client/views/FieldsDropdown.tsx b/src/client/views/FieldsDropdown.tsx new file mode 100644 index 000000000..5638d34c6 --- /dev/null +++ b/src/client/views/FieldsDropdown.tsx @@ -0,0 +1,120 @@ +/** + * This creates a dropdown menu that's populated with possible field key names (e.g., author, tags) + * + * The set of field names actually displayed is based on searching the prop 'Document' and its descendants : + * The field list will contain all of the fields within the prop Document and all of its children; + * this list is then pruned down to only include fields that are not marked in Documents.ts to be non-filterable + */ + +import { computed, makeObservable, observable, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import Select from 'react-select'; +import { Doc } from '../../fields/Doc'; +import { DocOptions, FInfo } from '../documents/Documents'; +import { SearchUtil } from '../util/SearchUtil'; +import { SettingsManager } from '../util/SettingsManager'; +import './FilterPanel.scss'; +import { ObservableReactComponent } from './ObservableReactComponent'; + +interface fieldsDropdownProps { + Document: Doc; // show fields for this Doc if set, otherwise for all docs in dashboard + selectFunc: (value: string) => void; + menuClose?: () => void; + placeholder?: string | (() => string); + showPlaceholder?: true; // if true, then input field always shows the placeholder value; otherwise, it shows the current selection + addedFields?: string[]; +} + +@observer +export class FieldsDropdown extends ObservableReactComponent<fieldsDropdownProps> { + @observable _newField = ''; + constructor(props: any) { + super(props); + makeObservable(this); + } + + @computed get allDescendantDocs() { + const allDocs = new Set<Doc>(); + SearchUtil.foreachRecursiveDoc([this._props.Document], (depth, doc) => allDocs.add(doc)); + return Array.from(allDocs); + } + + @computed get fieldsOfDocuments() { + const keys = new Set<string>(); + this.allDescendantDocs.forEach(doc => SearchUtil.documentKeys(doc).filter(key => keys.add(key))); + const sortedKeys = Array.from(keys.keys()) + .filter(key => key[0]) + .filter(key => key.indexOf('modificationDate') !== -1 || (key[0] === key[0].toUpperCase() && !key.startsWith('_')) || !Doc.noviceMode) + .sort(); + + Array.from(keys).forEach(key => sortedKeys.splice(sortedKeys.indexOf(key), 1)); + + return [...Array.from(keys), ...sortedKeys]; + } + + render() { + const filteredOptions = ['author', ...(this._newField ? [this._newField] : []), ...(this._props.addedFields ?? []), ...this.fieldsOfDocuments.filter(facet => facet[0] === facet.charAt(0).toUpperCase())]; + + Object.entries(DocOptions) + .filter(opts => opts[1].filterable) + .forEach((pair: [string, FInfo]) => filteredOptions.push(pair[0])); + const options = filteredOptions.sort().map(facet => ({ value: facet, label: facet })); + + console.log(options); + return ( + <Select + styles={{ + control: (baseStyles, state) => ({ + ...baseStyles, + minHeight: '5px', + maxHeight: '30px', + color: SettingsManager.userColor, + backgroundColor: SettingsManager.userBackgroundColor, + padding: 0, + margin: 0, + }), + singleValue: (baseStyles, state) => ({ + ...baseStyles, + color: SettingsManager.userColor, + background: SettingsManager.userBackgroundColor, + }), + placeholder: (baseStyles, state) => ({ + ...baseStyles, + color: SettingsManager.userColor, + background: SettingsManager.userBackgroundColor, + }), + input: (baseStyles, state) => ({ + ...baseStyles, + padding: 0, + margin: 0, + color: SettingsManager.userColor, + background: 'transparent', + }), + option: (baseStyles, state) => ({ + ...baseStyles, + color: SettingsManager.userColor, + background: !state.isFocused ? SettingsManager.userBackgroundColor : SettingsManager.userVariantColor, + }), + menuList: (baseStyles, state) => ({ + ...baseStyles, + backgroundColor: SettingsManager.userBackgroundColor, + }), + }} + placeholder={typeof this._props.placeholder === 'string' ? this._props.placeholder : this._props.placeholder?.()} + options={options as any} + isMulti={false} + onChange={val => this._props.selectFunc((val as any as { value: string; label: string }).value)} + onKeyDown={e => { + if (e.key === 'Enter') { + runInAction(() => this._props.selectFunc((this._newField = (e.nativeEvent.target as any)?.value))); + } + e.stopPropagation(); + }} + onMenuClose={this._props.menuClose} + closeMenuOnSelect={true} + value={this._props.showPlaceholder ? null : undefined} + /> + ); + } +} diff --git a/src/client/views/FilterPanel.tsx b/src/client/views/FilterPanel.tsx index 818c81c9a..0521c4a4b 100644 --- a/src/client/views/FilterPanel.tsx +++ b/src/client/views/FilterPanel.tsx @@ -4,19 +4,16 @@ import * as React from 'react'; import { Handles, Rail, Slider, Ticks, Tracks } from 'react-compound-slider'; import { AiOutlineMinusSquare, AiOutlinePlusSquare } from 'react-icons/ai'; import { CiCircleRemove } from 'react-icons/ci'; -import Select from 'react-select'; import { Doc, DocListCast, Field, LinkedTo, StrListCast } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; import { List } from '../../fields/List'; import { RichTextField } from '../../fields/RichTextField'; -import { DocOptions, FInfo } from '../documents/Documents'; import { DocumentManager } from '../util/DocumentManager'; -import { UserOptions } from '../util/GroupManager'; import { SearchUtil } from '../util/SearchUtil'; import { SettingsManager } from '../util/SettingsManager'; import { undoable } from '../util/UndoManager'; +import { FieldsDropdown } from './FieldsDropdown'; import './FilterPanel.scss'; -import { FieldView } from './nodes/FieldView'; import { Handle, Tick, TooltipRail, Track } from './nodes/SliderBox-components'; import { ObservableReactComponent } from './ObservableReactComponent'; @@ -26,9 +23,7 @@ interface filterProps { @observer export class FilterPanel extends ObservableReactComponent<filterProps> { - public static LayoutString(fieldKey: string) { - return FieldView.LayoutString(FilterPanel, fieldKey); - } + @observable _selectedFacetHeaders = new Set<string>(); constructor(props: any) { super(props); @@ -38,45 +33,19 @@ export class FilterPanel extends ObservableReactComponent<filterProps> { /** * @returns the relevant doc according to the value of FilterBox._filterScope i.e. either the Current Dashboard or the Current Collection */ - get targetDoc() { + get Document() { return this._props.Document; } @computed get targetDocChildKey() { - const targetView = DocumentManager.Instance.getFirstDocumentView(this.targetDoc); + const targetView = DocumentManager.Instance.getFirstDocumentView(this.Document); return targetView?.ComponentView?.annotationKey ?? targetView?.ComponentView?.fieldKey ?? 'data'; } @computed get targetDocChildren() { - return [...DocListCast(this.targetDoc?.[this.targetDocChildKey] || Doc.ActiveDashboard?.data), ...DocListCast(this.targetDoc[Doc.LayoutFieldKey(this.targetDoc) + '_sidebar'])]; - } - - @computed get allDocs() { - const allDocs = new Set<Doc>(); - const targetDoc = this.targetDoc; - if (targetDoc) { - SearchUtil.foreachRecursiveDoc([this.targetDoc], (depth, doc) => allDocs.add(doc)); - } - return Array.from(allDocs); - } - - @computed get _allFacets() { - const noviceReqFields = ['author', 'tags', 'text', 'type', LinkedTo]; - const noviceLayoutFields: string[] = []; //["_layout_curPage"]; - const noviceFields = [...noviceReqFields, ...noviceLayoutFields]; - - const keys = new Set<string>(noviceFields); - this.allDocs.forEach(doc => SearchUtil.documentKeys(doc).filter(key => keys.add(key))); - const sortedKeys = Array.from(keys.keys()) - .filter(key => key[0]) - .filter(key => key.indexOf('modificationDate') !== -1 || (key[0] === key[0].toUpperCase() && !key.startsWith('_')) || noviceFields.includes(key) || !Doc.noviceMode) - .sort(); - - noviceFields.forEach(key => sortedKeys.splice(sortedKeys.indexOf(key), 1)); - - return [...noviceFields, ...sortedKeys]; + return [...DocListCast(this.Document?.[this.targetDocChildKey] || Doc.ActiveDashboard?.data), ...DocListCast(this.Document[Doc.LayoutFieldKey(this.Document) + '_sidebar'])]; } @computed get rangeFilters() { - return StrListCast(this.targetDoc?._childFiltersByRanges).filter((filter, i) => !(i % 3)); + return StrListCast(this.Document?._childFiltersByRanges).filter((filter, i) => !(i % 3)); } /** @@ -84,7 +53,7 @@ export class FilterPanel extends ObservableReactComponent<filterProps> { * ["#tags::bob::check", "tags::joe::check", "width", "height"] */ @computed get activeFilters() { - return StrListCast(this.targetDoc?._childFilters).concat(this.rangeFilters); + return StrListCast(this.Document?._childFilters).concat(this.rangeFilters); } @computed get mapActiveFiltersToFacets() { @@ -100,32 +69,27 @@ export class FilterPanel extends ObservableReactComponent<filterProps> { // this wants to return all the filter facets that have an existing filter set on them in order to show them in the rendered panel // this set may overlap the selectedFilters // if the components reloads, these will still exist and be shown - + // // ["#tags", "width", "height"] // - @computed get activeFacetHeaders() { const activeHeaders = new Array(); this.activeFilters.map(filter => activeHeaders.push(filter.split(Doc.FilterSep)[0])); return activeHeaders; } - /** - * @returns a string array of the current attributes - */ - // @computed get currentFacets() { - // return this.activeFilters.map(filter => filter.split(Doc.FilterSep)[0]); - // } static gatherFieldValues(childDocs: Doc[], facetKey: string, childFilters: string[]) { const valueSet = new Set<string>(childFilters.map(filter => filter.split(Doc.FilterSep)[1])); let rtFields = 0; let subDocs = childDocs; + let gatheredDocs = [] as Doc[]; if (subDocs.length > 0) { let newarray: Doc[] = []; while (subDocs.length > 0) { newarray = []; subDocs.forEach(t => { + gatheredDocs.push(t); const facetVal = t[facetKey]; if (facetVal instanceof RichTextField || typeof facetVal === 'string') rtFields++; facetVal !== undefined && valueSet.add(Field.toString(facetVal as Field)); @@ -135,7 +99,7 @@ export class FilterPanel extends ObservableReactComponent<filterProps> { DocListCast(t[annos ? fieldKey + '_annotations' : fieldKey]).forEach(newdoc => newarray.push(newdoc)); annos && DocListCast(t[fieldKey + '_sidebar']).forEach(newdoc => newarray.push(newdoc)); }); - subDocs = newarray; + subDocs = newarray.filter(d => !gatheredDocs.includes(d)); } } // } @@ -145,8 +109,8 @@ export class FilterPanel extends ObservableReactComponent<filterProps> { } public removeFilter = (filterName: string) => { - Doc.setDocFilter(this.targetDoc, filterName, undefined, 'remove'); - Doc.setDocRangeFilter(this.targetDoc, filterName, undefined); + Doc.setDocFilter(this.Document, filterName, undefined, 'remove'); + Doc.setDocRangeFilter(this.Document, filterName, undefined); }; // @observable _chosenFacets = new ObservableMap<string, 'text' | 'checkbox' | 'slider' | 'range'>(); @@ -154,18 +118,16 @@ export class FilterPanel extends ObservableReactComponent<filterProps> { @observable _collapseReturnKeys = new Array(); // this computed function gets the active filters and maps them to their headers - // // activeRenderedFacetInfos() // returns renderInfo for all user selected filters and for all existing filters set on the document // Map("tags" => {"checkbox"}, - // "width" => {"rangs", domain:[1978,1992]}) + // "width" => {"range", domain:[1978,1992]}) // - @computed get activeRenderedFacetInfos() { return new Set( Array.from(new Set(Array.from(this._selectedFacetHeaders).concat(this.activeFacetHeaders))).map(facetHeader => { - const facetValues = FilterPanel.gatherFieldValues(this.targetDocChildren, facetHeader, StrListCast(this.targetDoc.childFilters)); + const facetValues = FilterPanel.gatherFieldValues(this.targetDocChildren, facetHeader, StrListCast(this.Document.childFilters)); let nonNumbers = 0; let minVal = Number.MAX_VALUE, @@ -185,7 +147,7 @@ export class FilterPanel extends ObservableReactComponent<filterProps> { } else if (facetHeader !== 'tags' && nonNumbers / facetValues.strings.length < 0.1) { const extendedMinVal = minVal - Math.min(1, Math.floor(Math.abs(maxVal - minVal) * 0.1)); const extendedMaxVal = Math.max(minVal + 1, maxVal + Math.min(1, Math.ceil(Math.abs(maxVal - minVal) * 0.05))); - const ranged = Doc.readDocRangeFilter(this.targetDoc, facetHeader); // not the filter range, but the zooomed in range on the filter + const ranged = Doc.readDocRangeFilter(this.Document, facetHeader); // not the filter range, but the zooomed in range on the filter return { facetHeader, renderType: 'range', domain: [extendedMinVal, extendedMaxVal], range: ranged ? ranged : [extendedMinVal, extendedMaxVal] }; } else { return { facetHeader, renderType: 'checkbox' }; @@ -194,8 +156,6 @@ export class FilterPanel extends ObservableReactComponent<filterProps> { ); } - @observable _selectedFacetHeaders = new Set<string>(); - /** * user clicks on a filter facet because they want to see it. * this adds this chosen filter to a set of user selected filters called: selectedFilters @@ -229,7 +189,7 @@ export class FilterPanel extends ObservableReactComponent<filterProps> { facetValues = (facetHeader: string) => { const allCollectionDocs = new Set<Doc>(); SearchUtil.foreachRecursiveDoc(this.targetDocChildren, (depth: number, doc: Doc) => allCollectionDocs.add(doc)); - const set = new Set<string>([...StrListCast(this.targetDoc.childFilters).map(filter => filter.split(Doc.FilterSep)[1]), Doc.FilterNone, Doc.FilterAny]); + const set = new Set<string>([...StrListCast(this.Document.childFilters).map(filter => filter.split(Doc.FilterSep)[1]), Doc.FilterNone, Doc.FilterAny]); if (facetHeader === 'tags') allCollectionDocs.forEach(child => StrListCast(child[facetHeader]) @@ -253,59 +213,13 @@ export class FilterPanel extends ObservableReactComponent<filterProps> { return nonNumbers / facetValues.length > 0.1 ? facetValues.sort() : facetValues.sort((n1: string, n2: string) => Number(n1) - Number(n2)); }; - @computed get fieldsDropdown() { - const filteredOptions = ['author', 'tags', 'text', 'acl-Guest', ...this._allFacets.filter(facet => facet[0] === facet.charAt(0).toUpperCase())]; - - Object.entries(DocOptions) - .filter(opts => opts[1].filterable) - .forEach((pair: [string, FInfo]) => filteredOptions.push(pair[0])); - const options = filteredOptions.map(facet => ({ value: facet, label: facet })); - - return ( - <Select - styles={{ - control: (baseStyles, state) => ({ - ...baseStyles, - color: SettingsManager.userColor, - background: SettingsManager.userBackgroundColor, - }), - placeholder: (baseStyles, state) => ({ - ...baseStyles, - color: SettingsManager.userColor, - background: SettingsManager.userBackgroundColor, - }), - input: (baseStyles, state) => ({ - ...baseStyles, - color: SettingsManager.userColor, - background: SettingsManager.userBackgroundColor, - }), - option: (baseStyles, state) => ({ - ...baseStyles, - color: SettingsManager.userColor, - background: !state.isFocused ? SettingsManager.userBackgroundColor : SettingsManager.userVariantColor, - }), - menuList: (baseStyles, state) => ({ - ...baseStyles, - backgroundColor: SettingsManager.userBackgroundColor, - }), - }} - placeholder={'add a filter'} - options={options} - isMulti={false} - onChange={val => this.facetClick((val as UserOptions).value)} - onKeyDown={e => e.stopPropagation()} - //onMenuClose={onClose} - value={null} - closeMenuOnSelect={true} - /> - ); - } - render() { return ( <div className="filterBox-treeView"> <div className="filterBox-select"> - <div style={{ width: '100%' }}>{this.fieldsDropdown}</div> + <div style={{ width: '100%' }}> + <FieldsDropdown Document={this.Document} selectFunc={this.facetClick} showPlaceholder={true} placeholder="add a filter" addedFields={['acl-Guest', LinkedTo]} /> + </div> {/* THE FOLLOWING CODE SHOULD BE DEVELOPER FOR BOOLEAN EXPRESSION (AND / OR) */} {/* <div className="filterBox-select-bool"> <select className="filterBox-selection" onChange={action(e => this.targetDoc && (this.targetDoc._childFilters_boolean = (e.target as any).value))} defaultValue={StrCast(this.targetDoc?.childFilters_boolean)}> @@ -341,11 +255,11 @@ export class FilterPanel extends ObservableReactComponent<filterProps> { className="filterBox-facetHeader-remove" onClick={action(e => { if (renderInfo.facetHeader === 'text') { - Doc.setDocFilter(this.targetDoc, renderInfo.facetHeader, 'match', 'remove'); + Doc.setDocFilter(this.Document, renderInfo.facetHeader, 'match', 'remove'); } else { for (var key of this.facetValues(renderInfo.facetHeader)) { if (this.mapActiveFiltersToFacets.get(key)) { - Doc.setDocFilter(this.targetDoc, renderInfo.facetHeader, key, 'remove'); + Doc.setDocFilter(this.Document, renderInfo.facetHeader, key, 'remove'); } } } @@ -353,7 +267,7 @@ export class FilterPanel extends ObservableReactComponent<filterProps> { this._chosenFacetsCollapse.delete(renderInfo.facetHeader); if (renderInfo.domain) { - Doc.setDocRangeFilter(this.targetDoc, renderInfo.facetHeader, renderInfo.domain, 'remove'); + Doc.setDocRangeFilter(this.Document, renderInfo.facetHeader, renderInfo.domain, 'remove'); } })}> <CiCircleRemove />{' '} @@ -377,16 +291,16 @@ export class FilterPanel extends ObservableReactComponent<filterProps> { case 'text': return ( <input - key={this.targetDoc[Id]} + key={this.Document[Id]} placeholder={'enter text to match'} defaultValue={ - StrListCast(this.targetDoc._childFilters) + StrListCast(this.Document._childFilters) .find(filter => filter.split(Doc.FilterSep)[0] === facetHeader) ?.split(Doc.FilterSep)[1] } style={{ color: SettingsManager.userColor, background: SettingsManager.userBackgroundColor }} - onBlur={undoable(e => Doc.setDocFilter(this.targetDoc, facetHeader, e.currentTarget.value, !e.currentTarget.value ? 'remove' : 'match'), 'set text filter')} - onKeyDown={e => e.key === 'Enter' && undoable(e => Doc.setDocFilter(this.targetDoc, facetHeader, e.currentTarget.value, !e.currentTarget.value ? 'remove' : 'match'), 'set text filter')(e)} + onBlur={undoable(e => Doc.setDocFilter(this.Document, facetHeader, e.currentTarget.value, !e.currentTarget.value ? 'remove' : 'match'), 'set text filter')} + onKeyDown={e => e.key === 'Enter' && undoable(e => Doc.setDocFilter(this.Document, facetHeader, e.currentTarget.value, !e.currentTarget.value ? 'remove' : 'match'), 'set text filter')(e)} /> ); case 'checkbox': @@ -397,12 +311,12 @@ export class FilterPanel extends ObservableReactComponent<filterProps> { <input style={{ width: 20, marginLeft: 20 }} checked={['check', 'exists'].includes( - StrListCast(this.targetDoc._childFilters) + StrListCast(this.Document._childFilters) .find(filter => filter.split(Doc.FilterSep)[0] === facetHeader && filter.split(Doc.FilterSep)[1] == facetValue) ?.split(Doc.FilterSep)[2] ?? '' )} type={type} - onChange={undoable(e => Doc.setDocFilter(this.targetDoc, facetHeader, fval, e.target.checked ? 'check' : 'remove'), 'set filter')} + onChange={undoable(e => Doc.setDocFilter(this.Document, facetHeader, fval, e.target.checked ? 'check' : 'remove'), 'set filter')} /> {facetValue} </div> @@ -412,64 +326,51 @@ export class FilterPanel extends ObservableReactComponent<filterProps> { case 'range': const domain = renderInfoDomain; const range = renderInfoRange; - - if (range) { - console.log('this is info range ' + range[0] + ' , ' + range[1]); - } - if (domain) { - console.log('this is info domain ' + domain[0] + ', ' + domain[1]); - return ( - <> - {/* <div className="sliderBox-outerDiv-checkBox" style={{ float: 'left' }}> - <Checkbox color="primary" onChange={action(() => console.log('on change'))} /> - </div> */} - - <div className="sliderBox-outerDiv" style={{ width: '95%', height: 45, float: 'right' }}> - <Slider - mode={2} - step={Math.min(1, 0.1 * (domain[1] - domain[0]))} - domain={[domain[0], domain[1]]} // -1000, 1000 - rootStyle={{ position: 'relative', width: '100%' }} - onChange={values => Doc.setDocRangeFilter(this.targetDoc, facetHeader, values)} - values={renderInfoRange!}> - <Rail>{railProps => <TooltipRail {...railProps} />}</Rail> - <Handles> - {({ handles, activeHandleID, getHandleProps }) => ( - <div className="slider-handles"> - {handles.map((handle, i) => { - // const value = i === 0 ? defaultValues[0] : defaultValues[1]; - return ( - <div> - <Handle key={handle.id} handle={handle} domain={domain} isActive={handle.id === activeHandleID} getHandleProps={getHandleProps} /> - </div> - ); - })} - </div> - )} - </Handles> - <Tracks left={false} right={false}> - {({ tracks, getTrackProps }) => ( - <div className="slider-tracks"> - {tracks.map(({ id, source, target }) => ( - <Track key={id} source={source} target={target} disabled={false} getTrackProps={getTrackProps} /> - ))} - </div> - )} - </Tracks> - <Ticks count={5}> - {({ ticks }) => ( - <div className="slider-ticks"> - {ticks.map(tick => ( - <Tick key={tick.id} tick={tick} count={ticks.length} format={(val: number) => val.toString()} /> - ))} - </div> - )} - </Ticks> - </Slider> - </div> - </> + <div className="sliderBox-outerDiv" style={{ width: '95%', height: 45, float: 'right' }}> + <Slider + mode={2} + step={Math.min(1, 0.1 * (domain[1] - domain[0]))} + domain={[domain[0], domain[1]]} // -1000, 1000 + rootStyle={{ position: 'relative', width: '100%' }} + onChange={values => Doc.setDocRangeFilter(this.Document, facetHeader, values)} + values={renderInfoRange!}> + <Rail>{railProps => <TooltipRail {...railProps} />}</Rail> + <Handles> + {({ handles, activeHandleID, getHandleProps }) => ( + <div className="slider-handles"> + {handles.map((handle, i) => { + // const value = i === 0 ? defaultValues[0] : defaultValues[1]; + return ( + <div> + <Handle key={handle.id} handle={handle} domain={domain} isActive={handle.id === activeHandleID} getHandleProps={getHandleProps} /> + </div> + ); + })} + </div> + )} + </Handles> + <Tracks left={false} right={false}> + {({ tracks, getTrackProps }) => ( + <div className="slider-tracks"> + {tracks.map(({ id, source, target }) => ( + <Track key={id} source={source} target={target} disabled={false} getTrackProps={getTrackProps} /> + ))} + </div> + )} + </Tracks> + <Ticks count={5}> + {({ ticks }) => ( + <div className="slider-ticks"> + {ticks.map(tick => ( + <Tick key={tick.id} tick={tick} count={ticks.length} format={(val: number) => val.toString()} /> + ))} + </div> + )} + </Ticks> + </Slider> + </div> ); } diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index 733383002..e800798ca 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -24,6 +24,7 @@ import { MainView } from './MainView'; import { DocumentLinksButton } from './nodes/DocumentLinksButton'; import { OpenWhereMod } from './nodes/DocumentView'; import { AnchorMenu } from './pdf/AnchorMenu'; +import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; const modifiers = ['control', 'meta', 'shift', 'alt']; type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo; @@ -61,6 +62,8 @@ export class KeyManager { }); public handle = action((e: KeyboardEvent) => { + // accumulate buffer of characters to insert in a new text note. once the note is created, it will stop keyboard events from reaching this function. + if (FormattedTextBox.SelectOnLoadChar) FormattedTextBox.SelectOnLoadChar = FormattedTextBox.SelectOnLoadChar + (e.key === 'Enter' ? '\n' : e.key); e.key === 'Control' && (CtrlKey = true); //if (!Doc.noviceMode && e.key.toLocaleLowerCase() === "shift") DocServer.UPDATE_SERVER_CACHE(true); const keyname = e.key && e.key.toLowerCase(); diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 92644d3c5..122e5c4c3 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -438,7 +438,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() impleme <svg className="inkStroke" style={{ - transform: isInkMask ? `rotate(-${NumCast(this._props.CollectionFreeFormDocumentView?.()._props.w_Rotation?.() ?? 0)}deg) translate(${InkingStroke.MaskDim / 2}px, ${InkingStroke.MaskDim / 2}px)` : undefined, + transform: isInkMask ? `rotate(-${NumCast(this._props.CollectionFreeFormDocumentView?.()._props.rotation ?? 0)}deg) translate(${InkingStroke.MaskDim / 2}px, ${InkingStroke.MaskDim / 2}px)` : undefined, // mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? 'multiply' : 'unset', cursor: this._props.isSelected() ? 'default' : undefined, }} diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index b6cb845a6..56d28ee5d 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -19,7 +19,7 @@ import { Docs } from '../documents/Documents'; import { CalendarManager } from '../util/CalendarManager'; import { CaptureManager } from '../util/CaptureManager'; import { DocumentManager } from '../util/DocumentManager'; -import { DragManager } from '../util/DragManager'; +import { DragManager, dropActionType } from '../util/DragManager'; import { GroupManager } from '../util/GroupManager'; import { HistoryUtil } from '../util/History'; import { Hypothesis } from '../util/HypothesisUtils'; @@ -288,6 +288,7 @@ export class MainView extends ObservableReactComponent<{}> { fa.faFolderOpen, fa.faFolderPlus, fa.faFolderClosed, + fa.faBook, fa.faMapPin, fa.faMapMarker, fa.faFingerprint, @@ -397,6 +398,7 @@ export class MainView extends ObservableReactComponent<{}> { fa.faPaintBrush, fa.faTimes, fa.faFlag, + fa.faScroll, fa.faEye, fa.faArrowsAlt, fa.faQuoteLeft, @@ -452,6 +454,7 @@ export class MainView extends ObservableReactComponent<{}> { fa.faAngleDown, fa.faPlayCircle, fa.faClock, + fa.faRoute, fa.faRocket, fa.faExchangeAlt, fa.faHashtag, @@ -488,6 +491,7 @@ export class MainView extends ObservableReactComponent<{}> { fa.faUpload, fa.faBorderAll, fa.faBraille, + fa.faPersonChalkboard, fa.faChalkboard, fa.faPencilAlt, fa.faEyeSlash, @@ -623,7 +627,7 @@ export class MainView extends ObservableReactComponent<{}> { isContentActive={returnTrue} // headerBar is awlays contentActive which means its items are always documentActive ScreenToLocalTransform={this.headerBarScreenXf} childHideResizeHandles={true} - childDragAction="move" + childDragAction={dropActionType.move} dontRegisterView={true} hideResizeHandles={true} PanelWidth={this.headerBarDocWidth} @@ -905,7 +909,7 @@ export class MainView extends ObservableReactComponent<{}> { Document={Doc.MyDockedBtns} docViewPath={returnEmptyDocViewList} fieldKey="data" - dropAction="embed" + dropAction={dropActionType.embed} styleProvider={DefaultStyleProvider} select={emptyFunction} isAnyChildContentActive={returnFalse} diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx index a4303c3aa..f59042b04 100644 --- a/src/client/views/MarqueeAnnotator.tsx +++ b/src/client/views/MarqueeAnnotator.tsx @@ -88,7 +88,7 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP const textRegionAnno = Docs.Create.HTMLMarkerDocument([], { annotationOn: this.props.Document, - text: this.props.selectionText(), + text: this.props.selectionText() as any, // text want an RTFfield, but strings are acceptable, too. backgroundColor: 'transparent', presentation_duration: 2100, presentation_transition: 500, @@ -198,7 +198,7 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP const sourceAnchorCreator = () => this.highlight(this.props.highlightDragSrcColor ?? 'rgba(173, 216, 230, 0.75)', true, undefined, true); // hyperlink color const targetCreator = (annotationOn: Doc | undefined) => { - const target = DocUtils.GetNewTextDoc('Note linked to ' + this.props.Document.title, 0, 0, 100, 100, undefined, annotationOn, 'yellow'); + const target = DocUtils.GetNewTextDoc('Note linked to ' + this.props.Document.title, 0, 0, 100, 100, annotationOn, 'yellow'); FormattedTextBox.SetSelectOnLoad(target); return target; }; diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx index 20dc6c9fa..15b1f0275 100644 --- a/src/client/views/OverlayView.tsx +++ b/src/client/views/OverlayView.tsx @@ -9,7 +9,7 @@ import { Height, Width } from '../../fields/DocSymbols'; import { Id } from '../../fields/FieldSymbols'; import { NumCast } from '../../fields/Types'; import { DocumentType } from '../documents/DocumentTypes'; -import { DragManager } from '../util/DragManager'; +import { DragManager, dropActionType } from '../util/DragManager'; import { Transform } from '../util/Transform'; import { LightboxView } from './LightboxView'; import { ObservableReactComponent } from './ObservableReactComponent'; @@ -196,7 +196,7 @@ export class OverlayView extends ObservableReactComponent<{}> { if (e.metaKey) { const dragData = new DragManager.DocumentDragData([d]); dragData.offset = [-offsetx, -offsety]; - dragData.dropAction = 'move'; + dragData.dropAction = dropActionType.move; dragData.removeDocument = this.removeOverlayDoc; dragData.moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => { return dragData.removeDocument!(doc) ? addDocument(doc) : false; diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx index 456b753b4..a94c18295 100644 --- a/src/client/views/PreviewCursor.tsx +++ b/src/client/views/PreviewCursor.tsx @@ -85,7 +85,7 @@ export class PreviewCursor extends ObservableReactComponent<{}> { } else { FormattedTextBox.PasteOnLoad = e; if (e.clipboardData.getData('dash/pdfAnchor')) e.preventDefault(); - UndoManager.RunInBatch(() => this._addLiveTextDoc?.(DocUtils.GetNewTextDoc('', newPoint[0], newPoint[1], 500, undefined, undefined, undefined)), 'paste'); + UndoManager.RunInBatch(() => this._addLiveTextDoc?.(DocUtils.GetNewTextDoc('', newPoint[0], newPoint[1], 500, undefined, undefined)), 'paste'); } } //pasting in images diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx index cb38ab602..02f288a68 100644 --- a/src/client/views/PropertiesButtons.tsx +++ b/src/client/views/PropertiesButtons.tsx @@ -13,7 +13,8 @@ import { RxWidth } from 'react-icons/rx'; import { TbEditCircle, TbEditCircleOff, TbHandOff, TbHandStop, TbHighlight, TbHighlightOff } from 'react-icons/tb'; import { TfiBarChart } from 'react-icons/tfi'; import { Doc, DocListCast, Opt } from '../../fields/Doc'; -import { RichTextField } from '../../fields/RichTextField'; +import { DocData } from '../../fields/DocSymbols'; +import { ScriptField } from '../../fields/ScriptField'; import { BoolCast, ScriptCast } from '../../fields/Types'; import { ImageField } from '../../fields/URLField'; import { CollectionViewType, DocumentType } from '../documents/DocumentTypes'; @@ -27,6 +28,7 @@ import { InkingStroke } from './InkingStroke'; import './PropertiesButtons.scss'; import { Colors } from './global/globalEnums'; import { DocumentView, OpenWhere } from './nodes/DocumentView'; +import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; @observer export class PropertiesButtons extends React.Component<{}, {}> { @@ -174,6 +176,20 @@ export class PropertiesButtons extends React.Component<{}, {}> { ); } + @computed get flashcardButton() { + return this.propertyToggleBtn( + on => (on ? 'DISABLE FLASHCARD' : 'ENABLE FLASHCARD'), + 'layout_textPainted', + on => `${on ? 'Flashcard enabled' : 'Flashcard disabled'} `, + on => <MdTouchApp />, + (dv, doc) => { + const on = doc.onPaint ? true : false; + doc[DocData].onPaint = on ? undefined : ScriptField.MakeScript(`toggleDetail(documentView, "textPainted")`, { documentView: 'any' }); + doc[DocData].layout_textPainted = on ? undefined : `<ComparisonBox {...props} fieldKey={'${dv?.LayoutFieldKey ?? 'text'}'}/>`; + } + ); + } + @computed get fitContentButton() { return this.propertyToggleBtn( on => (on ? 'PREVIOUS VIEW' : 'VIEW ALL'), //'View All', @@ -481,13 +497,12 @@ export class PropertiesButtons extends React.Component<{}, {}> { render() { const layoutField = this.selectedDoc?.[Doc.LayoutFieldKey(this.selectedDoc)]; - const isText = layoutField instanceof RichTextField; + const isText = SelectionManager.Views.lastElement()?.ComponentView instanceof FormattedTextBox; const isInk = this.selectedDoc?.layout_isSvg; const isImage = layoutField instanceof ImageField; const isMap = this.selectedDoc?.type === DocumentType.MAP; const isCollection = this.selectedDoc?.type === DocumentType.COL; - //TODO: will likely need to create separate note-taking view type here - const isStacking = this.selectedDoc?._type_collection === CollectionViewType.Stacking || this.selectedDoc?._type_collection === CollectionViewType.NoteTaking; + const isStacking = [CollectionViewType.Stacking, CollectionViewType.Masonry, CollectionViewType.NoteTaking].includes(this.selectedDoc?._type_collection as any); const isFreeForm = this.selectedDoc?._type_collection === CollectionViewType.Freeform; const isTree = this.selectedDoc?._type_collection === CollectionViewType.Tree; const isTabView = this.selectedTabView; @@ -508,6 +523,7 @@ export class PropertiesButtons extends React.Component<{}, {}> { {/* {toggle(this.freezeThumb)} */} {toggle(this.forceActiveButton)} {toggle(this.verticalAlignButton, { display: !isText ? 'none' : '' })} + {toggle(this.flashcardButton, { display: !isText ? 'none' : '' })} {toggle(this.fitContentButton, { display: !isFreeForm && !isMap ? 'none' : '' })} {/* {toggle(this.isLightboxButton, { display: !isFreeForm && !isMap ? 'none' : '' })} */} {toggle(this.layout_autoHeightButton, { display: !isText && !isStacking && !isTree ? 'none' : '' })} diff --git a/src/client/views/PropertiesDocContextSelector.tsx b/src/client/views/PropertiesDocContextSelector.tsx index 361451c4d..b8bbde9de 100644 --- a/src/client/views/PropertiesDocContextSelector.tsx +++ b/src/client/views/PropertiesDocContextSelector.tsx @@ -30,12 +30,12 @@ export class PropertiesDocContextSelector extends ObservableReactComponent<Prope const targetContext = this._props.DocView.containerViewPath?.().lastElement()?.Document; const embeddings = Doc.GetEmbeddings(target); const containerProtos = embeddings.filter(embedding => embedding.embedContainer && embedding.embedContainer instanceof Doc).reduce((set, embedding) => set.add(Cast(embedding.embedContainer, Doc, null)), new Set<Doc>()); - const containerSets = Array.from(containerProtos.keys()).map(container => Doc.GetEmbeddings(container)); + const containerSets = Array.from(containerProtos.keys()).map(container => (Doc.GetEmbeddings(container).length ? Doc.GetEmbeddings(container) : [container])); const containers = containerSets.reduce((p, set) => { set.map(s => p.add(s)); return p; }, new Set<Doc>()); - const doclayoutSets = Array.from(containers.keys()).map(dp => Doc.GetEmbeddings(dp)); + const doclayoutSets = Array.from(containers.keys()).map(dp => (Doc.GetEmbeddings(dp).length ? Doc.GetEmbeddings(dp) : [dp])); const doclayouts = Array.from( doclayoutSets .reduce((p, set) => { diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index e4e7bec32..cbd3ff358 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -135,7 +135,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps return this.selectedDoc?.isGroup; } @computed get isStack() { - return [CollectionViewType.Stacking, CollectionViewType.NoteTaking].includes(this.selectedDoc?.type_collection as any); + return [CollectionViewType.Masonry, CollectionViewType.Multicolumn, CollectionViewType.Multirow, CollectionViewType.Stacking, CollectionViewType.NoteTaking].includes(this.selectedDoc?.type_collection as any); } rtfWidth = () => (!this.selectedLayoutDoc ? 0 : Math.min(NumCast(this.selectedLayoutDoc?._width), this._props.width - 20)); @@ -1076,6 +1076,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps <div className="transform-editor"> {!this.isStack ? null : this.getNumber('Gap', ' px', 0, 200, NumCast(this.selectedDoc!.gridGap), (val: number) => !isNaN(val) && (this.selectedDoc!.gridGap = val))} {!this.isStack ? null : this.getNumber('xMargin', ' px', 0, 500, NumCast(this.selectedDoc!.xMargin), (val: number) => !isNaN(val) && (this.selectedDoc!.xMargin = val))} + {!this.isStack ? null : this.getNumber('yMargin', ' px', 0, 500, NumCast(this.selectedDoc!.yMargin), (val: number) => !isNaN(val) && (this.selectedDoc!.yMargin = val))} {!this.isGroup ? null : this.getNumber('Padding', ' px', 0, 500, NumCast(this.selectedDoc!.xPadding), (val: number) => !isNaN(val) && (this.selectedDoc!.xPadding = this.selectedDoc!.yPadding = val))} {this.isInk ? this.controlPointsButton : null} {this.getNumber('Width', ' px', 0, Math.max(1000, this.shapeWid), this.shapeWid, (val: number) => !isNaN(val) && (this.shapeWid = val), 1000, 1)} diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index 0794efe4c..ab811858a 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -61,7 +61,7 @@ function togglePaintView(e: React.MouseEvent, doc: Opt<Doc>, props: Opt<FieldVie value: undefined, }; e.stopPropagation(); - UndoManager.RunInBatch(() => doc && ScriptCast(doc.onPaint).script.run(scriptProps), 'togglePaintView'); + ScriptCast(doc?.onPaint)?.script.run(scriptProps); } export function wavyBorderPath(pw: number, ph: number, inset: number = 0.05) { diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx index 5fc33207e..eed197b0b 100644 --- a/src/client/views/TemplateMenu.tsx +++ b/src/client/views/TemplateMenu.tsx @@ -3,7 +3,7 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast } from '../../fields/Doc'; import { ScriptField } from '../../fields/ScriptField'; -import { Cast, StrCast } from '../../fields/Types'; +import { Cast, DocCast, StrCast } from '../../fields/Types'; import { TraceMobx } from '../../fields/util'; import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../Utils'; import { Docs, DocUtils } from '../documents/Documents'; @@ -45,7 +45,6 @@ class OtherToggle extends React.Component<{ checked: boolean; name: string; togg export interface TemplateMenuProps { docViews: DocumentView[]; - templates?: Map<string, boolean>; } @observer @@ -61,17 +60,6 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { this.props.docViews.map(dv => dv.switchViews(false, 'layout')); }; - @undoBatch - @action - toggleTemplate = (event: React.ChangeEvent<HTMLInputElement>, template: string): void => { - this.props.docViews.forEach(d => (Doc.Layout(d.layoutDoc)['_show' + template] = event.target.checked ? template.toLowerCase() : '')); - }; - - @action - toggleTemplateActivity = (): void => { - this._hidden = !this._hidden; - }; - // todo: add brushes to brushMap to save with a style name onCustomKeypress = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { @@ -81,8 +69,11 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { componentDidMount() { !this._addedKeys && (this._addedKeys = new ObservableSet()); [...Array.from(Object.keys(this.props.docViews[0].Document[DocData])), ...Array.from(Object.keys(this.props.docViews[0].Document))] - .filter(key => key.startsWith('layout_')) - .map(key => runInAction(() => this._addedKeys.add(key.replace('layout_', '')))); + .filter(key => key.startsWith('layout_') && ( + StrCast(this.props.docViews[0].Document[key]).startsWith("<") || + DocCast(this.props.docViews[0].Document[key])?.isTemplateDoc + )) + .map(key => runInAction(() => this._addedKeys.add(key.replace('layout_', '')))); // prettier-ignore } return100 = () => 300; @@ -101,8 +92,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { const noteTypes = DocListCast(Cast(Doc.UserDoc()['template_notes'], Doc, null)?.data); const addedTypes = DocListCast(Cast(Doc.UserDoc()['template_clickFuncs'], Doc, null)?.data); const templateMenu: Array<JSX.Element> = []; - this.props.templates?.forEach((checked, template) => templateMenu.push(<TemplateToggle key={template} template={template} checked={checked} toggle={this.toggleTemplate} />)); - templateMenu.push(<OtherToggle key={'default'} name={'Default'} checked={templateName === 'layout'} toggle={this.toggleDefault} />); + templateMenu.push(<OtherToggle key="default" name={firstDoc.layout instanceof Doc ? StrCast(firstDoc.layout.title) : 'Default'} checked={templateName === 'layout'} toggle={this.toggleDefault} />); addedTypes.concat(noteTypes).map(template => (template.treeView_Checked = this.templateIsUsed(firstDoc, template))); this._addedKeys && Array.from(this._addedKeys) diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 31ca86f0f..8f1633122 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -11,7 +11,7 @@ import { List } from '../../../fields/List'; import { ImageCast, NumCast, StrCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { GetEffectiveAcl, inheritParentAcls } from '../../../fields/util'; -import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, incrementTitleCopy } from '../../../Utils'; +import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, DivHeight, DivWidth, emptyFunction, incrementTitleCopy } from '../../../Utils'; import { DocServer } from '../../DocServer'; import { Docs } from '../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; @@ -421,7 +421,6 @@ export class CollectionDockingView extends CollectionSubView() { if (map?.DashDoc && DocumentManager.Instance.getFirstDocumentView(map.DashDoc)) { SelectionManager.SelectView(DocumentManager.Instance.getFirstDocumentView(map.DashDoc), false); } - if (!className.includes('lm_close')) DocServer.UPDATE_SERVER_CACHE(); } } } @@ -433,8 +432,8 @@ export class CollectionDockingView extends CollectionSubView() { public CaptureThumbnail() { const content = this.DocumentView?.()?.ContentDiv; if (content) { - const _width = Number(getComputedStyle(content).width.replace('px', '')); - const _height = Number(getComputedStyle(content).height.replace('px', '')); + const _width = DivWidth(content); + const _height = DivHeight(content); return CollectionFreeFormView.UpdateIcon(this.layoutDoc[Id] + '-icon' + new Date().getTime(), content, _width, _height, _width, _height, 0, 1, true, this.layoutDoc[Id] + '-icon', (iconFile, _nativeWidth, _nativeHeight) => { const proto = this.dataDoc; // Cast(img.proto, Doc, null)!; proto['thumb_nativeWidth'] = _width; @@ -489,8 +488,10 @@ export class CollectionDockingView extends CollectionSubView() { Doc.AddDocToList(Doc.MyHeaderBar, 'data', tab.DashDoc, undefined, undefined, true); // if you close a tab that is not embedded somewhere else (an embedded Doc can be opened simultaneously in a tab), then add the tab to recently closed if (tab.DashDoc.embedContainer === this.Document) tab.DashDoc.embedContainer = undefined; - if (!tab.DashDoc.embedContainer) Doc.AddDocToList(Doc.MyRecentlyClosed, 'data', tab.DashDoc, undefined, true, true); - Doc.RemoveEmbedding(tab.DashDoc, tab.DashDoc); + if (!tab.DashDoc.embedContainer) { + Doc.AddDocToList(Doc.MyRecentlyClosed, 'data', tab.DashDoc, undefined, true, true); + Doc.RemoveEmbedding(tab.DashDoc, tab.DashDoc); + } } if (CollectionDockingView.Instance) { const dview = CollectionDockingView.Instance.Document; diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx index 41c5d5b42..763d2e3a6 100644 --- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx +++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx @@ -33,8 +33,7 @@ interface CMVFieldRowProps { createDropTarget: (ele: HTMLDivElement) => void; screenToLocalTransform: () => Transform; setDocHeight: (key: string, thisHeight: number) => void; - observeHeight: (myref: any) => void; - unobserveHeight: (myref: any) => void; + refList: any[]; showHandle: boolean; } @@ -68,20 +67,20 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF createRowDropRef = (ele: HTMLDivElement | null) => { this._dropDisposer?.(); - if (ele) { - this._ele = ele; - this._props.observeHeight(ele); - this._dropDisposer = DragManager.MakeDropTarget(ele, this.rowDrop.bind(this), this._props.Document); - } + if (ele) this._dropDisposer = DragManager.MakeDropTarget(ele, this.rowDrop.bind(this), this._props.Document); + else if (this._ele) this.props.refList.splice(this.props.refList.indexOf(this._ele), 1); + this._ele = ele; }; @action componentDidMount() { this.heading = this._props.headingObject?.heading || ''; this.color = this._props.headingObject?.color || '#f1efeb'; this.collapsed = this._props.headingObject?.collapsed || false; + this._ele && this.props.refList.push(this._ele); } componentWillUnmount() { - this._props.unobserveHeight(this._ele); + this._ele && this.props.refList.splice(this.props.refList.indexOf(this._ele), 1); + this._ele = null; } getTrueHeight = () => { diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx index 0f90818ef..8729ef549 100644 --- a/src/client/views/collections/CollectionMenu.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -11,7 +11,7 @@ import { RichTextField } from '../../../fields/RichTextField'; import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../fields/Types'; import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents, Utils } from '../../../Utils'; import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; -import { DragManager } from '../../util/DragManager'; +import { DragManager, dropActionType } from '../../util/DragManager'; import { SelectionManager } from '../../util/SelectionManager'; import { SettingsManager } from '../../util/SettingsManager'; import { Transform } from '../../util/Transform'; @@ -91,7 +91,7 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> { Document={selDoc} docViewPath={returnEmptyDocViewList} fieldKey="data" - dropAction="embed" + dropAction={dropActionType.embed} styleProvider={DefaultStyleProvider} select={emptyFunction} isContentActive={returnTrue} diff --git a/src/client/views/collections/CollectionNoteTakingView.scss b/src/client/views/collections/CollectionNoteTakingView.scss index 91a82d40f..4c2dcf9ab 100644 --- a/src/client/views/collections/CollectionNoteTakingView.scss +++ b/src/client/views/collections/CollectionNoteTakingView.scss @@ -98,6 +98,7 @@ overflow: auto; display: flex; flex-direction: column; + height: max-content; } .collectionSchemaView-previewDoc { diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx index b8133806f..6318620e0 100644 --- a/src/client/views/collections/CollectionNoteTakingView.tsx +++ b/src/client/views/collections/CollectionNoteTakingView.tsx @@ -1,4 +1,4 @@ -import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; +import { action, computed, IReactionDisposer, makeObservable, observable, observe, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, Field, Opt } from '../../../fields/Doc'; @@ -9,7 +9,7 @@ import { listSpec } from '../../../fields/Schema'; import { SchemaHeaderField } from '../../../fields/SchemaHeaderField'; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, returnFalse, returnZero, smoothScroll, Utils } from '../../../Utils'; +import { DivHeight, emptyFunction, returnFalse, returnZero, smoothScroll, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DragManager, dropActionType } from '../../util/DragManager'; import { SnappingManager } from '../../util/SnappingManager'; @@ -45,6 +45,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { public DividerWidth = 16; @observable docsDraggedRowCol: number[] = []; @observable _scroll = 0; + @observable _refList: any[] = []; constructor(props: any) { super(props); @@ -157,8 +158,16 @@ export class CollectionNoteTakingView extends CollectionSubView() { document.addEventListener('pointerup', this.removeDocDragHighlight, true); this._disposers.layout_autoHeight = reaction( () => this.layoutDoc._layout_autoHeight, - layout_autoHeight => - layout_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', '')))))) + layout_autoHeight => layout_autoHeight && this._props.setHeight?.(this.headerMargin + Math.max(...this._refList.map(DivHeight))) + ); + + this._disposers.refList = reaction( + () => ({ refList: this._refList.slice(), autoHeight: this.layoutDoc._layout_autoHeight && !LightboxView.Contains(this.DocumentView?.()) }), + ({ refList, autoHeight }) => { + if (autoHeight) refList.forEach(r => this.observer.observe(r)); + else this.observer.disconnect(); + }, + { fireImmediately: true } ); } @@ -345,7 +354,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { // get the index for where you need to insert the doc you are currently dragging const clientY = this.ScreenToLocalBoxXf().transformPoint(ex, ey)[1]; let dropInd = -1; - let pos0 = (this.refList.lastElement() as HTMLDivElement).children[0].getBoundingClientRect().height + this.yMargin * 2; + let pos0 = (this._refList.lastElement() as HTMLDivElement).children[0].getBoundingClientRect().height + this.yMargin * 2; colDocs.forEach((doc, i) => { let pos1 = this.getDocHeight(doc) + 2 * this.gridGap; pos1 += pos0; @@ -424,7 +433,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { if (super.onInternalDrop(e, de)) { // filter out the currently dragged docs from the child docs, since we will insert them later 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 droppedDocs = this.childDocs.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.childDocList; if (docs && newDocs.length) { @@ -489,65 +498,45 @@ export class CollectionNoteTakingView extends CollectionSubView() { headings = () => Array.from(this.Sections); - refList: any[] = []; - editableViewProps = () => ({ GetValue: () => '', SetValue: this.addGroup, contents: '+ New Column', }); + refList = () => this._refList; + // sectionNoteTaking returns a CollectionNoteTakingViewColumn (which is an individual column) - sectionNoteTaking = (heading: SchemaHeaderField | undefined, docList: Doc[]) => { - const type = 'number'; - return ( - <CollectionNoteTakingViewColumn - key={heading?.heading ?? 'unset'} - unobserveHeight={ref => this.refList.splice(this.refList.indexOf(ref), 1)} - observeHeight={ref => { - if (ref) { - this.refList.push(ref); - this.observer = new _global.ResizeObserver( - action((entries: any) => { - if (this.layoutDoc._layout_autoHeight && ref && this.refList.length && !SnappingManager.IsDragging) { - const height = this.headerMargin + Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace('px', ''))))); - if (!LightboxView.Contains(this.DocumentView?.())) { - this._props.setHeight?.(height); - } - } - }) - ); - this.observer.observe(ref); - } - }} - PanelWidth={this._props.PanelWidth} - select={this._props.select} - addDocument={this.addDocument} - chromeHidden={this.chromeHidden} - colHeaderData={this.colHeaderData} - Document={this.Document} - TemplateDataDocument={this._props.TemplateDataDocument} - resizeColumns={this.resizeColumns} - renderChildren={this.children} - numGroupColumns={this.numGroupColumns} - gridGap={this.gridGap} - pivotField={this.notetakingCategoryField} - fieldKey={this.fieldKey} - dividerWidth={this.DividerWidth} - maxColWidth={this.maxColWidth} - availableWidth={this.availableWidth} - headings={this.headings} - heading={heading?.heading ?? 'unset'} - headingObject={heading} - docList={docList} - yMargin={this.yMargin} - type={type} - createDropTarget={this.createDashEventsTarget} - screenToLocalTransform={this.ScreenToLocalBoxXf} - editableViewProps={this.editableViewProps} - /> - ); - }; + sectionNoteTaking = (heading: SchemaHeaderField | undefined, docList: Doc[]) => ( + <CollectionNoteTakingViewColumn + key={heading?.heading ?? 'unset'} + PanelWidth={this._props.PanelWidth} + refList={this._refList} + select={this._props.select} + addDocument={this.addDocument} + chromeHidden={this.chromeHidden} + colHeaderData={this.colHeaderData} + Document={this.Document} + TemplateDataDocument={this._props.TemplateDataDocument} + resizeColumns={this.resizeColumns} + renderChildren={this.children} + numGroupColumns={this.numGroupColumns} + gridGap={this.gridGap} + pivotField={this.notetakingCategoryField} + fieldKey={this.fieldKey} + dividerWidth={this.DividerWidth} + maxColWidth={this.maxColWidth} + availableWidth={this.availableWidth} + headings={this.headings} + heading={heading?.heading ?? 'unset'} + headingObject={heading} + docList={docList} + yMargin={this.yMargin} + createDropTarget={this.createDashEventsTarget} + screenToLocalTransform={this.ScreenToLocalBoxXf} + editableViewProps={this.editableViewProps} + /> + ); // 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. @@ -619,7 +608,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { return this.isContentActive() === false ? 'none' : undefined; } - observer: any; + observer = new _global.ResizeObserver(() => this._props.setHeight?.(this.headerMargin + Math.max(...this._refList.map(DivHeight)))); render() { TraceMobx(); diff --git a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx index 38846c79d..db178d500 100644 --- a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx +++ b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx @@ -36,15 +36,13 @@ interface CSVFieldColumnProps { yMargin: number; numGroupColumns: number; gridGap: number; - type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | undefined; headings: () => object[]; select: (ctrlPressed: boolean) => void; renderChildren: (docs: Doc[]) => JSX.Element[]; addDocument: (doc: Doc | Doc[]) => boolean; createDropTarget: (ele: HTMLDivElement) => void; screenToLocalTransform: () => Transform; - observeHeight: (myref: any) => void; - unobserveHeight: (myref: any) => void; + refList: any[]; editableViewProps: () => any; resizeColumns: (headers: SchemaHeaderField[]) => boolean; maxColWidth: number; @@ -78,15 +76,18 @@ export class CollectionNoteTakingViewColumn extends ObservableReactComponent<CSV createColumnDropRef = (ele: HTMLDivElement | null) => { this.dropDisposer?.(); - if (ele) { - this._ele = ele; - this._props.observeHeight(ele); - this.dropDisposer = DragManager.MakeDropTarget(ele, this.columnDrop.bind(this), this._props.Document); - } + if (ele) this.dropDisposer = DragManager.MakeDropTarget(ele, this.columnDrop.bind(this), this._props.Document); + else if (this._ele) this.props.refList.slice(this.props.refList.indexOf(this._ele), 1); + this._ele = ele; }; + componentDidMount(): void { + this._ele && this.props.refList.push(this._ele); + } + componentWillUnmount() { - this._props.unobserveHeight(this._ele); + this._ele && this.props.refList.splice(this._props.refList.indexOf(this._ele), 1); + this._ele = null; } @undoBatch @@ -289,11 +290,10 @@ export class CollectionNoteTakingViewColumn extends ObservableReactComponent<CSV render() { TraceMobx(); - const heading = this._heading; return ( <div className="collectionNoteTakingViewFieldColumn" - key={heading} + key={this._heading} style={{ width: this.columnWidth, background: this._background, diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx index d0df77cbe..7d7f0bb61 100644 --- a/src/client/views/collections/CollectionPileView.tsx +++ b/src/client/views/collections/CollectionPileView.tsx @@ -13,6 +13,7 @@ import { computePassLayout, computeStarburstLayout } from './collectionFreeForm' import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; import './CollectionPileView.scss'; import { CollectionSubView } from './CollectionSubView'; +import { dropActionType } from '../../util/DragManager'; @observer export class CollectionPileView extends CollectionSubView() { @@ -72,7 +73,7 @@ export class CollectionPileView extends CollectionSubView() { // pile children never have their contents active, but will be document active whenever the entire pile is. childContentsActive={returnFalse} childDocumentsActive={this._props.isDocumentActive} - childDragAction="move" + childDragAction={dropActionType.move} childClickScript={this.toggleIcon} /> </div> diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 54314f62c..ea1caf58f 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -11,12 +11,11 @@ import { listSpec } from '../../../fields/Schema'; import { SchemaHeaderField } from '../../../fields/SchemaHeaderField'; import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, returnEmptyDoclist, returnFalse, returnNone, returnZero, setupMoveUpEvents, smoothScroll, Utils } from '../../../Utils'; +import { DivHeight, emptyFunction, returnEmptyDoclist, returnFalse, returnNone, returnZero, setupMoveUpEvents, smoothScroll, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { CollectionViewType } from '../../documents/DocumentTypes'; import { DragManager, dropActionType } from '../../util/DragManager'; import { SettingsManager } from '../../util/SettingsManager'; -import { SnappingManager } from '../../util/SnappingManager'; import { Transform } from '../../util/Transform'; import { undoBatch, UndoManager } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; @@ -45,17 +44,16 @@ export type collectionStackingViewProps = { @observer export class CollectionStackingView extends CollectionSubView<Partial<collectionStackingViewProps>>() { + _disposers: { [key: string]: IReactionDisposer } = {}; _masonryGridRef: HTMLDivElement | null = null; // used in a column dragger, likely due for the masonry grid view. We want to use this _draggerRef = React.createRef<HTMLDivElement>(); - // Not sure what a pivot field is. Seems like we cause reaction in MobX get rid of it once we exit this view - _pivotFieldDisposer?: IReactionDisposer; - // Seems like we cause reaction in MobX get rid of our height once we exit this view - _layout_autoHeightDisposer?: IReactionDisposer; // keeping track of documents. Updated on internal and external drops. What's the difference? _docXfs: { height: () => number; width: () => number; stackedDocTransform: () => Transform }[] = []; // Doesn't look like this field is being used anywhere. Obsolete? _columnStart: number = 0; + + @observable _refList: any[] = []; // map of node headers to their heights. Used in Masonry @observable _heightMap = new Map<string, number>(); // Assuming that this is the current css cursor style @@ -207,27 +205,27 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection this._props.setContentViewBox?.(this); // reset section headers when a new filter is inputted - this._pivotFieldDisposer = reaction( + this._disposers.pivotField = reaction( () => this.pivotField, () => (this.dataDoc['_' + this.fieldKey + '_columnHeaders'] = new List()) ); - this._layout_autoHeightDisposer = reaction( + this._disposers.autoHeight = reaction( () => this.layoutDoc._layout_autoHeight, - layout_autoHeight => - layout_autoHeight && - this._props.setHeight?.( - Math.min( - NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), - this.headerMargin + (this.isStackingView ? Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace('px', '')))) : this.refList.reduce((p, r) => p + Number(getComputedStyle(r).height.replace('px', '')), 0)) - ) - ) + layout_autoHeight => layout_autoHeight && this._props.setHeight?.(this.headerMargin + (this.isStackingView ? Math.max(...this._refList.map(DivHeight)) : this._refList.reduce((p, r) => p + DivHeight(r), 0))) + ); + this._disposers.refList = reaction( + () => ({ refList: this._refList.slice(), autoHeight: this.layoutDoc._layout_autoHeight && !LightboxView.Contains(this.DocumentView?.()) }), + ({ refList, autoHeight }) => { + this.observer.disconnect(); + if (autoHeight) refList.forEach(r => this.observer.observe(r)); + }, + { fireImmediately: true } ); } componentWillUnmount() { super.componentWillUnmount(); - this._pivotFieldDisposer?.(); - this._layout_autoHeightDisposer?.(); + Object.keys(this._disposers).forEach(key => this._disposers[key]()); } isAnyChildContentActive = () => this._props.isAnyChildContentActive(); @@ -235,10 +233,6 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => { return this._props.removeDocument?.(doc) && addDocument?.(doc) ? true : false; }; - createRef = (ele: HTMLDivElement | null) => { - this._masonryGridRef = ele; - this.createDashEventsTarget(ele!); //so the whole grid is the drop target? - }; onChildClickHandler = () => this._props.childClickScript || ScriptCast(this.Document.onChildClick); @computed get onChildDoubleClickHandler() { @@ -523,7 +517,6 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection }); }; headings = () => Array.from(this.Sections); - refList: any[] = []; // what a section looks like if we're in stacking view sectionStacking = (heading: SchemaHeaderField | undefined, docList: Doc[]) => { const key = this.pivotField; @@ -536,23 +529,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection } return ( <CollectionStackingViewFieldColumn - unobserveHeight={ref => this.refList.splice(this.refList.indexOf(ref), 1)} - observeHeight={ref => { - if (ref) { - this.refList.push(ref); - this.observer = new _global.ResizeObserver( - action((entries: any) => { - if (this.layoutDoc._layout_autoHeight && ref && this.refList.length && !SnappingManager.IsDragging) { - const height = this.headerMargin + Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace('px', ''))))); - if (!LightboxView.Contains(this.DocumentView?.())) { - this._props.setHeight?.(height); - } - } - }) - ); - this.observer.observe(ref); - } - }} + refList={this._refList} addDocument={this.addDocument} chromeHidden={this.chromeHidden} colHeaderData={this.colHeaderData} @@ -591,21 +568,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection Document={this.Document} chromeHidden={this.chromeHidden} pivotField={this.pivotField} - unobserveHeight={ref => this.refList.splice(this.refList.indexOf(ref), 1)} - observeHeight={ref => { - if (ref) { - this.refList.push(ref); - this.observer = new _global.ResizeObserver( - action((entries: any) => { - if (this.layoutDoc._layout_autoHeight && ref && this.refList.length && !SnappingManager.IsDragging) { - const height = this.refList.reduce((p, r) => p + Number(getComputedStyle(r).height.replace('px', '')), 0); - this._props.setHeight?.(2 * this.headerMargin + height); // bcz: added 2x for header to fix problem with scrollbars appearing in Tools panel - } - }) - ); - this.observer.observe(ref); - } - }} + refList={this._refList} key={heading ? heading.heading : ''} rows={rows} headings={this.headings} @@ -709,7 +672,11 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection @computed get backgroundEvents() { return this._props.isContentActive() === false ? 'none' : undefined; } - observer: any; + + observer = new _global.ResizeObserver(() => this._props.setHeight?.(this.headerMargin + (this.isStackingView ? Math.max(...this._refList.map(DivHeight)) : this._refList.reduce((p, r) => p + DivHeight(r), 0)))); + + onPassiveWheel = (e: WheelEvent) => e.stopPropagation(); + _oldWheel: any; render() { TraceMobx(); const editableViewProps = { @@ -730,7 +697,14 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection <div className="collectionStackingMasonry-cont"> <div className={this.isStackingView ? 'collectionStackingView' : 'collectionMasonryView'} - ref={this.createRef} + ref={ele => { + this._masonryGridRef = ele; + this.createDashEventsTarget(ele); //so the whole grid is the drop target? + this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel); + this._oldWheel = ele; + // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling + ele?.addEventListener('wheel', this.onPassiveWheel, { passive: false }); + }} style={{ overflowY: this.isContentActive() ? 'auto' : 'hidden', background: this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor), diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index c455f20d8..6a3cb759e 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -9,7 +9,7 @@ import { ScriptField } from '../../../fields/ScriptField'; import { BoolCast, NumCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, returnEmptyString, setupMoveUpEvents } from '../../../Utils'; +import { DivHeight, DivWidth, emptyFunction, returnEmptyString, setupMoveUpEvents } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; import { DragManager } from '../../util/DragManager'; @@ -44,8 +44,7 @@ interface CSVFieldColumnProps { addDocument: (doc: Doc | Doc[]) => boolean; createDropTarget: (ele: HTMLDivElement) => void; screenToLocalTransform: () => Transform; - observeHeight: (myref: any) => void; - unobserveHeight: (myref: any) => void; + refList: any[]; } @observer @@ -53,7 +52,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent< private dropDisposer?: DragManager.DragDropDisposer; private _disposers: { [name: string]: IReactionDisposer } = {}; private _headerRef: React.RefObject<HTMLDivElement> = React.createRef(); - @observable private _background = 'inherit'; + @observable _background = 'inherit'; @observable _paletteOn = false; @observable _heading = ''; @observable _color = ''; @@ -71,15 +70,14 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent< // is that the only way to have drop targets? createColumnDropRef = (ele: HTMLDivElement | null) => { this.dropDisposer?.(); - if (ele) { - this._ele = ele; - this._props.observeHeight(ele); - this.dropDisposer = DragManager.MakeDropTarget(ele, this.columnDrop.bind(this), this._props.Document); - } + if (ele) this.dropDisposer = DragManager.MakeDropTarget(ele, this.columnDrop.bind(this), this._props.Document); + else if (this._ele) this.props.refList.splice(this.props.refList.indexOf(this._ele), 1); + this._ele = ele; }; @action componentDidMount() { + this._ele && this.props.refList.push(this._ele); this._disposers.collapser = reaction( () => this._props.headingObject?.collapsed, collapsed => (this.collapsed = collapsed !== undefined ? BoolCast(collapsed) : false), @@ -88,7 +86,8 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent< } componentWillUnmount() { this._disposers.collapser?.(); - this._props.unobserveHeight(this._ele); + this._ele && this.props.refList.splice(this.props.refList.indexOf(this._ele), 1); + this._ele = null; } //TODO: what is scripting? I found it in SetInPlace def but don't know what that is @@ -217,8 +216,8 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent< const layoutItems: ContextMenuProps[] = []; const docItems: ContextMenuProps[] = []; const dataDoc = this._props.TemplateDataDocument || this._props.Document; - const width = this._ele ? Number(getComputedStyle(this._ele).width.replace('px', '')) : 0; - const height = this._ele ? Number(getComputedStyle(this._ele).height.replace('px', '')) : 0; + const width = this._ele ? DivWidth(this._ele) : 0; + const height = this._ele ? DivHeight(this._ele) : 0; DocUtils.addDocumentCreatorMenuItems( doc => { FormattedTextBox.SetSelectOnLoad(doc); diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index fdbd1cc90..59a695a3d 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -219,12 +219,13 @@ export function CollectionSubView<X>(moreProps?: X) { const targetDocments = DocListCast(this.dataDoc[this._props.fieldKey]); const someMoved = !dropAction && docDragData.draggedDocuments.some(drag => targetDocments.includes(drag)); if (someMoved) docDragData.droppedDocuments = docDragData.droppedDocuments.map((drop, i) => (targetDocments.includes(docDragData.draggedDocuments[i]) ? docDragData.draggedDocuments[i] : drop)); - if ((!dropAction || dropAction === 'inSame' || dropAction === 'same' || dropAction === 'move' || someMoved) && docDragData.moveDocument) { + if ((!dropAction || dropAction === dropActionType.inPlace || dropAction === dropActionType.same || dropAction === dropActionType.move || someMoved) && docDragData.moveDocument) { const movedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] === d); const addedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] !== d); if (movedDocs.length) { const canAdd = - (de.embedKey || dropAction || Doc.AreProtosEqual(Cast(movedDocs[0].annotationOn, Doc, null), this.Document)) && (dropAction !== 'inSame' || docDragData.draggedDocuments.every(d => d.embedContainer === this.Document)); + (de.embedKey || dropAction || Doc.AreProtosEqual(Cast(movedDocs[0].annotationOn, Doc, null), this.Document)) && + (dropAction !== dropActionType.inPlace || docDragData.draggedDocuments.every(d => d.embedContainer === this.Document)); const moved = docDragData.moveDocument(movedDocs, this.Document, canAdd ? this.addDocument : returnFalse); added = canAdd || moved ? moved : undefined; } else if (addedDocs.length) { diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx index 38f6aa3e7..b92edd165 100644 --- a/src/client/views/collections/CollectionTimeView.tsx +++ b/src/client/views/collections/CollectionTimeView.tsx @@ -1,12 +1,10 @@ -import { toUpper } from 'lodash'; import { action, computed, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { emptyFunction, returnEmptyString, returnFalse, returnTrue, setupMoveUpEvents } from '../../../Utils'; +import { emptyFunction, returnFalse, returnTrue, setupMoveUpEvents } from '../../../Utils'; import { Doc, Opt, StrListCast } from '../../../fields/Doc'; import { List } from '../../../fields/List'; import { ObjectField } from '../../../fields/ObjectField'; -import { RichTextField } from '../../../fields/RichTextField'; import { listSpec } from '../../../fields/Schema'; import { ComputedField, ScriptField } from '../../../fields/ScriptField'; import { Cast, NumCast, StrCast } from '../../../fields/Types'; @@ -15,7 +13,7 @@ import { DocumentManager } from '../../util/DocumentManager'; import { ScriptingGlobals } from '../../util/ScriptingGlobals'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; -import { EditableView } from '../EditableView'; +import { FieldsDropdown } from '../FieldsDropdown'; import { DocumentView } from '../nodes/DocumentView'; import { FocusViewOptions } from '../nodes/FieldView'; import { PresBox } from '../nodes/trails'; @@ -192,51 +190,6 @@ export class CollectionTimeView extends CollectionSubView() { ContextMenu.Instance.addItem({ description: 'Options...', subitems: layoutItems, icon: 'eye' }); }; - @computed get _allFacets() { - const facets = new Set<string>(); - this.childDocs.forEach(child => Object.keys(Doc.GetProto(child)).forEach(key => facets.add(key))); - Doc.AreProtosEqual(this.dataDoc, this.Document) && this.childDocs.forEach(child => Object.keys(child).forEach(key => facets.add(key))); - return Array.from(facets); - } - menuCallback = (x: number, y: number) => { - ContextMenu.Instance.clearItems(); - const keySet: Set<string> = new Set(['tags']); - - this.childLayoutPairs.map(pair => - this._allFacets - .filter(fieldKey => pair.layout[fieldKey] instanceof RichTextField || typeof pair.layout[fieldKey] === 'number' || typeof pair.layout[fieldKey] === 'boolean' || typeof pair.layout[fieldKey] === 'string') - .filter(fieldKey => fieldKey[0] !== '_' && (fieldKey === 'tags' || fieldKey[0] === toUpper(fieldKey)[0])) - .map(fieldKey => keySet.add(fieldKey)) - ); - - const docItems: ContextMenuProps[] = Array.from(keySet).map(fieldKey => - ({ description: ':' + fieldKey, event: () => (this.layoutDoc._pivotField = fieldKey), icon: 'compress-arrows-alt' })); // prettier-ignore - docItems.push({ description: ':default', event: () => (this.layoutDoc._pivotField = undefined), icon: 'compress-arrows-alt' }); - ContextMenu.Instance.addItem({ description: 'Pivot Fields ...', subitems: docItems, icon: 'eye' }); - ContextMenu.Instance.displayMenu(x, y, ':'); - }; - - @computed get pivotKeyUI() { - return ( - <div className={'pivotKeyEntry'}> - <EditableView - GetValue={returnEmptyString} - SetValue={(value: any) => { - if (value?.length) { - this.layoutDoc._pivotField = value; - return true; - } - return false; - }} - background={'#f1efeb'} // this._props.headingObject ? this._props.headingObject.color : "#f1efeb"; - contents={':' + StrCast(this.layoutDoc._pivotField)} - showMenuOnLoad={true} - display={'inline'} - menuCallback={this.menuCallback} - /> - </div> - ); - } render() { let nonNumbers = 0; @@ -263,7 +216,6 @@ export class CollectionTimeView extends CollectionSubView() { return ( <div className={'collectionTimeView' + (doTimeline ? '' : '-pivot')} onContextMenu={this.specificMenu} style={{ width: this._props.PanelWidth(), height: '100%' }}> - {this.pivotKeyUI} {this.contents} {!this._props.isSelected() || !doTimeline ? null : ( <> @@ -272,6 +224,9 @@ export class CollectionTimeView extends CollectionSubView() { <div className="collectionTimeView-thumb-mid collectionTimeView-thumb" key="max" onPointerDown={this.onMidDown} /> </> )} + <div style={{ right: 0, top: 0, position: 'absolute' }}> + <FieldsDropdown Document={this.Document} selectFunc={fieldKey => (this.layoutDoc._pivotField = fieldKey)} placeholder={StrCast(this.layoutDoc._pivotField)} /> + </div> </div> ); } diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 786301136..3b37bdcfa 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -8,7 +8,7 @@ import { listSpec } from '../../../fields/Schema'; import { ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, returnAll, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnNone, returnOne, returnTrue, returnZero, Utils } from '../../../Utils'; +import { DivHeight, emptyFunction, returnAll, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnNone, returnOne, returnTrue, returnZero, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager, dropActionType } from '../../util/DragManager'; @@ -113,8 +113,8 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree computeHeight = () => { if (!this._isDisposing) { - const titleHeight = !this._titleRef ? this.marginTop() : Number(getComputedStyle(this._titleRef).height.replace('px', '')); - const bodyHeight = Array.from(this.refList).reduce((p, r) => p + Number(getComputedStyle(r).height.replace('px', '')), this.marginBot()) + 6; + const titleHeight = !this._titleRef ? this.marginTop() : DivHeight(this._titleRef); + const bodyHeight = Array.from(this.refList).reduce((p, r) => p + DivHeight(r), this.marginBot()) + 6; this.layoutDoc._layout_autoHeightMargins = bodyHeight; !this._props.dontRegisterView && this._props.setHeight?.(bodyHeight + titleHeight); } @@ -153,12 +153,18 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree return res; } - protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent, dropAction: dropActionType) => { + protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent, targetDropAction: dropActionType) => { const dragData = de.complete.docDragData; if (dragData) { - const sameTree = Doc.AreProtosEqual(dragData.treeViewDoc, this.Document) ? true : false; + const sourceDragAction = dragData.dropAction; + const sameTree = () => Doc.AreProtosEqual(dragData.treeViewDoc, this.Document); const isAlreadyInTree = () => sameTree || dragData.draggedDocuments.some(d => d.embedContainer === this.Document && this.childDocs.includes(d)); - dragData.dropAction = dropAction && !isAlreadyInTree() ? dropAction : sameTree && dragData.dropAction !== 'inSame' ? 'same' : dragData.dropAction; + dragData.dropAction = + targetDropAction && !isAlreadyInTree() // if dropped document is not in the tree + ? targetDropAction // then use the target's drop action if it's specified + : !sameTree() || sourceDragAction === dropActionType.inPlace // if doc from another tree, or a non inPlace source drag action is specified + ? sourceDragAction // use the source dragAction + : dropActionType.same; // otherwise use same tree semantics to move within tree e.stopPropagation(); } }; @@ -287,7 +293,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree @observable _renderCount = 1; @computed get treeViewElements() { TraceMobx(); - const dragAction = StrCast(this.Document.childDragAction) as dropActionType; + const dragAction = StrCast(this.Document.childDragAction) as any as dropActionType; const addDoc = (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => this.addDoc(doc, relativeTo, before); const moveDoc = (d: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => this._props.moveDocument?.(d, target, addDoc) || false; if (this._renderCount < this.treeChildren.length) setTimeout(action(() => (this._renderCount = Math.min(this.treeChildren.length, this._renderCount + 20)))); @@ -414,7 +420,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree return ( <div style={{ display: 'flex', flexDirection: 'column', height: '100%', pointerEvents: 'all' }}> {!this.buttonMenu && !this.noviceExplainer ? null : ( - <div className="documentButtonMenu" ref={action((r: HTMLDivElement | null) => r && (this._headerHeight = Number(getComputedStyle(r).height.replace(/px/, ''))))}> + <div className="documentButtonMenu" ref={action((r: HTMLDivElement | null) => r && (this._headerHeight = DivHeight(r)))}> {this.buttonMenu} {this.noviceExplainer} </div> @@ -473,7 +479,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree isAnnotationOverlayScrollable={true} childDocumentsActive={this._props.isContentActive} fieldKey={this._props.fieldKey + '_annotations'} - dropAction="move" + dropAction={dropActionType.move} select={emptyFunction} addDocument={this.addAnnotationDocument} removeDocument={this.remAnnotationDocument} diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index 02aa76d82..699ec3b95 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -564,6 +564,12 @@ export class TabMinimapView extends ObservableReactComponent<TabMinimapViewProps const dim = Math.max(xbounds, ybounds); return { l: bounds.x + xbounds / 2 - dim / 2, t: bounds.y + ybounds / 2 - dim / 2, cx: bounds.x + xbounds / 2, cy: bounds.y + ybounds / 2, dim }; } + @computed get xPadding() { + return !this.renderBounds ? 0 : Math.max(0, this._props.PanelWidth() / NumCast(this._props.document._freeform_scale, 1) - 2 * (this.renderBounds.cx - this.renderBounds.l)); + } + @computed get yPadding() { + return !this.renderBounds ? 0 : Math.max(0, this._props.PanelHeight() / NumCast(this._props.document._freeform_scale, 1) - 2 * (this.renderBounds.cy - this.renderBounds.l)); + } childLayoutTemplate = () => Cast(this._props.document.childLayoutTemplate, Doc, null); returnMiniSize = () => NumCast(this._props.document._miniMapSize, 150); miniDown = (e: React.PointerEvent) => { @@ -620,6 +626,8 @@ export class TabMinimapView extends ObservableReactComponent<TabMinimapViewProps childFiltersByRanges={CollectionDockingView.Instance?.childDocRangeFilters ?? returnEmptyDoclist} searchFilterDocs={CollectionDockingView.Instance?.searchFilterDocs ?? returnEmptyDoclist} fitContentsToBox={returnTrue} + xPadding={this.xPadding} + yPadding={this.yPadding} /> <div className="miniOverlay" onPointerDown={this.miniDown}> <TabMiniThumb miniLeft={miniLeft} miniTop={miniTop} miniWidth={miniWidth} miniHeight={miniHeight} /> @@ -630,7 +638,7 @@ export class TabMinimapView extends ObservableReactComponent<TabMinimapViewProps render() { return this._props.document.layout !== CollectionView.LayoutString(Doc.LayoutFieldKey(this._props.document)) || this._props.document?._type_collection !== CollectionViewType.Freeform ? null : ( <div className="miniMap-hidden"> - <Popup icon={<FontAwesomeIcon icon="globe-asia" size="lg" />} color={SettingsManager.userVariantColor} type={Type.TERT} onPointerDown={e => e.stopPropagation()} placement={'top-end'} popup={this.popup} /> + <Popup icon={<FontAwesomeIcon icon="globe-asia" size="lg" />} color={SettingsManager.userVariantColor} type={Type.TERT} onPointerDown={e => e.stopPropagation()} placement="top-end" popup={this.popup} /> </div> ); } diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index 85f7cf7fe..c6bbcb0a5 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -439,7 +439,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { droppedDocuments: Doc[], before: boolean, inside: number | boolean, - dropAction: dropActionType, + dropAction: dropActionType | undefined, removeDocument: DragManager.RemoveFunction | undefined, moveDocument: DragManager.MoveFunction | undefined, forceAdd: boolean, @@ -449,10 +449,10 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { const addDoc = inside ? this.localAdd : parentAddDoc; const canAdd = !StrCast((inside ? this.Document : this._props.treeViewParent)?.treeView_FreezeChildren).includes('add') || forceAdd; - if (canAdd && (dropAction !== 'inSame' || droppedDocuments.every(d => d.embedContainer === this._props.parentTreeView?.Document))) { - const move = (!dropAction || canEmbed || dropAction === 'proto' || dropAction === 'move' || dropAction === 'same' || dropAction === 'inSame') && moveDocument; + if (canAdd && (dropAction !== dropActionType.inPlace || droppedDocuments.every(d => d.embedContainer === this._props.parentTreeView?.Document))) { + const move = (!dropAction || canEmbed || dropAction === dropActionType.proto || dropAction === dropActionType.move || dropAction === dropActionType.same || dropAction === dropActionType.inPlace) && moveDocument; this._props.parentTreeView instanceof TreeView && (this._props.parentTreeView.dropping = true); - const res = droppedDocuments.reduce((added, d) => (move ? move(d, undefined, addDoc) || (dropAction === 'proto' ? addDoc(d) : false) : addDoc(d)) || added, false); + const res = droppedDocuments.reduce((added, d) => (move ? move(d, undefined, addDoc) || (dropAction === dropActionType.proto ? addDoc(d) : false) : addDoc(d)) || added, false); this._props.parentTreeView instanceof TreeView && (this._props.parentTreeView.dropping = false); return res; } @@ -631,7 +631,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { } return ( <div> - {!docs?.length || this._props.AddToMap /* hack to identify pres box trees */ ? null : ( + {!docs?.length || this.treeView.outlineMode || this._props.AddToMap /* hack to identify pres box trees */ ? null : ( <div className="treeView-sorting"> <IconButton color={sortings[sorting]?.color} @@ -884,7 +884,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { // prettier-ignore switch (property.split(':')[0]) { case StyleProp.Opacity: return this.treeView.outlineMode ? undefined : 1; - case StyleProp.BackgroundColor: return this.selected ? '#7089bb' : StrCast(doc._backgroundColor, StrCast(doc.backgroundColor)); + case StyleProp.BackgroundColor: return this.selected ? '#7089bb' : undefined;//StrCast(doc._backgroundColor, StrCast(doc.backgroundColor)); case StyleProp.Highlighting: if (this.treeView.outlineMode) return undefined; case StyleProp.BoxShadow: return undefined; case StyleProp.DocContents: @@ -1161,7 +1161,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { const before = pt[1] < rect.top + rect.height / 2; const inside = this.treeView.fileSysMode && !this.Document.isFolder ? false : pt[0] > rect.left + rect.width * 0.33 || (!before && this.treeViewOpen && this.childDocs?.length ? true : false); - const docs = this.treeView.onTreeDrop(de, (docs: Doc[]) => this.dropDocuments(docs, before, inside, 'copy', undefined, undefined, false, false)); + this.treeView.onTreeDrop(de, (docs: Doc[]) => this.dropDocuments(docs, before, inside, dropActionType.copy, undefined, undefined, false, false)); }; render() { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx index 8628ca3c3..43b877705 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx @@ -34,7 +34,7 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio set currState(val) { runInAction(() => (this._currState = val)); } // prettier-ignore componentWillUnmount(): void { - this._props.Freeform.layoutDoc.backgroundColor = this._originalbackground; + this._props.Freeform.dataDoc.backgroundColor = this._originalbackground; } setCurrState = (state: infoState) => { @@ -45,9 +45,9 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio }; setupStates = () => { - this._originalbackground = StrCast(this._props.Freeform.layoutDoc.backgroundColor); + this._originalbackground = StrCast(this._props.Freeform.dataDoc.backgroundColor); // state entry functions - const setBackground = (colour: string) => () => (this._props.Freeform.layoutDoc.backgroundColor = colour); + const setBackground = (colour: string) => () => (this._props.Freeform.dataDoc.backgroundColor = colour); const setOpacity = (opacity: number) => () => (this._props.Freeform.layoutDoc.opacity = opacity); // arc transition trigger conditions const firstDoc = () => (this._props.Freeform.childDocs.length ? this._props.Freeform.childDocs[0] : undefined); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index a69030019..9500b918a 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,6 +1,6 @@ import { Bezier } from 'bezier-js'; import { Colors } from 'browndash-components'; -import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx'; +import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction, trace } from 'mobx'; import { observer } from 'mobx-react'; import { computedFn } from 'mobx-utils'; import * as React from 'react'; @@ -17,7 +17,7 @@ import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../ import { ImageField } from '../../../../fields/URLField'; import { TraceMobx } from '../../../../fields/util'; import { GestureUtils } from '../../../../pen-gestures/GestureUtils'; -import { aggregateBounds, DashColor, emptyFunction, intersectRect, lightOrDark, OmitKeys, returnFalse, returnZero, setupMoveUpEvents, Utils } from '../../../../Utils'; +import { aggregateBounds, DashColor, emptyFunction, intersectRect, lightOrDark, numberValue, OmitKeys, returnFalse, returnZero, setupMoveUpEvents, Utils } from '../../../../Utils'; import { CognitiveServices } from '../../../cognitive_services/CognitiveServices'; import { Docs, DocUtils } from '../../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; @@ -30,14 +30,14 @@ import { SelectionManager } from '../../../util/SelectionManager'; import { freeformScrollMode } from '../../../util/SettingsManager'; import { SnappingManager } from '../../../util/SnappingManager'; import { Transform } from '../../../util/Transform'; -import { undoBatch, UndoManager } from '../../../util/UndoManager'; +import { undoable, undoBatch, UndoManager } from '../../../util/UndoManager'; import { Timeline } from '../../animationtimeline/Timeline'; import { ContextMenu } from '../../ContextMenu'; import { GestureOverlay } from '../../GestureOverlay'; import { CtrlKey } from '../../GlobalKeyHandler'; import { ActiveInkWidth, InkingStroke, SetActiveInkColor, SetActiveInkWidth } from '../../InkingStroke'; import { LightboxView } from '../../LightboxView'; -import { CollectionFreeFormDocumentView, CollectionFreeFormDocumentViewWrapper } from '../../nodes/CollectionFreeFormDocumentView'; +import { CollectionFreeFormDocumentView } from '../../nodes/CollectionFreeFormDocumentView'; import { SchemaCSVPopUp } from '../../nodes/DataVizBox/SchemaCSVPopUp'; import { DocumentView, OpenWhere } from '../../nodes/DocumentView'; import { FieldViewProps, FocusViewOptions } from '../../nodes/FieldView'; @@ -159,8 +159,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection ? { x: cb[0], y: cb[1], r: cb[2], b: cb[3] } : aggregateBounds( this._layoutElements.filter(e => e.bounds?.width && !e.bounds.z).map(e => e.bounds!), - NumCast(this.layoutDoc._xPadding, 10), - NumCast(this.layoutDoc._yPadding, 10) + NumCast(this.layoutDoc._xPadding, this._props.xPadding ?? 10), + NumCast(this.layoutDoc._yPadding, this._props.yPadding ?? 10) ); } @computed get nativeWidth() { @@ -462,7 +462,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const eles = this.childLayoutPairs.map(pair => pair.layout).filter(cd => (this.Document._freeform_useClusters ? NumCast(cd.layout_cluster) : NumCast(cd.group, -1)) === cluster); const clusterDocs = eles.map(ele => DocumentManager.Instance.getDocumentView(ele, this.DocumentView?.())!); const { left, top } = clusterDocs[0].getBounds || { left: 0, top: 0 }; - const de = new DragManager.DocumentDragData(eles, e.ctrlKey || e.altKey ? 'embed' : undefined); + const de = new DragManager.DocumentDragData(eles, e.ctrlKey || e.altKey ? dropActionType.embed : undefined); de.moveDocument = this._props.moveDocument; de.offset = this.screenToFreeformContentsXf.transformDirection(ptsParent.clientX - left, ptsParent.clientY - top); DragManager.StartDocumentDrag( @@ -760,7 +760,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this.onGesture(e, new GestureUtils.GestureEvent(gesture, points, GestureOverlay.getBounds(points), text)); }; - @action onPointerMove = (e: PointerEvent) => { if (this.tryDragCluster(e, this._hitCluster)) { e.stopPropagation(); // we're moving a cluster, so stop propagation and return true to end panning and let the document drag take over @@ -841,10 +840,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection if (excludeT < startSegmentT || excludeT > docCurveTVal) { const localStartTVal = startSegmentT - Math.floor(i / 4); t !== (localStartTVal < 0 ? 0 : localStartTVal) && segment.push(inkSegment.split(localStartTVal < 0 ? 0 : localStartTVal, t)); - segment.length && segments.push(segment); + if (segment.length && (Math.abs(segment[0].points[0].x - segment[0].points.lastElement().x) > 0.5 || Math.abs(segment[0].points[0].y - segment[0].points.lastElement().y) > 0.5)) segments.push(segment); } // start a new segment from the intersection t value - segment = tVals.length - 1 === index ? [inkSegment.split(t).right] : []; + if (tVals.length - 1 === index) { + const split = inkSegment.split(t).right; + if (split && (Math.abs(split.points[0].x - split.points.lastElement().x) > 0.5 || Math.abs(split.points[0].y - split.points.lastElement().y) > 0.5)) segment = [split]; + else segment = []; + } else segment = []; startSegmentT = docCurveTVal; }); } else { @@ -1143,23 +1146,29 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection isContentActive = () => this._props.isContentActive(); - @undoBatch + /** + * Create a new text note of the same style as the one being typed into. + * If the text doc is be part of a larger templated doc, the new Doc will be a copy of the templated Doc + * + * @param fieldProps render props for the text doc being typed into + * @param below whether to place the new text Doc below or to the right of the one being typed into. + * @returns whether the new text doc was created and added successfully + */ + createTextDocCopy = undoable((fieldProps: FieldViewProps, below: boolean) => { + const textDoc = DocCast(fieldProps.Document.rootDocument, fieldProps.Document); + const newDoc = Doc.MakeCopy(textDoc, true); + newDoc[DocData][Doc.LayoutFieldKey(newDoc, fieldProps.LayoutTemplateString)] = undefined; // the copy should not copy the text contents of it source, just the render style + newDoc.x = NumCast(textDoc.x) + (below ? 0 : NumCast(textDoc._width) + 10); + newDoc.y = NumCast(textDoc.y) + (below ? NumCast(textDoc._height) + 10 : 0); + FormattedTextBox.SetSelectOnLoad(newDoc); + FormattedTextBox.DontSelectInitialText = true; + return this.addDocument?.(newDoc); + }, 'copied text note'); + onKeyDown = (e: React.KeyboardEvent, fieldProps: FieldViewProps) => { if ((e.metaKey || e.ctrlKey || e.altKey || fieldProps.Document._createDocOnCR) && ['Tab', 'Enter'].includes(e.key)) { e.stopPropagation?.(); - const below = !e.altKey && e.key !== 'Tab'; - const layout_fieldKey = StrCast(fieldProps.fieldKey); - const newDoc = Doc.MakeCopy(fieldProps.Document, true); - const dataField = fieldProps.Document[Doc.LayoutFieldKey(newDoc)]; - newDoc[DocData][Doc.LayoutFieldKey(newDoc)] = dataField === undefined || Cast(dataField, listSpec(Doc), null)?.length !== undefined ? new List<Doc>([]) : undefined; - if (below) newDoc.y = NumCast(fieldProps.Document.y) + NumCast(fieldProps.Document._height) + 10; - else newDoc.x = NumCast(fieldProps.Document.x) + NumCast(fieldProps.Document._width) + 10; - if (layout_fieldKey !== 'layout' && fieldProps.Document[layout_fieldKey] instanceof Doc) { - newDoc[layout_fieldKey] = fieldProps.Document[layout_fieldKey]; - } - newDoc[DocData].text = undefined; - FormattedTextBox.SetSelectOnLoad(newDoc); - return this.addDocument?.(newDoc); + return this.createTextDocCopy(fieldProps, !e.altKey && e.key !== 'Tab'); } }; @computed get childPointerEvents() { @@ -1181,7 +1190,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const childLayout = entry.pair.layout; const childData = entry.pair.data; return ( - <CollectionFreeFormDocumentViewWrapper + <CollectionFreeFormDocumentView {...OmitKeys(entry, ['replica', 'pair']).omit} key={childLayout[Id] + (entry.replica || '')} Document={childLayout} @@ -1344,7 +1353,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection renderCutoffProvider = computedFn( function renderCutoffProvider(this: any, doc: Doc) { - return !this._renderCutoffData.get(doc[Id] + ''); + return this.Document.isTemplateDoc ? false : !this._renderCutoffData.get(doc[Id] + ''); }.bind(this) ); @@ -1467,7 +1476,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection if (!code.includes('dashDiv')) { const script = CompileScript(code, { params: { docView: 'any' }, typecheck: false, editable: true }); if (script.compiled) script.run({ this: this.DocumentView?.() }); - } else code && !first && eval(code); + } else code && !first && eval?.(code); }, { fireImmediately: true } ); @@ -1648,7 +1657,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection !this._props.isAnnotationOverlay && !Doc.noviceMode && optionItems.push({ description: (this._showAnimTimeline ? 'Close' : 'Open') + ' Animation Timeline', event: action(() => (this._showAnimTimeline = !this._showAnimTimeline)), icon: 'eye' }); - this._props.renderDepth && optionItems.push({ description: 'Use Background Color as Default', event: () => (Cast(Doc.UserDoc().emptyCollection, Doc, null)._backgroundColor = StrCast(this.layoutDoc._backgroundColor)), icon: 'palette' }); + this._props.renderDepth && optionItems.push({ description: 'Use Background Color as Default', event: () => (Cast(Doc.UserDoc().emptyCollection, Doc, null).backgroundColor = StrCast(this.layoutDoc.backgroundColor)), icon: 'palette' }); this._props.renderDepth && optionItems.push({ description: 'Fit Content Once', event: this.fitContentOnce, icon: 'object-group' }); if (!Doc.noviceMode) { optionItems.push({ description: (!Doc.NativeWidth(this.layoutDoc) || !Doc.NativeHeight(this.layoutDoc) ? 'Freeze' : 'Unfreeze') + ' Aspect', event: this.toggleNativeDimensions, icon: 'snowflake' }); @@ -1712,7 +1721,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection incrementalRender = action(() => { if (!LightboxView.LightboxDoc || LightboxView.Contains(this.DocumentView?.())) { const layout_unrendered = this.childDocs.filter(doc => !this._renderCutoffData.get(doc[Id])); - const loadIncrement = 5; + const loadIncrement = this.Document.isTemplateDoc ? Number.MAX_VALUE : 5; for (var i = 0; i < Math.min(layout_unrendered.length, loadIncrement); i++) { this._renderCutoffData.set(layout_unrendered[i][Id] + '', true); } @@ -1962,8 +1971,8 @@ ScriptingGlobals.add(function datavizFromSchema(doc: Doc) { for (let i = 0; i < children.length; i++) { let eachRow = []; for (let j = 0; j < keys.length; j++) { - var cell = children[i][keys[j]]; - if (cell && (cell as string)) cell = cell.toString().replace(/\,/g, ''); + var cell = children[i][keys[j]]?.toString(); + if (cell) cell = cell.toString().replace(/\,/g, ''); eachRow.push(cell); } csvRows.push(eachRow); diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index a417d777a..b913e05ad 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -178,7 +178,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps } else if (!e.ctrlKey && !e.metaKey && SelectionManager.Views.length < 2) { FormattedTextBox.SelectOnLoadChar = Doc.UserDoc().defaultTextLayout && !this._props.childLayoutString ? e.key : ''; FormattedTextBox.LiveTextUndo = UndoManager.StartBatch('type new note'); - this._props.addLiveTextDocument(DocUtils.GetNewTextDoc('-typed text-', x, y, 200, 100, this._props.xPadding === 0)); + this._props.addLiveTextDocument(DocUtils.GetNewTextDoc('-typed text-', x, y, 200, 100)); e.stopPropagation(); } }; @@ -494,7 +494,6 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps followLinkToggle: true, _width: 200, _height: 200, - _layout_fitContentsToBox: true, _layout_showSidebar: true, title: 'overview', }); @@ -635,8 +634,13 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps } MarqueeRef: HTMLDivElement | null = null; + /** + * This is called for every drag movement when a document is dragged over this collection. + * If the document is dragged within 25 pixels of the edge of the collection and paused, this will + * auto scroll the collection so that it can be dragged farther (unless auto panning has been disabled) + */ @action - onDragAutoScroll = (e: CustomEvent<React.DragEvent>) => { + onDragMovePause = (e: CustomEvent<React.DragEvent>) => { if ((e as any).handlePan || this._props.isAnnotationOverlay) return; (e as any).handlePan = true; @@ -659,7 +663,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps <div className="marqueeView" ref={r => { - r?.addEventListener('dashDragAutoScroll', this.onDragAutoScroll as any); + r?.addEventListener('dashDragMovePause', this.onDragMovePause as any); this.MarqueeRef = r; }} style={{ diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx index b181b59ce..125dd2781 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx @@ -1,10 +1,10 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; -import { Button } from 'browndash-components'; -import { action, computed, makeObservable } from 'mobx'; +import { Button, IconButton } from 'browndash-components'; +import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { emptyFunction, returnFalse } from '../../../../Utils'; +import { FaChevronRight } from 'react-icons/fa'; import { Doc, DocListCast } from '../../../../fields/Doc'; import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; import { DragManager, dropActionType } from '../../../util/DragManager'; @@ -37,6 +37,8 @@ const resizerWidth = 8; @observer export class CollectionMulticolumnView extends CollectionSubView() { + @observable _startIndex = 0; + constructor(props: any) { super(props); makeObservable(this); @@ -48,7 +50,7 @@ export class CollectionMulticolumnView extends CollectionSubView() { */ @computed private get ratioDefinedDocs() { - return this.childLayoutPairs.map(pair => pair.layout).filter(layout => StrCast(layout._dimUnit, '*') === DimUnit.Ratio); + return this.childLayouts.filter(layout => StrCast(layout._dimUnit, '*') === DimUnit.Ratio); } @computed @@ -57,6 +59,15 @@ export class CollectionMulticolumnView extends CollectionSubView() { return ratioDocs.length ? Math.min(...ratioDocs.map(layout => NumCast(layout._dimMagnitude))) : 1; } + @computed get maxShown() { + return NumCast(this.layoutDoc.layout_maxShown); + } + + @computed + private get childLayouts() { + return (this.maxShown ? this.childLayoutPairs.slice(this._startIndex, this._startIndex + this.maxShown) : this.childLayoutPairs).map(pair => pair.layout); + } + /** * This loops through all childLayoutPairs and extracts the values for _dimUnit * and _dimMagnitude, ignoring any that are malformed. Additionally, it then @@ -69,9 +80,9 @@ export class CollectionMulticolumnView extends CollectionSubView() { private get resolvedLayoutInformation(): LayoutData { let starSum = 0; const widthSpecifiers: WidthSpecifier[] = []; - this.childLayoutPairs.map(pair => { - const unit = StrCast(pair.layout._dimUnit, '*'); - const magnitude = NumCast(pair.layout._dimMagnitude, this.minimumDim); + this.childLayouts.map(layout => { + const unit = StrCast(layout._dimUnit, '*'); + const magnitude = NumCast(layout._dimMagnitude, this.minimumDim); if (unit && magnitude && magnitude > 0 && resolvedUnits.includes(unit)) { unit === DimUnit.Ratio && (starSum += magnitude); widthSpecifiers.push({ magnitude, unit }); @@ -90,7 +101,7 @@ export class CollectionMulticolumnView extends CollectionSubView() { */ // setTimeout(() => { // const { ratioDefinedDocs } = this; - // if (this.childLayoutPairs.length) { + // if (this.childPairs.length) { // const minimum = this.minimumDim; // if (minimum !== 0) { // ratioDefinedDocs.forEach(layout => layout._dimMagnitude = NumCast(layout._dimMagnitude, 1) / minimum, 1); @@ -187,13 +198,14 @@ export class CollectionMulticolumnView extends CollectionSubView() { return Transform.Identity(); // we're still waiting on promises to resolve } let offset = 0; - for (const { layout: candidate } of this.childLayoutPairs) { + var xf = Transform.Identity(); + this.childLayouts.map(candidate => { if (candidate === layout) { - return this.ScreenToLocalBoxXf().translate(-offset / (this._props.NativeDimScaling?.() || 1), 0); + return (xf = this.ScreenToLocalBoxXf().translate(-offset / (this._props.NativeDimScaling?.() || 1), 0)); } offset += this.lookupPixels(candidate) + resizerWidth; - } - return Transform.Identity(); // type coersion, this case should never be hit + }); + return xf; }; @undoBatch @@ -297,15 +309,15 @@ export class CollectionMulticolumnView extends CollectionSubView() { */ @computed private get contents(): JSX.Element[] | null { - const { childLayoutPairs } = this; const collector: JSX.Element[] = []; - for (let i = 0; i < childLayoutPairs.length; i++) { - const { layout } = childLayoutPairs[i]; + this.childLayouts.forEach((layout, i) => { collector.push( <Tooltip title={'Tab: ' + StrCast(layout.title)} key={'wrapper' + i}> <div className="document-wrapper" style={{ flexDirection: 'column', width: this.lookupPixels(layout) }}> {this.getDisplayDoc(layout)} - <Button tooltip="Remove document from header bar" icon={<FontAwesomeIcon icon="times" size="lg" />} onClick={undoable(e => this._props.removeDocument?.(layout), 'close doc')} color={SettingsManager.userColor} /> + {this.layoutDoc._chromeHidden ? null : ( + <Button tooltip="Remove document from header bar" icon={<FontAwesomeIcon icon="times" size="lg" />} onClick={undoable(e => this._props.removeDocument?.(layout), 'close doc')} color={SettingsManager.userColor} /> + )} <WidthLabel layout={layout} collectionDoc={this.Document} /> </div> </Tooltip>, @@ -317,10 +329,10 @@ export class CollectionMulticolumnView extends CollectionSubView() { select={this._props.select} columnUnitLength={this.getColumnUnitLength} toLeft={layout} - toRight={childLayoutPairs[i + 1]?.layout} + toRight={this.childLayouts[i + 1]} /> ); - } + }); collector.pop(); // removes the final extraneous resize bar return collector; } @@ -339,6 +351,20 @@ export class CollectionMulticolumnView extends CollectionSubView() { marginBottom: NumCast(this.Document._yMargin), }}> {this.contents} + {!this._startIndex ? null : ( + <Tooltip title="scroll back"> + <div style={{ position: 'absolute', bottom: 0, left: 0, background: SettingsManager.userVariantColor }} onClick={action(e => (this._startIndex = Math.min(this.childLayoutPairs.length - 1, this._startIndex + this.maxShown)))}> + <Button tooltip="Scroll back" icon={<FontAwesomeIcon icon="chevron-left" size="lg" />} onClick={action(e => (this._startIndex = Math.max(0, this._startIndex - this.maxShown)))} color={SettingsManager.userColor} /> + </div> + </Tooltip> + )} + {this._startIndex > this.childLayoutPairs.length - 1 || !this.maxShown ? null : ( + <Tooltip title="scroll forward"> + <div style={{ position: 'absolute', bottom: 0, right: 0, background: SettingsManager.userVariantColor }} onClick={action(e => (this._startIndex = Math.min(this.childLayoutPairs.length - 1, this._startIndex + this.maxShown)))}> + <IconButton icon={<FaChevronRight />} color={SettingsManager.userColor} /> + </div> + </Tooltip> + )} </div> ); } diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx index 659f7ccdc..17bf3e50c 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx @@ -1,7 +1,6 @@ import { action, computed, makeObservable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { emptyFunction, returnFalse } from '../../../../Utils'; import { Doc, DocListCast } from '../../../../fields/Doc'; import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; import { DragManager, dropActionType } from '../../../util/DragManager'; diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss index fed4e89cf..ac0bd2378 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss +++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss @@ -38,91 +38,97 @@ background: yellow; } } + } - .schema-column-menu, - .schema-filter-menu { - background: $light-gray; - position: absolute; - min-width: 200px; - max-width: 400px; - display: flex; - flex-direction: column; - align-items: flex-start; - z-index: 1; - - .schema-key-search-input { - width: calc(100% - 20px); - margin: 10px; - } + .schema-preview-divider { + height: 100%; + background: black; + cursor: ew-resize; + } +} - .schema-search-result { - cursor: pointer; - padding: 5px 10px; - width: 100%; +.schema-column-menu, +.schema-filter-menu { + background: $light-gray; + position: relative; + min-width: 200px; + max-width: 400px; + display: flex; + flex-direction: column; + align-items: flex-start; + z-index: 1; - &:hover { - background-color: $medium-gray; - } + .schema-key-search-input { + width: calc(100% - 20px); + margin: 10px; + } - .schema-search-result-type, - .schema-search-result-desc { - color: $dark-gray; - font-size: $body-text; - } - } + .schema-search-result { + cursor: pointer; + padding: 5px 10px; + width: 100%; - .schema-key-search, - .schema-new-key-options { - width: 100%; - display: flex; - flex-direction: column; - align-items: flex-start; - } + &:hover { + background-color: $medium-gray; + } + .schema-search-result-type { + font-family: 'Courier New', Courier, monospace; + } - .schema-new-key-options { - margin: 10px; - .schema-key-warning { - color: red; - font-weight: normal; - align-self: center; - } - } + .schema-search-result-type, + .schema-search-result-desc { + color: $dark-gray; + font-size: $body-text; + } + .schema-search-result-desc { + font-style: italic; + } + } - .schema-key-list { - width: 100%; - max-height: 300px; - overflow-y: auto; - } + .schema-key-search, + .schema-new-key-options { + width: 100%; + display: flex; + flex-direction: column; + align-items: flex-start; + } - .schema-key-type-option { - margin: 2px 0px; + .schema-new-key-options { + margin: 10px; + .schema-key-warning { + color: red; + font-weight: normal; + align-self: center; + } + } - input { - margin-right: 5px; - } - } + .schema-key-list { + width: 100%; + max-height: 300px; + overflow-y: auto; + } - .schema-key-default-val { - margin: 5px 0; - } + .schema-key-type-option { + margin: 2px 0px; - .schema-column-menu-button { - cursor: pointer; - padding: 2px 5px; - background: $medium-blue; - border-radius: 9999px; - color: $white; - width: fit-content; - margin: 5px; - align-self: center; - } + input { + margin-right: 5px; } } - .schema-preview-divider { - height: 100%; - background: black; - cursor: ew-resize; + .schema-key-default-val { + margin: 5px 0; + } + + .schema-column-menu-button { + cursor: pointer; + padding: 2px 5px; + background: $medium-blue; + border-radius: 9999px; + color: $white; + width: fit-content; + margin: 5px; + align-self: center; } } @@ -133,6 +139,7 @@ .row-menu { display: flex; justify-content: center; + cursor: pointer; } } diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx index 31b4a2dd4..12f0ad5e9 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx @@ -1,5 +1,5 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, makeObservable, observable, ObservableMap, observe } from 'mobx'; +import { action, computed, makeObservable, observable, ObservableMap, observe, trace } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast, Field, NumListCast, Opt, StrListCast } from '../../../../fields/Doc'; @@ -10,7 +10,7 @@ import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Ty import { emptyFunction, returnEmptyDoclist, returnEmptyString, returnFalse, returnIgnore, returnNever, returnTrue, setupMoveUpEvents, smoothScroll } from '../../../../Utils'; import { Docs, DocumentOptions, DocUtils, FInfo } from '../../../documents/Documents'; import { DocumentManager } from '../../../util/DocumentManager'; -import { DragManager } from '../../../util/DragManager'; +import { DragManager, dropActionType } from '../../../util/DragManager'; import { SelectionManager } from '../../../util/SelectionManager'; import { undoable, undoBatch } from '../../../util/UndoManager'; import { ContextMenu } from '../../ContextMenu'; @@ -25,6 +25,8 @@ import { CollectionSubView } from '../CollectionSubView'; import './CollectionSchemaView.scss'; import { SchemaColumnHeader } from './SchemaColumnHeader'; import { SchemaRowBox } from './SchemaRowBox'; +import { Popup, PopupTrigger, Type } from 'browndash-components'; +import { SettingsManager } from '../../../util/SettingsManager'; const { default: { SCHEMA_NEW_NODE_HEIGHT } } = require('../../global/globalCssVariables.module.scss'); // prettier-ignore export enum ColumnType { @@ -161,7 +163,7 @@ export class CollectionSchemaView extends CollectionSubView() { (change as any).added.forEach((doc: Doc) => // for each document added Doc.GetAllPrototypes(doc.value as Doc).forEach(proto => // for all of its prototypes (and itself) Object.keys(proto).forEach(action(key => // check if any of its keys are new, and add them - !this.fieldInfos.get(key) && this.fieldInfos.set(key, new FInfo(key, key === 'author')))))); + !this.fieldInfos.get(key) && this.fieldInfos.set(key, new FInfo("-no description-", key === 'author')))))); break; case 'update': //let oldValue = change.oldValue; // fill this in if the entire child list will ever be reassigned with a new list } @@ -681,6 +683,7 @@ export class CollectionSchemaView extends CollectionSubView() { } else { this.setKey(this._menuValue, this._newFieldDefault); } + this._columnMenuIndex = undefined; })}> done </div> @@ -688,12 +691,12 @@ export class CollectionSchemaView extends CollectionSubView() { ); } - onPassiveWheel = (e: WheelEvent) => { + onKeysPassiveWheel = (e: WheelEvent) => { // if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this) - if (!this._oldWheel.scrollTop && e.deltaY <= 0) e.preventDefault(); + if (!this._oldKeysWheel.scrollTop && e.deltaY <= 0) e.preventDefault(); e.stopPropagation(); }; - _oldWheel: any; + _oldKeysWheel: any; @computed get keysDropdown() { return ( <div className="schema-key-search"> @@ -708,9 +711,9 @@ export class CollectionSchemaView extends CollectionSubView() { <div className="schema-key-list" ref={r => { - this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel); - this._oldWheel = r; - r?.addEventListener('wheel', this.onPassiveWheel, { passive: false }); + this._oldKeysWheel?.removeEventListener('wheel', this.onKeysPassiveWheel); + this._oldKeysWheel = r; + r?.addEventListener('wheel', this.onKeysPassiveWheel, { passive: false }); }}> {this._menuKeys.map(key => ( <div @@ -721,14 +724,14 @@ export class CollectionSchemaView extends CollectionSubView() { }}> <p> <span className="schema-search-result-key"> - {key} - {this.fieldInfos.get(key)!.fieldType ? ', ' : ''} + <b>{key}</b> + {this.fieldInfos.get(key)!.fieldType ? ':' : ''} </span> <span className="schema-search-result-type" style={{ color: this.fieldInfos.get(key)!.readOnly ? 'red' : 'inherit' }}> {this.fieldInfos.get(key)!.fieldType} </span> + <span className="schema-search-result-desc"> {this.fieldInfos.get(key)!.description}</span> </p> - <p className="schema-search-result-desc">{this.fieldInfos.get(key)!.description}</p> </div> ))} </div> @@ -740,16 +743,17 @@ export class CollectionSchemaView extends CollectionSubView() { const x = this._columnMenuIndex! == -1 ? 0 : this.displayColumnWidths.reduce((total, curr, index) => total + (index < this._columnMenuIndex! ? curr : 0), CollectionSchemaView._rowMenuWidth); return ( <div className="schema-column-menu" style={{ left: x, minWidth: CollectionSchemaView._minColWidth }}> - <input className="schema-key-search-input" type="text" value={this._menuValue} onKeyDown={this.onSearchKeyDown} onChange={this.updateKeySearch} onPointerDown={e => e.stopPropagation()} /> + <input className="schema-key-search-input" type="text" onKeyDown={this.onSearchKeyDown} onChange={this.updateKeySearch} onPointerDown={e => e.stopPropagation()} /> + {this._makeNewField ? this.newFieldMenu : this.keysDropdown} + </div> + ); + } + get renderKeysMenu() { + console.log('RNDERMENUT:' + this._columnMenuIndex); + return ( + <div className="schema-column-menu" style={{ left: 0, minWidth: CollectionSchemaView._minColWidth }}> + <input className="schema-key-search-input" type="text" onKeyDown={this.onSearchKeyDown} onChange={this.updateKeySearch} onPointerDown={e => e.stopPropagation()} /> {this._makeNewField ? this.newFieldMenu : this.keysDropdown} - <div - className="schema-column-menu-button" - onPointerDown={action(e => { - e.stopPropagation(); - this.closeColumnMenu(); - })}> - cancel - </div> </div> ); } @@ -833,6 +837,8 @@ export class CollectionSchemaView extends CollectionSubView() { isContentActive = () => this._props.isSelected() || this._props.isContentActive(); screenToLocal = () => this.ScreenToLocalBoxXf().translate(-this.tableWidth, 0); previewWidthFunc = () => this.previewWidth; + onPassiveWheel = (e: WheelEvent) => e.stopPropagation(); + _oldWheel: any; render() { return ( <div className="collectionSchemaView" ref={(ele: HTMLDivElement | null) => this.createDashEventsTarget(ele)} onDrop={this.onExternalDrop.bind(this)}> @@ -841,15 +847,23 @@ export class CollectionSchemaView extends CollectionSubView() { className="schema-table" style={{ width: `calc(100% - ${this.previewWidth}px)` }} onWheel={e => this._props.isContentActive() && e.stopPropagation()} - ref={r => { - // prevent wheel events from passively propagating up through containers - r?.addEventListener('wheel', (e: WheelEvent) => {}, { passive: false }); + ref={ele => { + // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling + this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel); + (this._oldWheel = ele)?.addEventListener('wheel', this.onPassiveWheel, { passive: false }); }}> <div className="schema-header-row" style={{ height: this.rowHeightFunc() }}> <div className="row-menu" style={{ width: CollectionSchemaView._rowMenuWidth }}> - <div className="schema-header-button" onPointerDown={e => (this._columnMenuIndex === -1 ? this.closeColumnMenu() : this.openColumnMenu(-1, true))}> - <FontAwesomeIcon icon="plus" /> - </div> + <Popup + placement="right" + background={SettingsManager.userBackgroundColor} + color={SettingsManager.userColor} + toggle={<FontAwesomeIcon onPointerDown={e => this.openColumnMenu(-1, true)} icon="plus" />} + trigger={PopupTrigger.CLICK} + type={Type.TERT} + isOpen={this._columnMenuIndex !== -1 ? false : undefined} + popup={this.renderKeysMenu} + /> </div> {this.columnKeys.map((key, index) => ( <SchemaColumnHeader @@ -870,7 +884,7 @@ export class CollectionSchemaView extends CollectionSubView() { /> ))} </div> - {this._columnMenuIndex !== undefined && this.renderColumnMenu} + {this._columnMenuIndex !== undefined && this._columnMenuIndex !== -1 && this.renderColumnMenu} {this._filterColumnIndex !== undefined && this.renderFilterMenu} <CollectionSchemaViewDocs schema={this} childDocs={this.sortedDocsFunc} rowHeight={this.rowHeightFunc} setRef={(ref: HTMLDivElement | null) => (this._tableContentRef = ref)} /> {this.layoutDoc.chromeHidden ? null : ( @@ -981,7 +995,7 @@ class CollectionSchemaViewDoc extends ObservableReactComponent<CollectionSchemaV styleProvider={this.noOpacityStyleProvider} waitForDoubleClickToClick={returnNever} defaultDoubleClick={returnIgnore} - dragAction="move" + dragAction={dropActionType.move} onClickScriptDisable="always" focus={this._props.schema.focusDocument} childFilters={this._props.schema.childDocFilters} diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx index 001ad5ab6..ed1b519b4 100644 --- a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx +++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx @@ -10,7 +10,7 @@ import { Doc, DocListCast, Field } from '../../../../fields/Doc'; import { RichTextField } from '../../../../fields/RichTextField'; import { BoolCast, Cast, DateCast, DocCast, FieldValue, StrCast } from '../../../../fields/Types'; import { ImageField } from '../../../../fields/URLField'; -import { FInfo } from '../../../documents/Documents'; +import { FInfo, FInfoFieldType } from '../../../documents/Documents'; import { DocFocusOrOpen } from '../../../util/DocumentManager'; import { Transform } from '../../../util/Transform'; import { undoBatch, undoable } from '../../../util/UndoManager'; @@ -29,6 +29,7 @@ import { Popup, Size, Type } from 'browndash-components'; import { IconLookup, faCaretDown } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { SettingsManager } from '../../../util/SettingsManager'; +import { dropActionType } from '../../../util/DragManager'; export interface SchemaTableCellProps { Document: Doc; @@ -86,7 +87,7 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro isSelected: returnFalse, setHeight: returnFalse, select: emptyFunction, - dragAction: 'move', + dragAction: dropActionType.move, renderDepth: 1, isContentActive: returnFalse, whenChildContentsActiveChanged: emptyFunction, @@ -133,7 +134,7 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro if (shiftDown && enterKey) { this._props.setColumnValues(this._props.fieldKey.replace(/^_/, ''), value); } - const ret = KeyValueBox.SetField(this._props.Document, this._props.fieldKey.replace(/^_/, ''), value); + const ret = KeyValueBox.SetField(this._props.Document, this._props.fieldKey.replace(/^_/, ''), value, Doc.IsDataProto(this._props.Document) ? true : undefined); this._props.finishEdit?.(); return ret; }, 'edit schema cell')} @@ -149,7 +150,7 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro if (cellValue instanceof DateField) return ColumnType.Date; if (cellValue instanceof RichTextField) return ColumnType.RTF; if (typeof cellValue === 'number') return ColumnType.Any; - if (typeof cellValue === 'string' && columnTypeStr !== 'enumeration') return ColumnType.Any; + if (typeof cellValue === 'string' && columnTypeStr !== FInfoFieldType.enumeration) return ColumnType.Any; if (typeof cellValue === 'boolean') return ColumnType.Boolean; if (columnTypeStr && columnTypeStr in FInfotoColType) { diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts index 51672513b..0579b07c7 100644 --- a/src/client/views/global/globalScripts.ts +++ b/src/client/views/global/globalScripts.ts @@ -21,6 +21,9 @@ import { RichTextMenu } from '../nodes/formattedText/RichTextMenu'; import { WebBox } from '../nodes/WebBox'; import { VideoBox } from '../nodes/VideoBox'; import { DocData } from '../../../fields/DocSymbols'; +import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; +import { PrefetchProxy } from '../../../fields/Proxy'; +import { MakeTemplate } from '../../util/DropConverter'; ScriptingGlobals.add(function IsNoneSelected() { return SelectionManager.Views.length <= 0; @@ -72,13 +75,29 @@ ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: b }); // toggle: Set overlay status of selected document +ScriptingGlobals.add(function setDefaultTemplate(checkResult?: boolean) { + if (checkResult) { + return Doc.UserDoc().defaultTextLayout; + } + const view = SelectionManager.Views.length === 1 && SelectionManager.Views[0].ComponentView instanceof FormattedTextBox ? SelectionManager.Views[0] : undefined; + + if (view) { + const tempDoc = view.Document; + if (!view.layoutDoc.isTemplateDoc) { + MakeTemplate(tempDoc); + } + Doc.UserDoc().defaultTextLayout = new PrefetchProxy(tempDoc); + tempDoc && Doc.AddDocToList(Cast(Doc.UserDoc().template_notes, Doc, null), 'data', tempDoc); + } else Doc.UserDoc().defaultTextLayout = undefined; +}); +// toggle: Set overlay status of selected document ScriptingGlobals.add(function setHeaderColor(color?: string, checkResult?: boolean) { if (checkResult) { return SelectionManager.Views.length ? StrCast(SelectionManager.Docs.lastElement().layout_headingColor) : Doc.SharingDoc().headingColor; } if (SelectionManager.Views.length) { SelectionManager.Docs.forEach(doc => { - Doc.GetProto(doc).layout_headingColor = color; + doc[DocData].layout_headingColor = color === 'transparent' ? undefined : color; doc.layout_showTitle = color === 'transparent' ? undefined : StrCast(doc.layout_showTitle, 'title'); }); } else { @@ -98,7 +117,7 @@ ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) { selected ? selected.CollectionFreeFormDocumentView?.float() : console.log('[FontIconBox.tsx] toggleOverlay failed'); }); -ScriptingGlobals.add(function showFreeform(attr: 'flashcards' | 'center' | 'grid' | 'snaplines' | 'clusters' | 'arrange' | 'viewAll' | 'fitOnce', checkResult?: boolean) { +ScriptingGlobals.add(function showFreeform(attr: 'flashcards' | 'center' | 'grid' | 'snaplines' | 'clusters' | 'arrange' | 'viewAll' | 'fitOnce', checkResult?: boolean, persist?: boolean) { const selected = SelectionManager.Docs.lastElement(); // prettier-ignore const map: Map<'flashcards' | 'center' |'grid' | 'snaplines' | 'clusters' | 'arrange'| 'viewAll' | 'fitOnce', { waitForRender?: boolean, checkResult: (doc:Doc) => any; setDoc: (doc:Doc, dv:DocumentView) => void;}> = new Map([ @@ -112,16 +131,16 @@ ScriptingGlobals.add(function showFreeform(attr: 'flashcards' | 'center' | 'grid }], ['viewAll', { checkResult: (doc:Doc) => BoolCast(doc?._freeform_fitContentsToBox, false), - setDoc: (doc:Doc,dv:DocumentView) => doc._freeform_fitContentsToBox = !doc._freeform_fitContentsToBox, + setDoc: (doc:Doc,dv:DocumentView) => { + if (persist) doc._freeform_fitContentsToBox = !doc._freeform_fitContentsToBox; + else if (doc._freeform_fitContentsToBox) doc._freeform_fitContentsToBox = undefined; + else (dv.ComponentView as CollectionFreeFormView)?.fitContentOnce(); + }, }], ['center', { checkResult: (doc:Doc) => BoolCast(doc?._stacking_alignCenter, false), setDoc: (doc:Doc,dv:DocumentView) => doc._stacking_alignCenter = !doc._stacking_alignCenter, }], - ['fitOnce', { - checkResult: (doc:Doc) => false, - setDoc: (doc:Doc, dv:DocumentView) => (dv.ComponentView as CollectionFreeFormView)?.fitContentOnce() - }], ['clusters', { waitForRender: true, // flags that undo batch should terminate after a re-render giving the script the chance to fire checkResult: (doc:Doc) => BoolCast(doc?._freeform_useClusters, false), diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx index e694806a5..81dd0eb98 100644 --- a/src/client/views/linking/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -10,7 +10,7 @@ import { Cast, DocCast, StrCast } from '../../../fields/Types'; import { WebField } from '../../../fields/URLField'; import { DocumentType } from '../../documents/DocumentTypes'; import { DocumentManager } from '../../util/DocumentManager'; -import { DragManager } from '../../util/DragManager'; +import { DragManager, dropActionType } from '../../util/DragManager'; import { LinkFollower } from '../../util/LinkFollower'; import { LinkManager } from '../../util/LinkManager'; import { SelectionManager } from '../../util/SelectionManager'; @@ -45,7 +45,7 @@ export async function StartLinkTargetsDrag(dragEle: HTMLElement, docView: Docume const dragData = new DragManager.DocumentDragData(moddrag.length ? moddrag : draggedDocs); dragData.canEmbed = true; - dragData.dropAction = 'embed'; + dragData.dropAction = dropActionType.embed; DragManager.StartDocumentDrag([dragEle], dragData, downX, downY, undefined); } @@ -89,7 +89,7 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> { this, e, e => { - const dragData = new DragManager.DocumentDragData([this._props.linkDoc], 'embed'); + const dragData = new DragManager.DocumentDragData([this._props.linkDoc], dropActionType.embed); dragData.dropPropertiesToRemove = ['hidden']; DragManager.StartDocumentDrag([this._editRef.current!], dragData, e.x, e.y); return true; @@ -177,12 +177,12 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> { className="linkMenu-name" //title="drag to view target. click to customize." onPointerDown={this.onLinkButtonDown}> <div className="linkMenu-item-buttons"> - <Tooltip title={<div className="dash-tooltip">Edit Link</div>}> + <Tooltip disableInteractive={true} title={<div className="dash-tooltip">Edit Link</div>}> <div className="linkMenu-icon-wrapper" ref={this._editRef} onPointerDown={this.onEdit} onClick={e => e.stopPropagation()}> <FontAwesomeIcon className="linkMenu-icon" icon="edit" size="sm" /> </div> </Tooltip> - <Tooltip title={<div className="dash-tooltip">Show/Hide Link</div>}> + <Tooltip disableInteractive={true} title={<div className="dash-tooltip">Show/Hide Link</div>}> <div title="click to show link" className="linkMenu-icon-wrapper" onPointerDown={this.onIconDown}> <FontAwesomeIcon className="linkMenu-icon" icon={destinationIcon} size="sm" /> </div> @@ -211,7 +211,7 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> { </p> ) : null} <div className="linkMenu-title-wrapper"> - <Tooltip title={<div className="dash-tooltip">Follow Link</div>}> + <Tooltip disableInteractive={true} title={<div className="dash-tooltip">Follow Link</div>}> <p className="linkMenu-destination-title"> {this._props.linkDoc.linksToAnnotation && Cast(this._props.destinationDoc.data, WebField)?.url.href === this._props.linkDoc.annotationUri ? 'Annotation in' : ''} {StrCast(title)} </p> @@ -221,7 +221,7 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> { </div> <div className="linkMenu-item-buttons"> - <Tooltip title={<div className="dash-tooltip">Delete Link</div>}> + <Tooltip disableInteractive={true} title={<div className="dash-tooltip">Delete Link</div>}> <div className="linkMenu-deleteButton" onPointerDown={this.deleteLink} onClick={e => e.stopPropagation()}> <FontAwesomeIcon className="fa-icon" icon="trash" size="sm" /> </div> diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index a0a64ab59..2800ea200 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -1,4 +1,4 @@ -import { action, computed, makeObservable, observable } from 'mobx'; +import { action, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { OmitKeys, numberRange } from '../../../Utils'; @@ -12,14 +12,16 @@ import { DocumentManager } from '../../util/DocumentManager'; import { ScriptingGlobals } from '../../util/ScriptingGlobals'; import { SelectionManager } from '../../util/SelectionManager'; import { DocComponent } from '../DocComponent'; -import { ObservableReactComponent } from '../ObservableReactComponent'; import { StyleProp } from '../StyleProvider'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import './CollectionFreeFormDocumentView.scss'; import { DocumentView, DocumentViewProps, OpenWhere } from './DocumentView'; import { FieldViewProps } from './FieldView'; -export interface CollectionFreeFormDocumentViewWrapperProps extends DocumentViewProps { +/// Ugh, typescript has no run-time way of iterating through the keys of an interface. so we need +/// manaully keep this list of keys in synch wih the fields of the freeFormProps interface +const freeFormPropsKeys = ['x', 'y', 'z', 'zIndex', 'rotation', 'opacity', 'backgroundColor', 'color', 'highlight', 'width', 'height', 'autoDim', 'transition']; +interface freeFormProps { x: number; y: number; z: number; @@ -33,102 +35,17 @@ export interface CollectionFreeFormDocumentViewWrapperProps extends DocumentView opacity?: number; highlight?: boolean; transition?: string; - RenderCutoffProvider: (doc: Doc) => boolean; - CollectionFreeFormView: CollectionFreeFormView; -} -@observer -export class CollectionFreeFormDocumentViewWrapper extends ObservableReactComponent<CollectionFreeFormDocumentViewWrapperProps> { - constructor(props: CollectionFreeFormDocumentViewWrapperProps) { - super(props); - makeObservable(this); - } - @observable X = this.props.x; - @observable Y = this.props.y; - @observable Z = this.props.z; - @observable ZIndex = this.props.zIndex; - @observable Rotation = this.props.rotation; - @observable Opacity = this.props.opacity; - @observable BackgroundColor = this.props.backgroundColor; - @observable Color = this.props.color; - @observable Highlight = this.props.highlight; - @observable Width = this.props.width; - @observable Height = this.props.height; - @observable AutoDim = this.props.autoDim; - @observable Transition = this.props.transition; - CollectionFreeFormView = this.props.CollectionFreeFormView; // needed for type checking - RenderCutoffProvider = this.props.RenderCutoffProvider; // needed for type checking - - get Document() { - return this._props.Document; - } - @computed get WrapperKeys() { - return Object.keys(this).filter(key => key.startsWith('w_')).map(key => key.replace('w_', '')) - .map(key => ({upper:key, lower:key[0].toLowerCase() + key.substring(1)})); // prettier-ignore - } - - // wrapper functions around prop fields that have been converted to observables to keep 'props' from ever changing. - // this way, downstream code only invalidates when it uses a specific prop, not when any prop changes - w_X = () => this.X; // prettier-ignore - w_Y = () => this.Y; // prettier-ignore - w_Z = () => this.Z; // prettier-ignore - w_ZIndex = () => this.ZIndex ?? NumCast(this.Document.zIndex); // prettier-ignore - w_Rotation = () => this.Rotation ?? NumCast(this.Document._rotation); // prettier-ignore - w_Opacity = () => this.Opacity; // prettier-ignore - w_BackgroundColor = () => this.BackgroundColor ?? Cast(this.Document._backgroundColor, 'string', null); // prettier-ignore - w_Color = () => this.Color ?? Cast(this.Document._color, 'string', null); // prettier-ignore - w_Highlight = () => this.Highlight; // prettier-ignore - w_Width = () => this.Width; // prettier-ignore - w_Height = () => this.Height; // prettier-ignore - w_AutoDim = () => this.AutoDim; // prettier-ignore - w_Transition = () => this.Transition; // prettier-ignore - - PanelWidth = () => this._props.autoDim ? this._props.PanelWidth?.() : this.Width; // prettier-ignore - PanelHeight = () => this._props.autoDim ? this._props.PanelHeight?.() : this.Height; // prettier-ignore - - componentDidUpdate(prevProps: Readonly<React.PropsWithChildren<CollectionFreeFormDocumentViewWrapperProps & { fieldKey: string }>>) { - super.componentDidUpdate(prevProps); - this.WrapperKeys.forEach(action(keys => ((this as any)[keys.upper] = (this.props as any)[keys.lower]))); - } - render() { - const layoutProps = this.WrapperKeys.reduce((val, keys) => [(val['w_' + keys.upper] = (this as any)['w_' + keys.upper]), val][1], {} as { [key: string]: Function }); - return ( - <CollectionFreeFormDocumentView - {...OmitKeys(this._props, this.WrapperKeys.map(keys => keys.lower) ).omit} // prettier-ignore - {...layoutProps} - PanelWidth={this.PanelWidth} - PanelHeight={this.PanelHeight} - /> - ); - } } export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { - w_X: () => number; - w_Y: () => number; - w_Z: () => number; - w_ZIndex?: () => number; - w_Rotation?: () => number; - w_Color: () => string; - w_BackgroundColor: () => string; - w_Opacity: () => number | undefined; - w_Highlight: () => boolean | undefined; - w_Transition: () => string | undefined; - w_Width: () => number; - w_Height: () => number; - PanelWidth: () => number; - PanelHeight: () => number; RenderCutoffProvider: (doc: Doc) => boolean; CollectionFreeFormView: CollectionFreeFormView; } - @observer -export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps>() { - constructor(props: CollectionFreeFormDocumentViewProps) { - super(props); - makeObservable(this); - } +export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps & freeFormProps>() { get displayName() { // this makes mobx trace() statements more descriptive return 'CollectionFreeFormDocumentView(' + this.Document.title + ')'; } // prettier-ignore + public static CollectionFreeFormDocViewClassName = 'collectionFreeFormDocumentView-container'; public static animFields: { key: string; val?: number }[] = [ { key: 'x' }, { key: 'y' }, @@ -145,16 +62,53 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF public static animStringFields = ['backgroundColor', 'color', 'fillColor']; // fields that are configured to be animatable using animation frames public static animDataFields = (doc: Doc) => (Doc.LayoutFieldKey(doc) ? [Doc.LayoutFieldKey(doc)] : []); // fields that are configured to be animatable using animation frames - get CollectionFreeFormView() { - return this._props.CollectionFreeFormView; + constructor(props: CollectionFreeFormDocumentViewProps & freeFormProps) { + super(props); + makeObservable(this); + } + + get WrapperKeys() { + // each of these keys is set by the CollectionView and passed via props. however, if any one of these props changes + // (or any other prop), then it's as if they all change. + // Anything that accesses these props will then invalidate unncessarily. + // To avoid this, we copy these prop values into local observables. Now when 'props' changes, nothing invalidates. + // Instead, we copy each values into its observable which ohnly triggers invalidations if the new value is different + // than the old value, and then only things that access that observable will invalidate. + return freeFormPropsKeys + .map(key => ({upper:key[0].toUpperCase() + key.substring(1), lower:key})); // prettier-ignore + } + @observable X = this.props.x; + @observable Y = this.props.y; + @observable Z = this.props.z; + @observable ZIndex = this.props.zIndex; + @observable Rotation = this.props.rotation; + @observable Opacity = this.props.opacity; + @observable BackgroundColor = this.props.backgroundColor; + @observable Color = this.props.color; + @observable Highlight = this.props.highlight; + @observable Width = this.props.width; + @observable Height = this.props.height; + @observable AutoDim = this.props.autoDim; + @observable Transition = this.props.transition; + + componentDidUpdate(prevProps: Readonly<React.PropsWithChildren<CollectionFreeFormDocumentViewProps & freeFormProps>>) { + super.componentDidUpdate(prevProps); + this.WrapperKeys.forEach(action(keys => ((this as any)[keys.upper] = (this.props as any)[keys.lower]))); } + CollectionFreeFormView = this.props.CollectionFreeFormView; // needed for type checking + // this way, downstream code only invalidates when it uses a specific prop, not when any prop changes + DataTransition = () => this._props.transition; // prettier-ignore + RenderCutoffProvider = this.props.RenderCutoffProvider; // needed for type checking + PanelWidth = () => this._props.autoDim ? this._props.PanelWidth?.() : this.Width; // prettier-ignore + PanelHeight = () => this._props.autoDim ? this._props.PanelHeight?.() : this.Height; // prettier-ignore + styleProvider = (doc: Doc | undefined, props: Opt<FieldViewProps>, property: string) => { if (doc === this.layoutDoc) { switch (property) { - case StyleProp.Opacity: return this._props.w_Opacity(); // only change the opacity for this specific document, not its children - case StyleProp.BackgroundColor: return this._props.w_BackgroundColor(); - case StyleProp.Color: return this._props.w_Color(); + 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; } // prettier-ignore } return this._props.styleProvider?.(doc, props, property); @@ -253,14 +207,14 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF nudge = (x: number, y: number) => { const [locX, locY] = this._props.ScreenToLocalTransform().transformDirection(x, y); - this.Document.x = this._props.w_X() + locX; - this.Document.y = this._props.w_Y() + locY; + this.Document.x = this.X + locX; + this.Document.y = this.Y + locY; }; screenToLocalTransform = () => this._props .ScreenToLocalTransform() - .translate(-this._props.w_X(), -this._props.w_Y()) - .rotateDeg(-(this._props.w_Rotation?.() || 0)); + .translate(-this.X, -this.Y) + .rotateDeg(-(this.Rotation || 0)); returnThis = () => this; /// this indicates whether the doc view is activated because of its relationshop to a group @@ -273,27 +227,26 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF const isGroup = this.dataDoc.isGroup && (!this.layoutDoc.backgroundColor || this.layoutDoc.backgroundColor === 'transparent'); return isGroup ? (this._props.isDocumentActive?.() ? 'group' : this._props.isGroupActive?.() ? 'child' : 'inactive') : this._props.isGroupActive?.() ? 'child' : undefined; }; - public static CollectionFreeFormDocViewClassName = 'collectionFreeFormDocumentView-container'; render() { TraceMobx(); - const passOnProps = OmitKeys(this._props, Object.keys(this._props).filter(key => key.startsWith('w_'))).omit; // prettier-ignore + return ( <div className={CollectionFreeFormDocumentView.CollectionFreeFormDocViewClassName} style={{ - width: this._props.PanelWidth(), - height: this._props.PanelHeight(), - transform: `translate(${this._props.w_X()}px, ${this._props.w_Y()}px) rotate(${NumCast(this._props.w_Rotation?.())}deg)`, - transition: this._props.w_Transition?.() || StrCast(this.Document.dataTransition), - zIndex: this._props.w_ZIndex?.(), - display: this._props.w_Width?.() ? undefined : 'none', + width: this.PanelWidth(), + height: this.PanelHeight(), + transform: `translate(${this.X}px, ${this.Y}px) rotate(${NumCast(this.Rotation)}deg)`, + transition: this.Transition || StrCast(this.Document.dataTransition), + zIndex: this.ZIndex, + display: this.Width ? undefined : 'none', }}> - {this._props.RenderCutoffProvider(this.Document) ? ( - <div style={{ position: 'absolute', width: this._props.PanelWidth(), height: this._props.PanelHeight(), background: 'lightGreen' }} /> + {this.RenderCutoffProvider(this.Document) ? ( + <div style={{ position: 'absolute', width: this.PanelWidth(), height: this.PanelHeight(), background: 'lightGreen' }} /> ) : ( <DocumentView - {...passOnProps} - DataTransition={this._props.w_Transition} + {...OmitKeys(this._props,this.WrapperKeys.map(val => val.lower)).omit} // prettier-ignore + DataTransition={this.DataTransition} CollectionFreeFormDocumentView={this.returnThis} styleProvider={this.styleProvider} ScreenToLocalTransform={this.screenToLocalTransform} diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index 828c82142..083575530 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -6,7 +6,7 @@ import { emptyFunction, returnFalse, returnNone, returnZero, setupMoveUpEvents } import { Doc, Opt } from '../../../fields/Doc'; import { DocCast, NumCast, StrCast } from '../../../fields/Types'; import { DocUtils, Docs } from '../../documents/Documents'; -import { DragManager } from '../../util/DragManager'; +import { DragManager, dropActionType } from '../../util/DragManager'; import { undoBatch } from '../../util/UndoManager'; import { SnappingManager } from '../../util/SnappingManager'; import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent'; @@ -15,8 +15,8 @@ import './ComparisonBox.scss'; import { DocumentView } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import { PinProps, PresBox } from './trails'; -import { IntegerType } from 'mongodb'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; +import { RichTextField } from '../../../fields/RichTextField'; @observer export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface { @@ -147,7 +147,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() this, e, e => { - const de = new DragManager.DocumentDragData([DocCast(this.dataDoc[which])], 'move'); + const de = new DragManager.DocumentDragData([DocCast(this.dataDoc[which])], dropActionType.move); de.moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => { this.clearDoc(which); return addDocument(doc); @@ -210,13 +210,15 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() const displayDoc = (which: string) => { const whichDoc = DocCast(this.dataDoc[which]); const targetDoc = DocCast(whichDoc?.annotationOn, whichDoc); - - return targetDoc ? ( + // if there is no Doc in the first comparison slot, but the comparison box's fieldKey slot has a RichTextField, then render a text box to show the contents of the document's field key slot + const layoutTemplateString = !targetDoc && which.endsWith('1') && this.Document[this.fieldKey] instanceof RichTextField ? FormattedTextBox.LayoutString(this.fieldKey) : undefined; + return targetDoc || layoutTemplateString ? ( <> <DocumentView {...this._props} - Document={targetDoc} - TemplateDataDocument={undefined} + renderDepth={this.props.renderDepth + 1} + LayoutTemplateString={layoutTemplateString} + Document={layoutTemplateString ? this.Document : targetDoc} containerViewPath={this.DocumentView?.().docViewPath} moveDocument={which.endsWith('1') ? this.moveDoc1 : this.moveDoc2} removeDocument={which.endsWith('1') ? this.remDoc1 : this.remDoc2} @@ -229,7 +231,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() hideLinkButton={true} pointerEvents={this._isAnyChildContentActive ? undefined : returnNone} /> - {clearButton(which)} + {layoutTemplateString ? null : clearButton(which)} </> // placeholder image if doc is missing ) : ( <div className="placeholder"> diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index c4dab16fb..5421c1b50 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -180,7 +180,6 @@ .documentView-titleWrapper, .documentView-titleWrapper-hover { - overflow: hidden; color: $black; transform-origin: top left; top: 0; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 7e86125e5..7e7a9d7f9 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,11 +1,10 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; -import { Dropdown, DropdownType, Type } from 'browndash-components'; import { Howl } from 'howler'; import { IReactionDisposer, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Bounce, Fade, Flip, JackInTheBox, Roll, Rotate, Zoom } from 'react-awesome-reveal'; -import { Utils, emptyFunction, isTargetChildOf as isParentOf, lightOrDark, returnEmptyString, returnFalse, returnTrue, returnVal, simulateMouseClick } from '../../../Utils'; +import { DivWidth, Utils, emptyFunction, isTargetChildOf as isParentOf, lightOrDark, returnEmptyString, returnFalse, returnTrue, returnVal, simulateMouseClick } from '../../../Utils'; import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../fields/Doc'; import { AclPrivate, Animation, AudioPlay, DocData, DocViews } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; @@ -48,6 +47,7 @@ import { KeyValueBox } from './KeyValueBox'; import { LinkAnchorBox } from './LinkAnchorBox'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; import { PresEffect, PresEffectDirection } from './trails'; +import { FieldsDropdown } from '../FieldsDropdown'; interface Window { MediaRecorder: MediaRecorder; } @@ -413,7 +413,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document if (!Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, Date.now())) { this.cleanupPointerEvents(); this._longPressSelector && clearTimeout(this._longPressSelector); - this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && 'embed') || ((this.Document.dragAction || this._props.dragAction || undefined) as dropActionType)); + this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && dropActionType.embed) || ((this.Document.dragAction || this._props.dragAction || undefined) as dropActionType)); } }; @@ -457,11 +457,11 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document deleteClicked = undoable(() => this._props.removeDocument?.(this.Document), 'delete doc'); setToggleDetail = undoable( - (defaultLayout: string, scriptFieldKey: 'onClick') => + (scriptFieldKey: 'onClick') => (this.Document[scriptFieldKey] = ScriptField.MakeScript( `toggleDetail(documentView, "${StrCast(this.Document.layout_fieldKey) .replace('layout_', '') - .replace(/^layout$/, 'detail')}", "${defaultLayout}")`, + .replace(/^layout$/, 'detail')}")`, { documentView: 'any' } )), 'set toggle detail' @@ -518,7 +518,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document }; onContextMenu = (e?: React.MouseEvent, pageX?: number, pageY?: number) => { - if (e && this.layoutDoc._layout_hideContextMenu && Doc.noviceMode) { + if (e && this.layoutDoc.layout_hideContextMenu && Doc.noviceMode) { e.preventDefault(); e.stopPropagation(); //!this._props.isSelected(true) && SelectionManager.SelectView(this.DocumentView(), false); @@ -705,7 +705,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document panelHeight = () => this._props.PanelHeight() - this.headerMargin; screenToLocalContent = () => this._props.ScreenToLocalTransform().translate(0, -this.headerMargin); onClickFunc = this.disableClickScriptFunc ? undefined : () => this.onClickHandler; - setHeight = (height: number) => !this._props.suppressSetHeight && (this.layoutDoc._height = height); + setHeight = (height: number) => !this._props.suppressSetHeight && (this.layoutDoc._height = Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), height)); setContentView = action((view: ViewBoxInterface) => (this._componentView = view)); isContentActive = (): boolean | undefined => this._isContentActive; childFilters = () => [...this._props.childFilters(), ...StrListCast(this.layoutDoc.childFilters)]; @@ -787,28 +787,25 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document } captionStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps>, property: string) => this._props?.styleProvider?.(doc, props, property + ':caption'); - fieldsDropdown = (reqdFields: string[], dropdownWidth: number, placeholder: string, onChange: (val: string | number) => void, onClose: () => void) => { - const filteredFields = Object.entries(DocOptions).reduce((set, [field, opts]) => (opts.filterable ? set.add(field) : set), new Set(reqdFields)); + fieldsDropdown = (placeholder: string) => { return ( - <div style={{ width: dropdownWidth }}> - <div - ref={action((r: any) => r && (this._titleDropDownInnerWidth = Number(getComputedStyle(r).width.replace('px', ''))))} - onPointerDown={action(e => (this._changingTitleField = true))} - style={{ width: 'max-content', transformOrigin: 'left', transform: `scale(${this.titleHeight / 30 /* height of Dropdown */})` }}> - <Dropdown - activeChanged={action(isOpen => !isOpen && (this._changingTitleField = false))} - selectedVal={placeholder} - setSelectedVal={onChange} - color={SettingsManager.userColor} - background={SettingsManager.userVariantColor} - type={Type.TERT} - closeOnSelect={true} - dropdownType={DropdownType.SELECT} - items={Array.from(filteredFields).map(facet => ({ val: facet, text: facet }))} - width={100} - fillWidth - /> - </div> + <div + ref={action((r: any) => r && (this._titleDropDownInnerWidth = DivWidth(r)))} + onPointerDown={action(e => (this._changingTitleField = true))} + style={{ width: 'max-content', background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor, transformOrigin: 'left', transform: `scale(${this.titleHeight / 30 /* height of Dropdown */})` }}> + <FieldsDropdown + Document={this.Document} + placeholder={placeholder} + selectFunc={action((field: string | number) => { + if (this.layoutDoc.layout_showTitle) { + this.layoutDoc._layout_showTitle = field; + } else if (!this._props.layout_showTitle) { + Doc.UserDoc().layout_showTitle = field; + } + this._changingTitleField = false; + })} + menuClose={action(() => (this._changingTitleField = false))} + /> </div> ); }; @@ -842,22 +839,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document background, pointerEvents: (!this.disableClickScriptFunc && this.onClickHandler) || this.Document.ignoreClick ? 'none' : this.isContentActive() || this._props.isDocumentActive?.() ? 'all' : undefined, }}> - {!dropdownWidth - ? null - : this.fieldsDropdown( - [StrCast(this.layoutDoc.layout_showTitle)], - dropdownWidth, - StrCast(this.layoutDoc.layout_showTitle).split(':')[0], - action((field: string | number) => { - if (this.layoutDoc.layout_showTitle) { - this.layoutDoc._layout_showTitle = field; - } else if (!this._props.layout_showTitle) { - Doc.UserDoc().layout_showTitle = field; - } - this._changingTitleField = false; - }), - action(() => (this._changingTitleField = false)) - )} + {!dropdownWidth ? null : <div style={{ width: dropdownWidth }}>{this.fieldsDropdown(showTitle)}</div>} <div style={{ width: `calc(100% - ${dropdownWidth}px)`, @@ -867,10 +849,12 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document }}> <EditableView ref={this._titleRef} - contents={showTitle - .split(';') - .map(field => targetDoc[field.trim()]?.toString()) - .join(' \\ ')} + contents={ + showTitle + .split(';') + .map(field => Field.toString(targetDoc[field.trim()] as Field)) + .join(' \\ ') || '-unset-' + } display="block" oneLine={true} fontSize={(this.titleHeight / 15) * 10} @@ -1220,7 +1204,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { }; public noOnClick = () => this._docViewInternal?.noOnClick(); public toggleFollowLink = (zoom?: boolean, setTargetToggle?: boolean): void => this._docViewInternal?.toggleFollowLink(zoom, setTargetToggle); - public setToggleDetail = (defaultLayout = '', scriptFieldKey = 'onClick') => this._docViewInternal?.setToggleDetail(defaultLayout, scriptFieldKey); + public setToggleDetail = (scriptFieldKey = 'onClick') => this._docViewInternal?.setToggleDetail(scriptFieldKey); public onContextMenu = (e?: React.MouseEvent, pageX?: number, pageY?: number) => this._docViewInternal?.onContextMenu?.(e, pageX, pageY); public cleanupPointerEvents = () => this._docViewInternal?.cleanupPointerEvents(); public startDragging = (x: number, y: number, dropAction: dropActionType, hideSource = false) => this._docViewInternal?.startDragging(x, y, dropAction, hideSource); @@ -1298,7 +1282,22 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { custom && DocUtils.makeCustomViewClicked(this.Document, Docs.Create.StackingDocument, layout, undefined); }, 'set custom view'); + /** + * This switches between the current view of a Doc and a specified alternate layout view. + * The current view of the Doc is stored in the layout_default field so that it can be restored. + * If the current view of the Doc is already the specified alternate layout view, this will switch + * back to the original layout (stored in layout_default) + * @param detailLayoutKeySuffix the name of the alternate layout field key (NOTE: 'layout_' will be prepended to this string to get the actual field nam) + */ + public toggleDetail = (detailLayoutKeySuffix: string) => { + const curLayout = StrCast(this.Document.layout_fieldKey).replace('layout_', '').replace('layout', ''); + if (!this.Document.layout_default && curLayout !== detailLayoutKeySuffix) this.Document.layout_default = curLayout; + const defaultLayout = StrCast(this.Document.layout_default); + if (this.Document.layout_fieldKey === 'layout_' + detailLayoutKeySuffix) this.switchViews(defaultLayout ? true : false, defaultLayout, undefined, true); + else this.switchViews(true, detailLayoutKeySuffix, undefined, true); + }; public switchViews = (custom: boolean, view: string, finished?: () => void, useExistingLayout = false) => { + const batch = UndoManager.StartBatch('switchView:' + view); runInAction(() => this._docViewInternal && (this._docViewInternal._animateScalingTo = 0.1)); // shrink doc setTimeout( action(() => { @@ -1311,6 +1310,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { setTimeout( action(() => { this._docViewInternal && (this._docViewInternal._animateScalingTo = 0); + batch.end(); finished?.(); }), Math.max(0, (this._docViewInternal?.animateScaleTime() ?? 0) - 10) @@ -1470,9 +1470,8 @@ ScriptingGlobals.add(function deiconifyViewToLightbox(documentView: DocumentView LightboxView.Instance.AddDocTab(documentView.Document, OpenWhere.lightbox, 'layout'); //, 0); }); -ScriptingGlobals.add(function toggleDetail(dv: DocumentView, detailLayoutKeySuffix: string, defaultLayout = '') { - if (dv.Document.layout_fieldKey === 'layout_' + detailLayoutKeySuffix) dv.switchViews(defaultLayout ? true : false, defaultLayout, undefined, true); - else dv.switchViews(true, detailLayoutKeySuffix, undefined, true); +ScriptingGlobals.add(function toggleDetail(dv: DocumentView, detailLayoutKeySuffix: string) { + dv.toggleDetail(detailLayoutKeySuffix); }); ScriptingGlobals.add(function updateLinkCollection(linkCollection: Doc, linkSource: Doc) { @@ -1503,8 +1502,8 @@ ScriptingGlobals.add(function updateTagsCollection(collection: Doc) { let created = false; const matchedDocs = matchedTags .filter(tagDoc => !Doc.AreProtosEqual(collection, tagDoc)) - .map(tagDoc => { - let embedding = collectionDocs.find(doc => Doc.AreProtosEqual(tagDoc, doc)); + .reduce((aset, tagDoc) => { + let embedding = Array.from(aset).find(doc => Doc.AreProtosEqual(tagDoc, doc)) ?? collectionDocs.find(doc => Doc.AreProtosEqual(tagDoc, doc)); if (!embedding) { embedding = Doc.MakeEmbedding(tagDoc); embedding.x = wid; @@ -1513,8 +1512,10 @@ ScriptingGlobals.add(function updateTagsCollection(collection: Doc) { wid += NumCast(tagDoc._width); created = true; } - return embedding; - }); + Doc.SetContainer(embedding, collection); + aset.add(embedding); + return aset; + }, new Set<Doc>()); created && (collection[DocData].data = new List<Doc>(matchedDocs)); return true; diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx index 2e03a766a..50d4c7c78 100644 --- a/src/client/views/nodes/EquationBox.tsx +++ b/src/client/views/nodes/EquationBox.tsx @@ -11,6 +11,7 @@ import { LightboxView } from '../LightboxView'; import './EquationBox.scss'; import { FieldView, FieldViewProps } from './FieldView'; import EquationEditor from './formattedText/EquationEditor'; +import { DivHeight, DivWidth } from '../../../Utils'; @observer export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { @@ -57,8 +58,8 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { @action keyPressed = (e: KeyboardEvent) => { - const _height = Number(getComputedStyle(this._ref.current!.element.current).height.replace('px', '')); - const _width = Number(getComputedStyle(this._ref.current!.element.current).width.replace('px', '')); + const _height = DivHeight(this._ref.current!.element.current); + const _width = DivWidth(this._ref.current!.element.current); if (e.key === 'Enter') { const nextEq = Docs.Create.EquationDocument(e.shiftKey ? StrCast(this.dataDoc.text) : 'x', { title: '# math', diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx index 3577cc8d9..f02ad7300 100644 --- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx @@ -1,13 +1,13 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Button, ColorPicker, Dropdown, DropdownType, EditableText, IconButton, IListItemProps, MultiToggle, NumberDropdown, NumberDropdownType, Popup, Size, Toggle, ToggleType, Type } from 'browndash-components'; -import { computed, makeObservable, observable } from 'mobx'; +import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc'; import { ScriptField } from '../../../../fields/ScriptField'; import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; -import { emptyFunction, setupMoveUpEvents, Utils } from '../../../../Utils'; +import { emptyFunction, returnTrue, setupMoveUpEvents, Utils } from '../../../../Utils'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; import { SelectionManager } from '../../../util/SelectionManager'; import { SettingsManager } from '../../../util/SettingsManager'; @@ -81,9 +81,13 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { } }; - // Determining UI Specs + /** + * this chooses the appropriate title for the label + * if the Document is a template, then we use the title of the data doc that it renders + * otherwise, we use the Document's title itself. + */ @computed get label() { - return StrCast(this.dataDoc.icon_label, StrCast(this.Document.title)); + return StrCast(this.Document.isTemplateDoc ? this.dataDoc.title : this.Document.title); } Icon = (color: string, iconFalse?: boolean) => { let icon; @@ -308,13 +312,16 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { ); } + @observable _hackToRecompute = 0; // bcz: ugh ... <Toggle>'s toggleStatus initializes but doesn't track its value after a click. so a click that does nothing to the toggle state will toggle the button anyway. this forces the Toggle to re-read the ToggleStatus value. + @computed get toggleButton() { // Determine the type of toggle button const buttonText = StrCast(this.dataDoc.buttonText); const tooltip = StrCast(this.Document.toolTip); const script = ScriptCast(this.Document.onClick); - const toggleStatus = script ? script.script.run({ this: this.Document, self: this.Document, value: undefined, _readOnly_: true }).result : false; + const double = ScriptCast(this.Document.onDoubleClick); + const toggleStatus = script?.script.run({ this: this.Document, self: this.Document, value: undefined, _readOnly_: true }).result ?? false; // Colors const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color); const backgroundColor = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor); @@ -330,7 +337,19 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { //background={SettingsManager.userBackgroundColor} icon={this.Icon(color)!} label={this.label} - onPointerDown={() => script.script.run({ this: this.Document, self: this.Document, value: !toggleStatus, _readOnly_: false })} + onPointerDown={e => + setupMoveUpEvents( + this, + e, + returnTrue, + emptyFunction, + action((e, doubleTap) => { + (!doubleTap || !double) && script?.script.run({ this: this.Document, self: this.Document, value: !toggleStatus, _readOnly_: false }); + doubleTap && double?.script.run({ this: this.Document, self: this.Document, value: !toggleStatus, _readOnly_: false }); + this._hackToRecompute = this._hackToRecompute + 1; + }) + ) + } /> ); } diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 923aead64..251235b93 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -141,6 +141,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl const targetField = Doc.LayoutFieldKey(layoutDoc); const targetDoc = layoutDoc[DocData]; if (targetDoc[targetField] instanceof ImageField) { + added = true; this.dataDoc[this.fieldKey] = ObjectField.MakeCopy(targetDoc[targetField] as ImageField); Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(targetDoc), this.fieldKey); Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(targetDoc), this.fieldKey); diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index 39a45693e..89a5ac0b8 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -76,7 +76,7 @@ export class KeyValueBox extends ObservableReactComponent<FieldViewProps> { value = eq ? value.substring(1) : value; const dubEq = value.startsWith(':=') ? 'computed' : value.startsWith('$=') ? 'script' : false; value = dubEq ? value.substring(2) : value; - const options: ScriptOptions = { addReturn: true, typecheck: false, params: { this: Doc.name, self: Doc.name, _last_: 'any', _readOnly_: 'boolean' }, editable: true }; + const options: ScriptOptions = { addReturn: true, typecheck: false, params: { this: Doc.name, self: Doc.name, documentView: 'any', _last_: 'any', _readOnly_: 'boolean' }, editable: true }; if (dubEq) options.typecheck = false; const script = CompileScript(value, { ...options, transformer: DocumentIconContainer.getTransformer() }); return !script.compiled ? undefined : { script, type: dubEq, onDelegate: eq }; diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx index fd3074a88..be20b5934 100644 --- a/src/client/views/nodes/LabelBox.tsx +++ b/src/client/views/nodes/LabelBox.tsx @@ -17,12 +17,8 @@ import './LabelBox.scss'; import { PinProps, PresBox } from './trails'; import { Docs } from '../../documents/Documents'; -export interface LabelBoxProps extends FieldViewProps { - label?: string; -} - @observer -export class LabelBox extends ViewBoxBaseComponent<LabelBoxProps>() { +export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(LabelBox, fieldKey); } @@ -32,7 +28,7 @@ export class LabelBox extends ViewBoxBaseComponent<LabelBoxProps>() { private dropDisposer?: DragManager.DragDropDisposer; private _timeout: any; - constructor(props: LabelBoxProps) { + constructor(props: FieldViewProps) { super(props); makeObservable(this); } @@ -45,7 +41,7 @@ export class LabelBox extends ViewBoxBaseComponent<LabelBoxProps>() { } @computed get Title() { - return this.dataDoc.title_custom ? StrCast(this.Document.title) : this._props.label ? this._props.label : Field.toString(this.dataDoc[this.fieldKey] as Field); + return Field.toString(this.dataDoc[this.fieldKey] as Field); } protected createDropTarget = (ele: HTMLDivElement) => { diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx index 00e1f04c5..0a4325d8c 100644 --- a/src/client/views/nodes/LinkAnchorBox.tsx +++ b/src/client/views/nodes/LinkAnchorBox.tsx @@ -5,7 +5,7 @@ import { Utils, emptyFunction, setupMoveUpEvents } from '../../../Utils'; import { Doc } from '../../../fields/Doc'; import { NumCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; -import { DragManager } from '../../util/DragManager'; +import { DragManager, dropActionType } from '../../util/DragManager'; import { LinkFollower } from '../../util/LinkFollower'; import { SelectionManager } from '../../util/SelectionManager'; import { ViewBoxBaseComponent } from '../DocComponent'; @@ -54,7 +54,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() { const separation = Math.sqrt((pt[0] - e.clientX) * (pt[0] - e.clientX) + (pt[1] - e.clientY) * (pt[1] - e.clientY)); if (separation > 100) { const dragData = new DragManager.DocumentDragData([this.Document]); - dragData.dropAction = 'embed'; + dragData.dropAction = dropActionType.embed; dragData.dropPropertiesToRemove = ['link_anchor_1_x', 'link_anchor_1_y', 'link_anchor_2_x', 'link_anchor_2_y', 'onClick']; DragManager.StartDocumentDrag([this._ref.current!], dragData, pt[0], pt[1]); return true; diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx index decdbb240..6e4d0e92a 100644 --- a/src/client/views/nodes/LinkBox.tsx +++ b/src/client/views/nodes/LinkBox.tsx @@ -2,7 +2,7 @@ import { action, computed, IReactionDisposer, makeObservable, observable, reacti import { observer } from 'mobx-react'; import * as React from 'react'; import Xarrow from 'react-xarrows'; -import { DocData } from '../../../fields/DocSymbols'; +import { DocCss, DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { DocCast, NumCast, StrCast } from '../../../fields/Types'; import { DashColor, emptyFunction, lightOrDark, returnFalse } from '../../../Utils'; @@ -83,6 +83,8 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { b.Document[b.LayoutFieldKey]; a.Document.layout_scrollTop; b.Document.layout_scrollTop; + a.Document[DocCss]; + b.Document[DocCss]; const axf = a.screenToViewTransform(); // these force re-render when a or b moves (so do NOT remove) const bxf = b.screenToViewTransform(); diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx index c185c66fc..927e6fad4 100644 --- a/src/client/views/nodes/MapBox/MapBox.tsx +++ b/src/client/views/nodes/MapBox/MapBox.tsx @@ -379,7 +379,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem }); const targetCreator = (annotationOn: Doc | undefined) => { - const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, undefined, annotationOn, 'yellow'); + const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn, 'yellow'); FormattedTextBox.SetSelectOnLoad(target); return target; }; @@ -592,7 +592,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem /// this should use SELECTED pushpin for lat/long if there is a selection, otherwise CENTER const anchor = Docs.Create.ConfigDocument({ title: 'MapAnchor:' + this.Document.title, - text: StrCast(this.selectedPinOrRoute?.map) || StrCast(this.Document.map) || 'map location', + text: (StrCast(this.selectedPinOrRoute?.map) || StrCast(this.Document.map) || 'map location') as any, config_latitude: NumCast((existingPin ?? this.selectedPinOrRoute)?.latitude ?? this.dataDoc.latitude), config_longitude: NumCast((existingPin ?? this.selectedPinOrRoute)?.longitude ?? this.dataDoc.longitude), config_map_zoom: NumCast(this.dataDoc.map_zoom), diff --git a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx index 8a5bd7ce6..3eb051dbf 100644 --- a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx +++ b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx @@ -232,7 +232,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps> }); const targetCreator = (annotationOn: Doc | undefined) => { - const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, undefined, annotationOn, 'yellow'); + const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn, 'yellow'); FormattedTextBox.SetSelectOnLoad(target); return target; }; @@ -466,7 +466,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps> /// this should use SELECTED pushpin for lat/long if there is a selection, otherwise CENTER const anchor = Docs.Create.ConfigDocument({ title: 'MapAnchor:' + this.Document.title, - text: StrCast(this.selectedPin?.map) || StrCast(this.Document.map) || 'map location', + text: (StrCast(this.selectedPin?.map) || StrCast(this.Document.map) || 'map location') as any, config_latitude: NumCast((existingPin ?? this.selectedPin)?.latitude ?? this.dataDoc.latitude), config_longitude: NumCast((existingPin ?? this.selectedPin)?.longitude ?? this.dataDoc.longitude), config_map_zoom: NumCast(this.dataDoc.map_zoom), diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx index f6d94ce05..e38a42b29 100644 --- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx @@ -11,7 +11,7 @@ import { Upload } from '../../../../server/SharedMediaTypes'; import { DocumentType } from '../../../documents/DocumentTypes'; import { Docs } from '../../../documents/Documents'; import { DocumentManager } from '../../../util/DocumentManager'; -import { DragManager } from '../../../util/DragManager'; +import { DragManager, dropActionType } from '../../../util/DragManager'; import { ScriptingGlobals } from '../../../util/ScriptingGlobals'; import { Presentation } from '../../../util/TrackMovements'; import { undoBatch } from '../../../util/UndoManager'; @@ -251,7 +251,7 @@ ScriptingGlobals.add(function resumeWorkspaceReplaying(value: Doc, _readOnly_: b ScriptingGlobals.add(function startRecordingDrag(value: { doc: Doc | string; e: React.PointerEvent }) { if (DocCast(value.doc)) { - DragManager.StartDocumentDrag([value.e.target as HTMLElement], new DragManager.DocumentDragData([DocCast(value.doc)], 'embed'), value.e.clientX, value.e.clientY); + DragManager.StartDocumentDrag([value.e.target as HTMLElement], new DragManager.DocumentDragData([DocCast(value.doc)], dropActionType.embed), value.e.clientX, value.e.clientY); value.e.preventDefault(); return true; } diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx index 89650889d..d9d0dbe3e 100644 --- a/src/client/views/nodes/ScriptingBox.tsx +++ b/src/client/views/nodes/ScriptingBox.tsx @@ -130,7 +130,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() } }) ); - observer.observe(document.getElementsByClassName('scriptingBox')[0]); + observer.observe(document.getElementsByClassName('scriptingBox-outerDiv')[0]); } @action @@ -811,22 +811,20 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() render() { TraceMobx(); return ( - <div className={`scriptingBox`} onContextMenu={this.specificContextMenu} onPointerUp={!this._function ? this.suggestionPos : undefined}> - <div className="scriptingBox-outerDiv" onWheel={e => this._props.isSelected() && e.stopPropagation()}> - {this._paramSuggestion ? ( - <div className="boxed" ref={this._suggestionRef} style={{ left: this._suggestionBoxX + 20, top: this._suggestionBoxY - 15, display: 'inline' }}> - {' '} - {this._scriptSuggestedParams}{' '} - </div> - ) : null} - {!this._applied && !this._function ? this.renderScriptingInputs : null} - {this._applied && !this._function ? this.renderParamsInputs() : null} - {!this._applied && this._function ? this.renderFunctionInputs() : null} - - {!this._applied && !this._function ? this.renderScriptingTools() : null} - {this._applied && !this._function ? this.renderTools('Run', () => this.onRun()) : null} - {!this._applied && this._function ? this.renderTools('Create Function', () => this.onCreate()) : null} - </div> + <div className="scriptingBox-outerDiv" onContextMenu={this.specificContextMenu} onPointerUp={!this._function ? this.suggestionPos : undefined} onWheel={e => this._props.isSelected() && e.stopPropagation()}> + {this._paramSuggestion ? ( + <div className="boxed" ref={this._suggestionRef} style={{ left: this._suggestionBoxX + 20, top: this._suggestionBoxY - 15, display: 'inline' }}> + {' '} + {this._scriptSuggestedParams}{' '} + </div> + ) : null} + {!this._applied && !this._function ? this.renderScriptingInputs : null} + {this._applied && !this._function ? this.renderParamsInputs() : null} + {!this._applied && this._function ? this.renderFunctionInputs() : null} + + {!this._applied && !this._function ? this.renderScriptingTools() : null} + {this._applied && !this._function ? this.renderTools('Run', () => this.onRun()) : null} + {!this._applied && this._function ? this.renderTools('Create Function', () => this.onCreate()) : null} </div> ); } diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 40647feff..b2ae7201c 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -31,6 +31,7 @@ import { FocusViewOptions, FieldView, FieldViewProps } from './FieldView'; import { RecordingBox } from './RecordingBox'; import { PinProps, PresBox } from './trails'; import './VideoBox.scss'; +import { dropActionType } from '../../util/DragManager'; /** * VideoBox @@ -335,7 +336,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl this._props.addDocument?.(imageSnapshot); const link = DocUtils.MakeLink(imageSnapshot, this.getAnchor(true), { link_relationship: 'video snapshot' }); link && (DocCast(link.link_anchor_2)[DocData].timecodeToHide = NumCast(DocCast(link.link_anchor_2).timecodeToShow) + 3); - setTimeout(() => downX !== undefined && downY !== undefined && DocumentManager.Instance.getFirstDocumentView(imageSnapshot)?.startDragging(downX, downY, 'move', true)); + setTimeout(() => downX !== undefined && downY !== undefined && DocumentManager.Instance.getFirstDocumentView(imageSnapshot)?.startDragging(downX, downY, dropActionType.move, true)); }; getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 2c5398e40..c9340edc0 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -14,7 +14,7 @@ import { listSpec } from '../../../fields/Schema'; import { Cast, NumCast, StrCast, WebCast } from '../../../fields/Types'; import { ImageField, WebField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; -import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, getWordAtPoint, lightOrDark, returnFalse, returnOne, returnZero, setupMoveUpEvents, smoothScroll, stringHash, Utils } from '../../../Utils'; +import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, DivHeight, emptyFunction, getWordAtPoint, lightOrDark, returnFalse, returnOne, returnZero, setupMoveUpEvents, smoothScroll, stringHash, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentManager } from '../../util/DocumentManager'; import { ScriptingGlobals } from '../../util/ScriptingGlobals'; @@ -83,7 +83,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem return this.webField?.toString() || ''; } @computed get _urlHash() { - return ""+ (stringHash(this._url)??''); + return '' + (stringHash(this._url) ?? ''); } @computed get scrollHeight() { return Math.max(NumCast(this.layoutDoc._height), this._scrollHeight); @@ -782,7 +782,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem className="webBox-htmlSpan" ref={action((r: any) => { if (r) { - this._scrollHeight = Number(getComputedStyle(r).height.replace('px', '')); + this._scrollHeight = DivHeight(r); this.lighttext = Array.from(r.children).some((c: any) => c instanceof HTMLElement && lightOrDark(getComputedStyle(c).color) !== Colors.WHITE); } })} diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx index b49e7dcf0..5c4d850ad 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.tsx +++ b/src/client/views/nodes/formattedText/DashFieldView.tsx @@ -4,24 +4,23 @@ import { action, computed, IReactionDisposer, makeObservable, observable, reacti import { observer } from 'mobx-react'; import * as React from 'react'; import * as ReactDOM from 'react-dom/client'; -import Select from 'react-select'; import { Doc, DocListCast, Field } from '../../../../fields/Doc'; import { List } from '../../../../fields/List'; import { listSpec } from '../../../../fields/Schema'; import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField'; -import { Cast, StrCast } from '../../../../fields/Types'; +import { Cast } from '../../../../fields/Types'; import { emptyFunction, returnFalse, returnZero, setupMoveUpEvents } from '../../../../Utils'; import { DocServer } from '../../../DocServer'; import { CollectionViewType } from '../../../documents/DocumentTypes'; import { Transform } from '../../../util/Transform'; -import { undoable, undoBatch } from '../../../util/UndoManager'; +import { undoBatch } from '../../../util/UndoManager'; import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu'; import { SchemaTableCell } from '../../collections/collectionSchema/SchemaTableCell'; +import { FilterPanel } from '../../FilterPanel'; import { ObservableReactComponent } from '../../ObservableReactComponent'; import { OpenWhere } from '../DocumentView'; import './DashFieldView.scss'; import { FormattedTextBox } from './FormattedTextBox'; -import { FilterPanel } from '../../FilterPanel'; export class DashFieldView { dom: HTMLDivElement; // container for label and value @@ -29,7 +28,7 @@ export class DashFieldView { node: any; tbox: FormattedTextBox; - unclickable = () => !this.tbox._props.isSelected() && this.node.marks.some((m: any) => m.type === this.tbox.EditorView?.state.schema.marks.linkAnchor && m.attrs.noPreview); + unclickable = () => !this.tbox._props.rootSelected?.() && this.node.marks.some((m: any) => m.type === this.tbox.EditorView?.state.schema.marks.linkAnchor && m.attrs.noPreview); constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) { this.node = node; this.tbox = tbox; @@ -108,12 +107,12 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi super(props); makeObservable(this); this._fieldKey = this._props.fieldKey; - this._textBoxDoc = this._props.tbox.Document; + this._textBoxDoc = this._fieldKey.startsWith('_') ? this._props.tbox.Document : this._props.tbox.dataDoc; if (this._props.docId) { DocServer.GetRefField(this._props.docId).then(action(dashDoc => dashDoc instanceof Doc && (this._dashDoc = dashDoc))); } else { - this._dashDoc = this._props.tbox.Document; + this._dashDoc = this._fieldKey.startsWith('_') ? this._props.tbox.Document : this._props.tbox.dataDoc; } } @@ -202,7 +201,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi style={{ width: this._props.width, height: this._props.height, - pointerEvents: this._props.tbox._props.isSelected() || this._props.tbox.isAnyChildContentActive?.() ? undefined : 'none', + pointerEvents: this._props.tbox._props.rootSelected?.() || this._props.tbox.isAnyChildContentActive?.() ? undefined : 'none', }}> {this._props.hideKey ? null : ( <span className="dashFieldView-labelSpan" title="click to see related tags" onPointerDown={this.onPointerDownLabelSpan}> diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 4250b018e..4fce72cdc 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -23,14 +23,14 @@ import { RichTextField } from '../../../../fields/RichTextField'; import { ComputedField } from '../../../../fields/ScriptField'; import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; import { GetEffectiveAcl, TraceMobx } from '../../../../fields/util'; -import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils'; +import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, DivWidth, emptyFunction, numberRange, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils'; import { gptAPICall, GPTCallType } from '../../../apis/gpt/GPT'; import { DocServer } from '../../../DocServer'; import { Docs, DocUtils } from '../../../documents/Documents'; import { CollectionViewType } from '../../../documents/DocumentTypes'; import { DictationManager } from '../../../util/DictationManager'; import { DocumentManager } from '../../../util/DocumentManager'; -import { DragManager } from '../../../util/DragManager'; +import { DragManager, dropActionType } from '../../../util/DragManager'; import { MakeTemplate } from '../../../util/DropConverter'; import { LinkManager } from '../../../util/LinkManager'; import { RTFMarkup } from '../../../util/RTFMarkup'; @@ -67,7 +67,6 @@ import { RichTextMenu, RichTextMenuPlugin } from './RichTextMenu'; import { RichTextRules } from './RichTextRules'; import { schema } from './schema_rts'; import { SummaryView } from './SummaryView'; -import Select from 'react-select'; // import * as applyDevTools from 'prosemirror-dev-tools'; @observer export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface { @@ -100,7 +99,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps private _dropDisposer?: DragManager.DragDropDisposer; private _recordingStart: number = 0; private _ignoreScroll = false; - private _lastText = ''; private _hadDownFocus = false; private _focusSpeed: Opt<number>; private _keymap: any = undefined; @@ -306,7 +304,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps e.preventDefault(); e.stopPropagation(); const targetCreator = (annotationOn?: Doc) => { - const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, undefined, annotationOn); + const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn); FormattedTextBox.SetSelectOnLoad(target); return target; }; @@ -374,9 +372,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps // if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended) if (force || ((this._finishingLink || this._props.isContentActive() || this._inDrop) && removeSelection(newJson) !== removeSelection(prevData?.Data))) { const numstring = NumCast(dataDoc[this.fieldKey], null); - dataDoc[this.fieldKey] = numstring !== undefined ? Number(newText) : new RichTextField(newJson, newText); - dataDoc[this.fieldKey + '_noTemplate'] = true; // mark the data field as being split from the template if it has been edited + dataDoc[this.fieldKey] = numstring !== undefined ? Number(newText) : newText ? new RichTextField(newJson, newText) : undefined; textChange && ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.Document, text: newText }); + this._applyingChange = ''; // turning this off here allows a Doc to retrieve data from template if noTemplate below is changed to false + dataDoc[this.fieldKey + '_noTemplate'] = newText ? true : false; // mark the data field as being split from the template if it has been edited unchanged = false; } } else { @@ -623,7 +622,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps docId: draggedDoc[Id], float: 'unset', }); - if (!['embed', 'copy'].includes((dropAction ?? '') as any)) { + if (![dropActionType.embed, dropActionType.copy].includes(dropAction ?? dropActionType.move)) { added = dragData.removeDocument?.(draggedDoc) ? true : false; } else { added = true; @@ -747,7 +746,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const hr = Math.round(Date.now() / 1000 / 60 / 60); numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-hr-' + (hr - i), { opacity: ((10 - i - 1) / 10).toString() })); } - this.layoutDoc[DocCss] = this.layoutDoc[DocCss] + 1; // css changes happen outside of react/mobx. so we need to set a flag that will notify anyone intereted in layout changes triggered by css changes (eg., CollectionLinkView) + this.layoutDoc[DocCss] = this.layoutDoc[DocCss] + 1; // css changes happen outside of react/mobx. so we need to set a flag that will notify anyone interested in layout changes triggered by css changes (eg., CollectionLinkView) }; @observable _showSidebar = false; @@ -758,7 +757,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps @action toggleSidebar = (preview: boolean = false) => { const defaultSidebar = 250; - const prevWidth = 1 - this.sidebarWidth() / Number(getComputedStyle(this._ref.current!).width.replace('px', '')); + const prevWidth = 1 - this.sidebarWidth() / DivWidth(this._ref.current!); if (preview) this._showSidebar = true; else { this.layoutDoc[this.SidebarKey + '_freeform_scale_max'] = 1; @@ -948,9 +947,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps description: 'Make Default Layout', event: () => { if (!this.layoutDoc.isTemplateDoc) { - const title = StrCast(this.Document.title); - this.Document.title = 'text'; - MakeTemplate(this.Document, true, title); + MakeTemplate(this.Document); } Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.Document); Doc.AddDocToList(Cast(Doc.UserDoc().template_notes, Doc, null), 'data', this.Document); @@ -1201,7 +1198,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps this._cachedLinks = LinkManager.Links(this.Document); this._disposers.breakupDictation = reaction(() => Doc.RecordingEvent, this.breakupDictation); this._disposers.layout_autoHeight = reaction( - () => ({ autoHeight: this.layout_autoHeight, fontSize: this.fontSize }), + () => ({ autoHeight: this.layout_autoHeight, fontSize: this.fontSize, css: this.Document[DocCss] }), (autoHeight, fontSize) => setTimeout(() => autoHeight && this.tryUpdateScrollHeight()) ); this._disposers.highlights = reaction( @@ -1467,39 +1464,37 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } const selectOnLoad = Doc.AreProtosEqual(this._props.TemplateDataDocument ?? this.Document, FormattedTextBox.SelectOnLoad) && (!LightboxView.LightboxDoc || LightboxView.Contains(this.DocumentView?.())); - if (this._editorView && selectOnLoad && !this._props.dontRegisterView && !this._props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) { - const selLoadChar = FormattedTextBox.SelectOnLoadChar; + const selLoadChar = FormattedTextBox.SelectOnLoadChar; + if (selectOnLoad) { FormattedTextBox.SelectOnLoad = undefined; + FormattedTextBox.SelectOnLoadChar = ''; + } + if (this._editorView && selectOnLoad && !this._props.dontRegisterView && !this._props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) { this._props.select(false); if (selLoadChar) { const $from = this._editorView.state.selection.anchor ? this._editorView.state.doc.resolve(this._editorView.state.selection.anchor - 1) : undefined; const mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) }); const curMarks = this._editorView.state.storedMarks ?? $from?.marksAcross(this._editorView.state.selection.$head) ?? []; const storedMarks = [...curMarks.filter(m => m.type !== mark.type), mark]; - const tr = this._editorView.state.tr - .setStoredMarks(storedMarks) - .insertText(FormattedTextBox.SelectOnLoadChar, this._editorView.state.doc.content.size - 1, this._editorView.state.doc.content.size) - .setStoredMarks(storedMarks); + const tr1 = this._editorView.state.tr.setStoredMarks(storedMarks); + const tr2 = selLoadChar === 'Enter' ? tr1.insert(this._editorView.state.doc.content.size - 1, schema.nodes.paragraph.create()) : tr1.insertText(selLoadChar, this._editorView.state.doc.content.size - 1); + const tr = tr2.setStoredMarks(storedMarks); + this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size)))); } else if (curText && !FormattedTextBox.DontSelectInitialText) { selectAll(this._editorView.state, this._editorView?.dispatch); } } - selectOnLoad && this._editorView!.focus(); + if (selectOnLoad) { + FormattedTextBox.DontSelectInitialText = false; + this._editorView!.focus(); + } if (this._props.isContentActive()) this.prepareForTyping(); - if (this._editorView) { - const tr = this._editorView.state.tr; - const { from, to } = tr.selection; - // for some reason, the selection is sometimes lost in the sidebar view when prosemirror syncs the seledtion with the dom, so reset the selection after the document has ben fully instantiated. - if (FormattedTextBox.DontSelectInitialText) setTimeout(() => this._editorView?.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(from), tr.doc.resolve(to)))), 250); - - if (FormattedTextBox.PasteOnLoad) { - const pdfAnchorId = FormattedTextBox.PasteOnLoad.clipboardData?.getData('dash/pdfAnchor'); - FormattedTextBox.PasteOnLoad = undefined; - pdfAnchorId && this.addPdfReference(pdfAnchorId); - } + if (this._editorView && FormattedTextBox.PasteOnLoad) { + const pdfAnchorId = FormattedTextBox.PasteOnLoad.clipboardData?.getData('dash/pdfAnchor'); + FormattedTextBox.PasteOnLoad = undefined; + pdfAnchorId && this.addPdfReference(pdfAnchorId); } - FormattedTextBox.DontSelectInitialText = false; } // add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet. @@ -1588,7 +1583,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } if (!state || !editor || !this.ProseRef?.children[0].className.includes('-focused')) return; if (!state.selection.empty && !(state.selection instanceof NodeSelection)) this.setupAnchorMenu(); - else if (this._props.isContentActive()) { + else if (this._props.isContentActive() && !e.button) { const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY }); let xpos = pcords?.pos || 0; while (xpos > 0 && !state.doc.resolve(xpos).node()?.isTextblock) { @@ -2031,7 +2026,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } render() { TraceMobx(); - const scale = (this._props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._freeform_scale, 1); + const scale = this._props.NativeDimScaling?.() || 1; // * NumCast(this.layoutDoc._freeform_scale, 1); const rounded = StrCast(this.layoutDoc.layout_borderRounding) === '100%' ? '-rounded' : ''; setTimeout(() => !this._props.isContentActive() && FormattedTextBoxComment.textBox === this && FormattedTextBoxComment.Hide); const paddingX = NumCast(this.layoutDoc._xMargin, this._props.xPadding || 0); @@ -2091,7 +2086,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps onScroll={this.onScroll} onDrop={this.ondrop}> <div - className={`formattedTextBox-inner${rounded} ${this.layoutDoc.layout_centered ? 'centered' : ''}`} + className={`formattedTextBox-inner${rounded} ${this.layoutDoc._layout_centered ? 'centered' : ''}`} ref={this.createDropTarget} style={{ padding: StrCast(this.layoutDoc._textBoxPadding), diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index cd0cdaa74..dc2c06701 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -104,7 +104,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { return this._activeAlignment; } @computed get textVcenter() { - return BoolCast(this.layoutDoc?.layout_centered); + return BoolCast(this.layoutDoc?._layout_centered); } _disposer: IReactionDisposer | undefined; componentDidMount() { @@ -450,7 +450,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { } vcenterToggle = (view: EditorView, dispatch: any) => { - this.layoutDoc && (this.layoutDoc.layout_centered = !this.layoutDoc.layout_centered); + this.layoutDoc && (this.layoutDoc._layout_centered = !this.layoutDoc._layout_centered); }; align = (view: EditorView, dispatch: any, alignment: 'left' | 'right' | 'center') => { if (this.TextView?._props.rootSelected?.()) { diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index 9bd41f42c..d5c91fc09 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -78,7 +78,7 @@ export class RichTextRules { this.TextBox.dataDoc[paintedField] = CollectionView.LayoutString(this.TextBox.fieldKey); const layoutFieldKey = StrCast(this.TextBox.layoutDoc.layout_fieldKey); // save the current layout fieldkey this.TextBox.layoutDoc.layout_fieldKey = paintedField; // setup the paint layout field key - this.TextBox.DocumentView?.().setToggleDetail(layoutFieldKey.replace('layout_', '').replace('layout', ''), 'onPaint'); // create the script to toggle between the painted and regular view + this.TextBox.DocumentView?.().setToggleDetail('onPaint'); // create the script to toggle between the painted and regular view this.TextBox.layoutDoc.layout_fieldKey = layoutFieldKey; // restore the layout field key to text return state.tr.delete(start, end).setBlockType(start, start, schema.nodes.code_block); diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index b8f6575dd..e34144fae 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -36,6 +36,7 @@ import { FocusViewOptions, FieldView, FieldViewProps } from '../FieldView'; import { ScriptingBox } from '../ScriptingBox'; import './PresBox.scss'; import { PresEffect, PresEffectDirection, PresMovement, PresStatus } from './PresEnums'; +import { dropActionType } from '../../../util/DragManager'; export interface pinDataTypes { scrollable?: boolean; dataviz?: number[]; @@ -737,7 +738,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const targetDoc: Doc = this.targetDoc; const finished = () => { afterNav?.(); - console.log('Finish Slide Nav: ' + targetDoc.title); targetDoc[Animation] = undefined; }; const selViewCache = Array.from(this.selectedArray); @@ -1074,7 +1074,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }; childLayoutTemplate = () => Docs.Create.PresElementBoxDocument(); - removeDocument = (doc: Doc) => Doc.RemoveDocFromList(this.Document, this.fieldKey, doc); + removeDocument = (doc: Doc | Doc[]) => !(doc instanceof Doc ? [doc] : doc).map(d => Doc.RemoveDocFromList(this.Document, this.fieldKey, d)).some(p => !p); getTransform = () => this.ScreenToLocalBoxXf().translate(-5, -65); // listBox padding-left and pres-box-cont minHeight panelHeight = () => this._props.PanelHeight() - 40; /** @@ -2222,10 +2222,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { // prettier-ignore switch (layout) { 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, _layout_fitContentsToBox: true, x, y }); - case 'header': return Docs.Create.FreeformDocument([header()], { title: input ? input : 'Section header', _width: 400, _height: 225, _layout_fitContentsToBox: true, x, y }); - case 'content': return Docs.Create.FreeformDocument([contentTitle(), content()], { title: input ? input : 'Title and content', _width: 400, _height: 225, _layout_fitContentsToBox: true, x, y }); - case 'twoColumns': return Docs.Create.FreeformDocument([contentTitle(), content1(), content2()], { title: input ? input : 'Title and two columns', _width: 400, _height: 225, _layout_fitContentsToBox: true, x, y }) + case 'title': return Docs.Create.FreeformDocument([title(), subtitle()], { title: input ? input : 'Title slide', _width: 400, _height: 225, _freeform_fitContentsToBox: true, x, y }); + case 'header': return Docs.Create.FreeformDocument([header()], { title: input ? input : 'Section header', _width: 400, _height: 225, _freeform_fitContentsToBox: true, x, y }); + case 'content': return Docs.Create.FreeformDocument([contentTitle(), content()], { title: input ? input : 'Title and content', _width: 400, _height: 225, _freeform_fitContentsToBox: true, x, y }); + case 'twoColumns': return Docs.Create.FreeformDocument([contentTitle(), content1(), content2()], { title: input ? input : 'Title and two columns', _width: 400, _height: 225, _freeform_fitContentsToBox: true, x, y }) } }; @@ -2607,7 +2607,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { childIgnoreNativeSize={true} moveDocument={returnFalse} ignoreUnrendered={true} - childDragAction="move" + childDragAction={dropActionType.move} setContentViewBox={emptyFunction} //childLayoutFitWidth={returnTrue} childOpacity={returnOne} diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index 0b664beaa..9f153e86d 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -88,9 +88,11 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { * (Note: There is no longer a need to press enter to submit a search. Any update to the input * causes a search to be submitted automatically.) */ + _timeout: any = undefined; onInputChange = action((e: React.ChangeEvent<HTMLInputElement>) => { this._searchString = e.target.value; - this.submitSearch(); + this._timeout && clearTimeout(this._timeout); + this._timeout = setTimeout(() => this.submitSearch(), 300); }); /** @@ -334,6 +336,8 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { * brushes and highlights. All search matches are cleared as well. */ resetSearch = action(() => { + this._timeout && clearTimeout(this._timeout); + this._timeout = undefined; this._results.forEach((_, doc) => { DocumentManager.Instance.getFirstDocumentView(doc)?.ComponentView?.search?.('', undefined, true); Doc.UnBrushDoc(doc); @@ -436,10 +440,10 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { </select> )} <input - defaultValue={''} + defaultValue="" autoComplete="off" onChange={this.onInputChange} - onKeyPress={e => { + onKeyDown={e => { e.key === 'Enter' ? this.submitSearch() : null; e.stopPropagation(); }} diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx index 4155800b8..addad2bbc 100644 --- a/src/client/views/topbar/TopBar.tsx +++ b/src/client/views/topbar/TopBar.tsx @@ -1,6 +1,6 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Button, IconButton, isDark, Size, Type } from 'browndash-components'; -import { action, computed, observable, reaction } from 'mobx'; +import { action, computed, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { FaBug } from 'react-icons/fa'; @@ -25,13 +25,24 @@ import { Colors } from '../global/globalEnums'; import { DocumentViewInternal, returnEmptyDocViewList } from '../nodes/DocumentView'; import { DefaultStyleProvider } from '../StyleProvider'; import './TopBar.scss'; +import { dropActionType } from '../../util/DragManager'; +import { Flip } from 'react-awesome-reveal'; +import { ObservableReactComponent } from '../ObservableReactComponent'; /** * ABOUT: This is the topbar in Dash, which included the current Dashboard as well as access to information on the user * and settings and help buttons. Future scope for this bar is to include the collaborators that are on the same Dashboard. */ @observer -export class TopBar extends React.Component { +export class TopBar extends ObservableReactComponent<{}> { + static Instance: TopBar; + @observable private _flipDocumentation = 0; + constructor(props: any) { + super(props); + makeObservable(this); + TopBar.Instance = this; + } + navigateToHome = () => { (CollectionDockingView.Instance?.CaptureThumbnail() ?? new Promise<void>(res => res())).then(() => { Doc.ActivePage = 'home'; @@ -102,7 +113,7 @@ export class TopBar extends React.Component { Document={selDoc} docViewPath={returnEmptyDocViewList} fieldKey="data" - dropAction="embed" + dropAction={dropActionType.embed} styleProvider={DefaultStyleProvider} select={emptyFunction} isContentActive={returnTrue} @@ -161,7 +172,6 @@ export class TopBar extends React.Component { </div> ) : null; } - /** * Returns the right hand side of the topbar. * This part of the topbar includes information about the current user, @@ -183,7 +193,9 @@ export class TopBar extends React.Component { /> ) : null} <IconButton tooltip={'Issue Reporter ⌘I'} size={Size.SMALL} color={this.color} onClick={ReportManager.Instance.open} icon={<FaBug />} /> - <IconButton tooltip={'Documentation ⌘D'} size={Size.SMALL} color={this.color} onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/', '_blank')} icon={<FontAwesomeIcon icon="question-circle" />} /> + <Flip key={this._flipDocumentation}> + <IconButton tooltip={'Documentation ⌘D'} size={Size.SMALL} color={this.color} onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/', '_blank')} icon={<FontAwesomeIcon icon="question-circle" />} /> + </Flip> <IconButton tooltip={'Settings ⌘⇧S'} size={Size.SMALL} color={this.color} onClick={SettingsManager.Instance.open} icon={<FontAwesomeIcon icon="cog" />} /> <IconButton size={Size.SMALL} @@ -198,6 +210,11 @@ export class TopBar extends React.Component { ); } + /** + * Make the documentation icon flip around to draw attention to it. + */ + FlipDocumentationIcon = action(() => (this._flipDocumentation = this._flipDocumentation + 1)); + render() { return ( //TODO:glr Add support for light / dark mode diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 56d50846a..e47bfcbed 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -172,7 +172,7 @@ export class Doc extends RefField { public static get MyTrails() { return DocCast(Doc.ActiveDashboard?.myTrails); } // prettier-ignore public static get MyCalendars() { return DocCast(Doc.ActiveDashboard?.myCalendars); } // prettier-ignore public static get MyOverlayDocs() { return DocListCast(Doc.ActiveDashboard?.myOverlayDocs ?? DocCast(Doc.UserDoc().myOverlayDocs)?.data); } // prettier-ignore - public static get MyPublishedDocs() { return DocListCast(Doc.ActiveDashboard?.myPublishedDocs ?? DocCast(Doc.UserDoc().myPublishedDocs)?.data); } // prettier-ignore + public static get MyPublishedDocs() { return DocListCast(Doc.ActiveDashboard?.myPublishedDocs).concat(DocListCast(DocCast(Doc.UserDoc().myPublishedDocs)?.data)); } // prettier-ignore public static get MyDashboards() { return DocCast(Doc.UserDoc().myDashboards); } // prettier-ignore public static get MyTemplates() { return DocCast(Doc.UserDoc().myTemplates); } // prettier-ignore public static get MyImports() { return DocCast(Doc.UserDoc().myImports); } // prettier-ignore @@ -391,7 +391,10 @@ export class Doc extends RefField { export namespace Doc { export function SetContainer(doc: Doc, container: Doc) { - container !== Doc.MyRecentlyClosed && (doc.embedContainer = container); + if (container !== Doc.MyRecentlyClosed) { + doc.embedContainer = container; + Doc.AddEmbedding(doc, doc); + } } export function RunCachedUpdate(doc: Doc, field: string) { const update = doc[CachedUpdates][field]; @@ -580,6 +583,9 @@ export namespace Doc { Doc.RemoveDocFromList(doc[DocData], 'proto_embeddings', embedding); } export function AddEmbedding(doc: Doc, embedding: Doc) { + if (embedding === null) { + console.log('WHAT?'); + } Doc.AddDocToList(doc[DocData], 'proto_embeddings', embedding, undefined, undefined, undefined, undefined, undefined, true); } export function GetEmbeddings(doc: Doc) { @@ -604,15 +610,26 @@ export namespace Doc { const dataDoc = doc[DocData]; const availableEmbeddings = Doc.GetEmbeddings(dataDoc); const bestEmbedding = [...(dataDoc !== doc ? [doc] : []), ...availableEmbeddings].find(doc => !doc.embedContainer && doc.author === Doc.CurrentUserEmail); - bestEmbedding && Doc.AddDocToList(dataDoc, 'protoEmbeddings', doc, undefined, undefined, undefined, undefined, undefined, true); + bestEmbedding && Doc.AddEmbedding(doc, doc); return bestEmbedding ?? Doc.MakeEmbedding(doc); } // this lists out all the tag ids that can be in a RichTextField that might contain document ids. // if a document is cloned, we need to make sure to clone all of these referenced documents as well; export const DocsInTextFieldIds = ['audioId', 'textId', 'anchorId', 'docId']; - export async function makeClone(doc: Doc, cloneMap: Map<string, Doc>, linkMap: Map<string, Doc>, rtfs: { copy: Doc; key: string; field: RichTextField }[], exclusions: string[], pruneDocs: Doc[], cloneLinks: boolean): Promise<Doc> { - if (Doc.IsBaseProto(doc)) return doc; + export async function makeClone( + doc: Doc, + cloneMap: Map<string, Doc>, + linkMap: Map<string, Doc>, + rtfs: { copy: Doc; key: string; field: RichTextField }[], + exclusions: string[], + pruneDocs: Doc[], + cloneLinks: boolean, + cloneTemplates: boolean + ): Promise<Doc> { + if (Doc.IsBaseProto(doc) || ((Doc.Get(doc, 'isTemplateDoc', true) || Doc.Get(doc, 'isTemplateForField', true)) && !cloneTemplates)) { + return doc; + } if (cloneMap.get(doc[Id])) return cloneMap.get(doc[Id])!; const copy = new Doc(undefined, true); cloneMap.set(doc[Id], copy); @@ -627,7 +644,7 @@ export namespace Doc { const list = await Cast(doc[key], listSpec(Doc)); const docs = list && (await DocListCastAsync(list))?.filter(d => d instanceof Doc); if (docs !== undefined && docs.length) { - const clones = await Promise.all(docs.map(async d => Doc.makeClone(d, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks))); + const clones = await Promise.all(docs.map(async d => Doc.makeClone(d, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks, cloneTemplates))); assignKey(new List<Doc>(clones)); } else { assignKey(ObjectField.MakeCopy(field)); @@ -642,7 +659,7 @@ export namespace Doc { ); const results = docids && (await DocServer.GetRefFields(docids)); const docs = results && Array.from(Object.keys(results)).map(key => DocCast(results[key])); - docs?.map(doc => doc && Doc.makeClone(doc, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks)); + docs?.map(doc => doc && Doc.makeClone(doc, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks, cloneTemplates)); rtfs.push({ copy, key, field }); } } @@ -654,8 +671,14 @@ export namespace Doc { } else if (docAtKey instanceof Doc) { if (pruneDocs.includes(docAtKey)) { // prune doc and do nothing - } else if (!Doc.IsSystem(docAtKey) && (key.startsWith('layout') || ['embedContainer', 'annotationOn', 'proto'].includes(key) || ((key === 'link_anchor_1' || key === 'link_anchor_2') && doc.author === Doc.CurrentUserEmail))) { - assignKey(await Doc.makeClone(docAtKey, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks)); + } else if ( + !Doc.IsSystem(docAtKey) && + (key.includes('layout[') || + key.startsWith('layout') || // + ['embedContainer', 'annotationOn', 'proto'].includes(key) || + (['link_anchor_1', 'link_anchor_2'].includes(key) && doc.author === Doc.CurrentUserEmail)) + ) { + assignKey(await Doc.makeClone(docAtKey, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks, cloneTemplates)); } else { assignKey(docAtKey); } @@ -678,16 +701,17 @@ export namespace Doc { ((cloneMap.has(DocCast(link.link_anchor_1)?.[Id]) || cloneMap.has(DocCast(DocCast(link.link_anchor_1)?.annotationOn)?.[Id])) && (cloneMap.has(DocCast(link.link_anchor_2)?.[Id]) || cloneMap.has(DocCast(DocCast(link.link_anchor_2)?.annotationOn)?.[Id]))) ) { - linkMap.set(link[Id], await Doc.makeClone(link, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks)); + linkMap.set(link[Id], await Doc.makeClone(link, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks, cloneTemplates)); } }); - Doc.SetInPlace(copy, 'title', '>:' + doc.title, true); + if (Doc.Get(copy, 'title', true)) copy.title = '>:' + doc.title; + // Doc.SetInPlace(copy, 'title', '>:' + doc.title, true); copy.cloneOf = doc; cloneMap.set(doc[Id], copy); return copy; } - export function repairClone(clone: Doc, cloneMap: Map<string, Doc>, visited: Set<Doc>) { + export function repairClone(clone: Doc, cloneMap: Map<string, Doc>, cloneTemplates: boolean, visited: Set<Doc>) { if (visited.has(clone)) return; visited.add(clone); Object.keys(clone) @@ -696,22 +720,22 @@ export namespace Doc { const docAtKey = DocCast(clone[key]); if (docAtKey && !Doc.IsSystem(docAtKey)) { if (!Array.from(cloneMap.values()).includes(docAtKey)) { - clone[key] = cloneMap.get(docAtKey[Id]); + clone[key] = !cloneTemplates && (Doc.Get(docAtKey, 'isTemplateDoc', true) || Doc.Get(docAtKey, 'isTemplateForField', true)) ? docAtKey : cloneMap.get(docAtKey[Id]); } else { - repairClone(docAtKey, cloneMap, visited); + repairClone(docAtKey, cloneMap, cloneTemplates, visited); } } }); } - export function MakeClones(docs: Doc[], cloneLinks: boolean) { + export function MakeClones(docs: Doc[], cloneLinks: boolean, cloneTemplates: boolean) { const cloneMap = new Map<string, Doc>(); - return docs.map(doc => Doc.MakeClone(doc, cloneLinks, cloneMap)); + return docs.map(doc => Doc.MakeClone(doc, cloneLinks, cloneTemplates, cloneMap)); } - export async function MakeClone(doc: Doc, cloneLinks = true, cloneMap: Map<string, Doc> = new Map()) { + export async function MakeClone(doc: Doc, cloneLinks = true, cloneTemplates = true, cloneMap: Map<string, Doc> = new Map()) { const linkMap = new Map<string, Doc>(); const rtfMap: { copy: Doc; key: string; field: RichTextField }[] = []; - const copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ['cloneOf'], doc.embedContainer ? [DocCast(doc.embedContainer)] : [], cloneLinks); + const copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ['cloneOf'], doc.embedContainer ? [DocCast(doc.embedContainer)] : [], cloneLinks, cloneTemplates); const repaired = new Set<Doc>(); const linkedDocs = Array.from(linkMap.values()); linkedDocs.map((link: Doc) => LinkManager.Instance.addLink(link, true)); @@ -729,7 +753,7 @@ export namespace Doc { copy[key] = new RichTextField(field.Data.replace(docidsearch, replacer).replace(re, replacer2), field.Text); }); const clonedDocs = [...Array.from(cloneMap.values()), ...linkedDocs]; - clonedDocs.map(clone => Doc.repairClone(clone, cloneMap, repaired)); + clonedDocs.map(clone => Doc.repairClone(clone, cloneMap, cloneTemplates, repaired)); return { clone: copy, map: cloneMap, linkMap }; } @@ -950,7 +974,7 @@ export namespace Doc { } else if (field instanceof ObjectField) { copy[key] = doc[key] instanceof Doc && key.includes('layout[') - ? undefined // remove expanded template field documents + ? new ProxyField(Doc.MakeCopy(doc[key] as any)) // copy the expanded render template : ObjectField.MakeCopy(field); } else if (field instanceof Promise) { debugger; //This shouldn't happend... @@ -984,10 +1008,9 @@ export namespace Doc { Object.keys(doc) .filter(key => key.startsWith('acl')) .forEach(key => (delegate[key] = doc[key])); - if (!Doc.IsSystem(doc)) Doc.AddEmbedding(doc, delegate); title && (delegate.title = title); delegate[Initializing] = false; - Doc.AddEmbedding(doc, delegate); + if (!Doc.IsSystem(doc)) Doc.AddEmbedding(doc, delegate); return delegate; } return undefined; @@ -1008,7 +1031,6 @@ export namespace Doc { delegate[Initializing] = true; delegate.proto = delegateProto; delegate.author = Doc.CurrentUserEmail; - Doc.AddEmbedding(delegateProto, delegate); delegate[Initializing] = false; delegateProto[Initializing] = false; return delegate; @@ -1394,7 +1416,7 @@ export namespace Doc { const list = Array.from(Object.values(fieldlist)) .map(d => DocCast(d)) .filter(d => d); - const docs = clone ? (await Promise.all(Doc.MakeClones(list, false))).map(res => res.clone) : list; + const docs = clone ? (await Promise.all(Doc.MakeClones(list, false, false))).map(res => res.clone) : list; if (ptx !== undefined && pty !== undefined && newPoint !== undefined) { const firstx = list.length ? NumCast(list[0].x) + ptx - newPoint[0] : 0; const firsty = list.length ? NumCast(list[0].y) + pty - newPoint[1] : 0; @@ -1432,7 +1454,7 @@ export namespace Doc { case DocumentType.WEBCAM: return 'video'; case DocumentType.AUDIO: return 'microphone'; case DocumentType.BUTTON: return 'bolt'; - case DocumentType.PRES: return 'tv'; + case DocumentType.PRES: return 'route'; case DocumentType.SCRIPTING: return 'terminal'; case DocumentType.IMPORT: return 'cloud-upload-alt'; case DocumentType.VID: return 'video'; diff --git a/src/fields/DocSymbols.ts b/src/fields/DocSymbols.ts index f8a57acd5..64d657e4f 100644 --- a/src/fields/DocSymbols.ts +++ b/src/fields/DocSymbols.ts @@ -28,4 +28,4 @@ export const Initializing = Symbol('DocInitializing'); export const ForceServerWrite = Symbol('DocForceServerWrite'); export const CachedUpdates = Symbol('DocCachedUpdates'); -export const DashVersion = 'v0.7.0'; +export const DashVersion = 'v0.8.0'; diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts index 8eeb52709..1cacfe30c 100644 --- a/src/fields/documentSchemas.ts +++ b/src/fields/documentSchemas.ts @@ -96,9 +96,9 @@ export const documentSchema = createSchema({ // drag drop properties _dragOnlyWithinContainer: 'boolean', // whether document can be dropped into a different collection dragFactory: Doc, // the document that serves as the "template" for the onDragStart script. ie, to drag out copies of the dragFactory document. - dropAction: 'string', // override specifying what should happen when something is dropped on this document (can be "embed", "copy", "move") - dragAction: 'string', // override specifying what should happen when this document s dragged (can be "embed", "copy", "move") - childDragAction: 'string', // specify the override for what should happen when the child of a collection is dragged from it and dropped (can be "embed" or "copy") + dropAction: 'string', // override specifying what should happen when something is dropped on this document (dropActionType) + dragAction: 'string', // override specifying what should happen when this document s dragged (dropActionType) + childDragAction: 'string', // specify the override for what should happen when the child of a collection is dragged from it and dropped (dropActionType) dropPropertiesToRemove: listSpec('string'), // properties that should be removed from the embed/copy/etc of this document when it is dropped }); diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx index c31e73b42..dc4af8303 100644 --- a/src/mobile/MobileInterface.tsx +++ b/src/mobile/MobileInterface.tsx @@ -92,6 +92,7 @@ import { faTrash, faTrashAlt, faTree, + faRoute, faTv, faUndoAlt, faVideo, |