diff options
author | alyssaf16 <alyssa_feinberg@brown.edu> | 2024-09-18 07:27:02 -0400 |
---|---|---|
committer | alyssaf16 <alyssa_feinberg@brown.edu> | 2024-09-18 07:27:02 -0400 |
commit | fa10d872b2a63f5a53461c8fb5473706e78af4e9 (patch) | |
tree | 454827d08505299fdee8bcf17e90b290bd51422d /src | |
parent | b9adb3a47279e4bcb975240f3d37f3454cf0757a (diff) | |
parent | 0268a524540ff36bf007744eff6709dfcdedec96 (diff) |
Merge branch 'alyssa-starter' of https://github.com/brown-dash/Dash-Web into alyssa-starter
Diffstat (limited to 'src')
46 files changed, 749 insertions, 600 deletions
diff --git a/src/ClientUtils.ts b/src/ClientUtils.ts index 55801df81..01eda7e98 100644 --- a/src/ClientUtils.ts +++ b/src/ClientUtils.ts @@ -648,20 +648,14 @@ export function DivWidth(ele: HTMLElement | null): number { } export function dateRangeStrToDates(dateStr: string) { + const toDate = (str: string) => { + return !str.includes('T') && str.includes('-') ? new Date(Number(str.split('-')[0]), Number(str.split('-')[1]) - 1, Number(str.split('-')[2])) : new Date(str); + }; // dateStr in yyyy-mm-dd format const dateRangeParts = dateStr.split('|'); // splits into from and to date - const fromParts = dateRangeParts[0].split('-'); - const toParts = dateRangeParts[1].split('-'); - - const fromYear = parseInt(fromParts[0]); - const fromMonth = parseInt(fromParts[1]) - 1; - const fromDay = parseInt(fromParts[2]); - - const toYear = parseInt(toParts[0]); - const toMonth = parseInt(toParts[1]) - 1; - const toDay = parseInt(toParts[2]); - - return [new Date(fromYear, fromMonth, fromDay), new Date(toYear, toMonth, toDay)]; + if (dateRangeParts.length < 2 && !dateRangeParts[0]) return { start: new Date(), end: new Date() }; + if (dateRangeParts.length < 2) return { start: toDate(dateRangeParts[0]), end: toDate(dateRangeParts[0]) }; + return { start: new Date(dateRangeParts[0]), end: new Date(dateRangeParts[1]) }; } function replaceCanvases(oldDiv: HTMLElement, newDiv: HTMLElement) { diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts index b055546fc..56d505681 100644 --- a/src/client/documents/DocumentTypes.ts +++ b/src/client/documents/DocumentTypes.ts @@ -39,7 +39,6 @@ export enum DocumentType { COMPARISON = 'comparison', PUSHPIN = 'pushpin', MAPROUTE = 'maproute', - CALENDAR = 'calendar', SCRIPTDB = 'scriptdb', // database of scripts GROUPDB = 'groupdb', // database of groups diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index af181b031..da3419de2 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -248,6 +248,7 @@ 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_hideDecorations?: BOOLt = new BoolInfo('whether to suppress all document decortations when selected'); _layout_hideContextMenu?: BOOLt = new BoolInfo('whether the context menu can be shown'); layout_diagramEditor?: STRt = new StrInfo('specify the JSX string for a diagram editor view'); layout_hideContextMenu?: BOOLt = new BoolInfo('whether the context menu can be shown'); @@ -260,6 +261,7 @@ export class DocumentOptions { _layout_noSidebar?: BOOLt = new BoolInfo('whether to display the sidebar toggle button'); 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_dontCenter?: STRt = new StrInfo("whether collections will center their content - values of 'x', 'xy', or 'y'"); _layout_autoHeight?: BOOLt = new BoolInfo('whether document automatically resizes vertically to display contents'); _layout_autoHeightMargins?: NUMt = new NumInfo('Margin heights to be added to the computed auto height of a Doc'); _layout_curPage?: NUMt = new NumInfo('current page of a PDF or other? paginated document', false); @@ -272,6 +274,7 @@ export class DocumentOptions { _layout_showTitle?: string; // field name to display in header (:hover is an optional suffix) _layout_showSidebar?: BOOLt = new BoolInfo('whether an annotationsidebar should be displayed for text docuemnts'); _layout_showCaption?: string; // which field to display in the caption area. leave empty to have no caption + _layout_showTags?: BOOLt = new BoolInfo('whether to show the list of document tags at the bottom of a DocView'); _chromeHidden?: BOOLt = new BoolInfo('whether the editing chrome for a document is hidden'); hideClickBehaviors?: BOOLt = new BoolInfo('whether to hide click behaviors in context menu'); @@ -450,6 +453,7 @@ export class DocumentOptions { onDragStart?: ScriptField; // script to execute at start of drag operation -- e.g., when a "creator" button is dragged this script generates a different document to drop target?: Doc; // available for use in scripts. used to provide a document parameter to the script (Note, this is a convenience entry since any field could be used for parameterizing a script) tags?: LISTt = new ListInfo('hashtags added to document, typically using a text view', true); + tags_chat?: LISTt = new ListInfo('hashtags added to document by chatGPT', true); treeView_HideTitle?: BOOLt = new BoolInfo('whether to hide the top document title of a tree view'); treeView_HideUnrendered?: BOOLt = new BoolInfo("tells tree view not to display documents that have an 'layout_unrendered' tag unless they also have a treeView_FieldKey tag (presBox)"); treeView_HideHeaderIfTemplate?: BOOLt = new BoolInfo('whether to hide the header for a document in a tree view only if a childLayoutTemplate is provided (presBox)'); @@ -912,7 +916,15 @@ export namespace Docs { } export function CalendarDocument(options: DocumentOptions, documents: Array<Doc>) { - return InstanceFromProto(Prototypes.get(DocumentType.CALENDAR), new List(documents), { ...options }); + const inst = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { + _layout_nativeDimEditable: true, + _layout_reflowHorizontal: true, + _layout_reflowVertical: true, + ...options, + _type_collection: CollectionViewType.Calendar, + }); + documents.forEach(d => Doc.SetContainer(d, inst)); + return inst; } // shouldn't ever need to create a KVP document-- instead set the LayoutTemplateString to be a KeyValueBox for the DocumentView (see addDocTab in TabDocView) @@ -969,12 +981,8 @@ export namespace Docs { return doc; } - export function CalendarCollectionDocument(documents: Array<Doc>, options: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Calendar }); - } - export function StackingDocument(documents: Array<Doc>, options: DocumentOptions, id?: string, protoId?: string) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Stacking }, id, undefined, protoId); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _layout_dontCenter: 'y', ...options, _type_collection: CollectionViewType.Stacking }, id, undefined, protoId); } export function NoteTakingDocument(documents: Array<Doc>, options: DocumentOptions, id?: string, protoId?: string) { diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 14fb65252..2014c48e1 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -368,7 +368,7 @@ pie title Minerals in my tap water {key: "Chat", creator: Docs.Create.ChatDocument, opts: { _width: 300, _height: 300, }}, {key: "Header", creator: headerTemplate, opts: { _width: 300, _height: 120, _header_pointerEvents: "all", _header_height: 50, _header_fontSize: 9,_layout_autoHeightMargins: 50, _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: dropActionType.embed, 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, _layout_dontCenter:'xy', 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, @@ -477,6 +477,7 @@ pie title Minerals in my tap water const reqdStackOpts:DocumentOptions ={ title: "menuItemPanel", childDragAction: dropActionType.same, layout_boxShadow: "rgba(0,0,0,0)", dontRegisterView: true, ignoreClick: true, + _layout_dontCenter: 'y', _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)" }); @@ -510,7 +511,7 @@ pie title Minerals in my tap water // doc.myUserBtns = new PrefetchProxy(userBtns); 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_explainer: "This is a palette of documents that can be created.", _layout_dontCenter: "y", _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]); @@ -663,7 +664,8 @@ pie title Minerals in my tap water } static stackTools(): Button[] { return [ - { title: "Center", icon: "align-center", toolTip: "Center Align Stack", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"center", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform + { title: "V. Align", icon: "pallet", toolTip: "Center Align Stack", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"vcenter", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform + { title: "Center", icon: "align-center", toolTip: "Center Align Stack", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"hcenter", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform ] } static cardTools(): Button[] { @@ -777,8 +779,8 @@ pie title Minerals in my tap water } static imageTools() { return [ - { title: "Pixels",toolTip: "Set Native Pixel Sizze", btnType: ButtonType.ClickButton, icon: "portrait", scripts: { onClick: 'imageSetPixelSize();' }}, - { title: "Rotate",toolTip: "Rotate 90", btnType: ButtonType.ClickButton, icon: "redo-alt", scripts: { onClick: 'imageRotate90();' }}, + { title: "Pixels",toolTip: "Set Native Pixel Size", btnType: ButtonType.ClickButton, icon: "portrait", scripts: { onClick: 'imageSetPixelSize();' }}, + { title: "Rotate",toolTip: "Rotate 90", btnType: ButtonType.ClickButton, icon: "redo-alt", scripts: { onClick: 'imageRotate90();' }}, ]; } static contextMenuTools():Button[] { @@ -787,7 +789,7 @@ pie title Minerals in my tap water CollectionViewType.Stacking, CollectionViewType.Masonry, CollectionViewType.Multicolumn, CollectionViewType.Multirow, CollectionViewType.Time, CollectionViewType.Carousel, CollectionViewType.Carousel3D, CollectionViewType.Card, CollectionViewType.Linear, CollectionViewType.Map, - CollectionViewType.Grid, CollectionViewType.NoteTaking, ]), + CollectionViewType.Calendar, CollectionViewType.Grid, CollectionViewType.NoteTaking, ]), title: "Perspective", toolTip: "View", btnType: ButtonType.DropdownList, ignoreClick: true, width: 100, scripts: { script: '{ return 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: false, ignoreClick: true, scripts: { script: 'return setHeaderColor(value, _readOnly_)'} }, diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 83b83240e..5ae292760 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -269,8 +269,8 @@ export class DocumentManager { if (options.openLocation?.includes(OpenWhere.lightbox)) { // even if we found the document view, if the target is a lightbox, we try to open it in the lightbox to preserve lightbox semantics (eg, there's only one active doc in the lightbox) const target = DocCast(targetDoc.annotationOn, targetDoc); - const contextView = this.getDocumentView(DocCast(target.embedContainer)); - if (contextView?.ComponentView?.addDocTab?.(target, options.openLocation)) { + const compView = this.getDocumentView(DocCast(target.embedContainer))?.ComponentView; + if ((compView?.addDocTab ?? compView?._props.addDocTab)?.(target, options.openLocation)) { await new Promise<void>(waitres => { setTimeout(() => waitres()); }); diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 7db13689d..d55d193cc 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -569,7 +569,7 @@ export namespace DragManager { AbortDrag(); await finishDrag?.(new DragCompleteEvent(true, docDragData)); DragManager.StartWindowDrag?.(e, docDragData.droppedDocuments, aborted => { - if (!aborted && (docDragData?.dropAction === 'move' || docDragData?.dropAction === 'same')) { + if (!aborted && (docDragData?.dropAction === dropActionType.move || docDragData?.dropAction === dropActionType.same)) { docDragData.removeDocument?.(docDragData?.draggedDocuments[0]); } }); diff --git a/src/typings/type_decls.d b/src/client/util/node_modules/type_decls.d index 1a93bbe59..1a93bbe59 100644 --- a/src/typings/type_decls.d +++ b/src/client/util/node_modules/type_decls.d diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx index 33e905a54..eced64524 100644 --- a/src/client/views/DashboardView.tsx +++ b/src/client/views/DashboardView.tsx @@ -465,7 +465,7 @@ export class DashboardView extends ObservableReactComponent<object> { isSystem: true, layout_explainer: 'All of the calendars that you have created will appear here.', }; - const myCalendars = DocUtils.AssignScripts(Docs.Create.CalendarCollectionDocument([], reqdOpts)); + const myCalendars = DocUtils.AssignScripts(Docs.Create.StackingDocument([], reqdOpts)); // { treeView_ChildDoubleClick: 'openPresentation(documentView.rootDoc)' } return new PrefetchProxy(myCalendars); } diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index 58b7f207c..f14fd033b 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -9,7 +9,7 @@ import * as React from 'react'; import { FaEdit } from 'react-icons/fa'; import { returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick } from '../../ClientUtils'; import { emptyFunction } from '../../Utils'; -import { Doc } from '../../fields/Doc'; +import { Doc, DocListCast } from '../../fields/Doc'; import { Cast, DocCast } from '../../fields/Types'; import { DocUtils, IsFollowLinkScript } from '../documents/DocUtils'; import { CalendarManager } from '../util/CalendarManager'; @@ -264,27 +264,22 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( } @computed - get calendarButton() { - const targetDoc = this.view0?.Document; - return !targetDoc ? null : ( - <Tooltip title={<div className="dash-calendar-button">Open calendar menu</div>}> - <div - className="documentButtonBar-icon" - style={{ color: 'white' }} - onClick={() => { - CalendarManager.Instance.open(this.view0, targetDoc); - }}> - <FontAwesomeIcon className="documentdecorations-icon" icon={faCalendarDays as IconLookup} /> - </div> - </Tooltip> - ); - } - - @computed get keywordButton() { return !DocumentView.Selected().length ? null : ( <Tooltip title={<div className="dash-keyword-button">Open keyword menu</div>}> - <div className="documentButtonBar-icon" style={{ color: 'white' }} onClick={() => DocumentView.Selected().map(dv => (dv.dataDoc.showTags = !dv.dataDoc.showTags))}> + <div + className="documentButtonBar-icon" + style={{ color: 'white' }} + onClick={undoable(e => { + const showing = DocumentView.Selected().some(dv => dv.layoutDoc._layout_showTags); + DocumentView.Selected().forEach(dv => { + dv.layoutDoc._layout_showTags = !showing; + if (e.shiftKey) + DocListCast(dv.Document[Doc.LayoutFieldKey(dv.Document) + '_annotations']).forEach(doc => { + if (doc.face) doc.hidden = showing; + }); + }); + }, 'show Doc tags')}> <FontAwesomeIcon className="documentdecorations-icon" icon="tag" /> </div> </Tooltip> @@ -460,7 +455,6 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( {!DocumentView.Selected().some(v => v.allLinks.length) ? null : <div className="documentButtonBar-button">{this.followLinkButton}</div>} <div className="documentButtonBar-button">{this.pinButton}</div> <div className="documentButtonBar-button">{this.recordButton}</div> - <div className="documentButtonBar-button">{this.calendarButton}</div> <div className="documentButtonBar-button">{this.keywordButton}</div> {!Doc.UserDoc().documentLinksButton_fullMenu ? null : <div className="documentButtonBar-button">{this.shareButton}</div>} <div className="documentButtonBar-button">{this.menuButton}</div> diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss index 67e1054c3..346df10d5 100644 --- a/src/client/views/DocumentDecorations.scss +++ b/src/client/views/DocumentDecorations.scss @@ -38,6 +38,13 @@ $resizeHandler: 8px; background: green; border-radius: 50%; } + .documentDecorations-tagsView { + position: absolute; + height: 100%; + pointer-events: all; + border-radius: 50%; + color: black; + } } .documentDecorations-container { diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index da35459bb..5e7908725 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -35,6 +35,7 @@ import { DocumentView } from './nodes/DocumentView'; import { ImageBox } from './nodes/ImageBox'; import { OpenWhere, OpenWhereMod } from './nodes/OpenWhere'; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; +import { TagsView } from './TagsView'; interface DocumentDecorationsProps { PanelWidth: number; @@ -172,7 +173,8 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora !this._editingTitle && (this._accumulatedTitle = this._titleControlString.startsWith('$') ? (selected && Field.toKeyValueString(selected, this._titleControlString.substring(1))) || '-unset-' : this._titleControlString); this._editingTitle = true; this._keyinput.current && setTimeout(this._keyinput.current.focus); - }) + }), + false // can't preventDefault since that will mess up goldenlayout if you drag over the tab bar. so just stop propagation below. ); e.stopPropagation(); } @@ -194,7 +196,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora if (containers.size > 1) return false; const { left, top } = dragDocView.getBounds || { left: 0, top: 0 }; const dragData = new DragManager.DocumentDragData(DocumentView.SelectedDocs(), dragDocView._props.dropAction); - dragData.offset = dragDocView.screenToContentsTransform().transformDirection(e.x - left, e.y - top); + dragData.offset = dragDocView.screenToViewTransform().transformDirection(e.x - left, e.y - top); dragData.moveDocument = dragDocView._props.moveDocument; dragData.removeDocument = dragDocView._props.removeDocument; dragData.isDocDecorationMove = true; @@ -231,7 +233,8 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora if (iconViewDoc.activeFrame) { iconViewDoc.opacity = 0; // bcz: hacky ... allows inkMasks and other documents to be "turned off" without removing them from the animated collection which allows them to function properly in a presenation. } else { - iconView._props.removeDocument?.(iconView.Document); + if (iconView.Document.annotationOn && iconView.Document.face) iconView.Document.hidden = true; + else iconView._props.removeDocument?.(iconView.Document); } }); views.forEach(DocumentView.DeselectView); @@ -643,7 +646,6 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora render() { const { b, r, x, y } = this.Bounds; const seldocview = DocumentView.Selected().lastElement(); - const doc = DocumentView.SelectedDocs().lastElement(); if (SnappingManager.IsDragging || r - x < 1 || x === Number.MAX_VALUE || !seldocview || this._hidden || isNaN(r) || isNaN(b) || isNaN(x) || isNaN(y)) { setTimeout( action(() => { @@ -835,12 +837,20 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora <div className="link-button-container" style={{ - top: `${doc[DocData].showTags ? 4 + seldocview.TagPanelHeight : 4}px`, + top: DocumentView.Selected().length > 1 ? 0 : `${seldocview.Document._layout_showTags ? 4 + seldocview.TagPanelHeight : 4}px`, transform: `translate(${-this._resizeBorderWidth / 2 + 10}px, ${this._resizeBorderWidth + bounds.b - bounds.y + this._titleHeight}px) `, }}> <DocumentButtonBar views={() => DocumentView.Selected()} /> </div> )} + <div + className="documentDecorations-tagsView" + style={{ + top: `${seldocview.Document._layout_showTags ? 4 + seldocview.TagPanelHeight : 4}px`, + transform: `translate(${-this._resizeBorderWidth / 2 + 10}px, ${this._resizeBorderWidth + bounds.b - bounds.y + this._titleHeight}px) `, + }}> + {DocumentView.Selected().length > 1 ? <TagsView Views={DocumentView.Selected()} /> : null} + </div> </div> {useRotation && ( diff --git a/src/client/views/FilterPanel.tsx b/src/client/views/FilterPanel.tsx index b11fa3bd5..2f6d1fbaa 100644 --- a/src/client/views/FilterPanel.tsx +++ b/src/client/views/FilterPanel.tsx @@ -192,21 +192,17 @@ export class FilterPanel extends ObservableReactComponent<filterProps> { const allCollectionDocs = new Set<Doc>(); SearchUtil.foreachRecursiveDoc(this.targetDocChildren, (depth: number, doc: Doc) => allCollectionDocs.add(doc)); 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]) - .filter(h => h) - .forEach(key => set.add(key)) - ); - else - allCollectionDocs.forEach(child => { - const fieldVal = child[facetHeader] as FieldType; - if (!(fieldVal instanceof List)) { - // currently we have no good way of filtering based on a field that is a list - set.add(Field.toString(fieldVal)); - (fieldVal === true || fieldVal === false) && set.add((!fieldVal).toString()); - } - }); + + allCollectionDocs.forEach(child => { + const fieldVal = child[facetHeader] as FieldType; + const fieldStrList = StrListCast(child[facetHeader]).filter(h => h); + if (fieldStrList.length) fieldStrList.forEach(key => set.add(key)); + else if (!(fieldVal instanceof List)) { + // currently we have no good way of filtering based on a field that is a list + set.add(Field.toString(fieldVal)); + (fieldVal === true || fieldVal === false) && set.add((!fieldVal).toString()); + } + }); const facetValues = Array.from(set).filter(v => v); let nonNumbers = 0; diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 64cf7fbf8..389207893 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -3,7 +3,7 @@ import { faBuffer, faHireAHelper } from '@fortawesome/free-brands-svg-icons'; import * as far from '@fortawesome/free-regular-svg-icons'; import * as fa from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, configure, makeObservable, observable, reaction, runInAction } from 'mobx'; +import { action, computed, configure, makeObservable, observable, reaction, runInAction, trace } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import ResizeObserver from 'resize-observer-polyfill'; @@ -708,8 +708,8 @@ export class MainView extends ObservableReactComponent<object> { setupMoveUpEvents( this, e, - action(() => { - SnappingManager.SetPropertiesWidth(Math.max(0, this._dashUIWidth - e.clientX)); + action(moveEv => { + SnappingManager.SetPropertiesWidth(Math.max(0, this._dashUIWidth - moveEv.clientX)); return !SnappingManager.PropertiesWidth; }), action(() => { @@ -808,6 +808,7 @@ export class MainView extends ObservableReactComponent<object> { childFilters={returnEmptyFilter} childFiltersByRanges={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} + dontCenter="y" /> </div> ); @@ -833,6 +834,7 @@ export class MainView extends ObservableReactComponent<object> { }; @computed get mainInnerContent() { + trace(); const leftMenuFlyoutWidth = this._leftMenuFlyoutWidth + this.leftMenuWidth(); const width = this.propertiesWidth() + leftMenuFlyoutWidth; return ( diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index daa8e1720..f42838086 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -138,7 +138,9 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps return this.selectedDoc?.isGroup; } @computed get isStack() { - return [CollectionViewType.Masonry, CollectionViewType.Multicolumn, CollectionViewType.Multirow, CollectionViewType.Stacking, CollectionViewType.NoteTaking].includes(this.selectedDoc?.type_collection as CollectionViewType); + return [CollectionViewType.Masonry, CollectionViewType.Multicolumn, CollectionViewType.Multirow, CollectionViewType.Stacking, CollectionViewType.NoteTaking, CollectionViewType.Carousel].includes( + this.selectedDoc?.type_collection as CollectionViewType + ); } rtfWidth = () => (!this.selectedLayoutDoc ? 0 : Math.min(NumCast(this.selectedLayoutDoc?._width), this._props.width - 20)); diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx index 8f0a35df0..dd60bfa65 100644 --- a/src/client/views/SidebarAnnos.tsx +++ b/src/client/views/SidebarAnnos.tsx @@ -246,6 +246,7 @@ export class SidebarAnnos extends ObservableReactComponent<FieldViewProps & Extr isAnnotationOverlay={false} select={emptyFunction} NativeDimScaling={returnOne} + dontCenter="y" // childlayout_showTitle={this.layout_showTitle} isAnyChildContentActive={returnFalse} childDocumentsActive={this._props.isContentActive} diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index 76cb119ab..262f888fb 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -365,7 +365,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & </Tooltip> ); }; - const tags = () => props?.DocumentView?.() && CollectionFreeFormDocumentView.from(props.DocumentView()) ? <TagsView View={props.DocumentView()}/> : null; + const tags = () => props?.DocumentView?.() ? <TagsView Views={[props.DocumentView()]}/> : null; return ( <> {paint()} diff --git a/src/client/views/TagsView.tsx b/src/client/views/TagsView.tsx index 0ac015b36..be2c28185 100644 --- a/src/client/views/TagsView.tsx +++ b/src/client/views/TagsView.tsx @@ -9,7 +9,7 @@ import { emptyFunction } from '../../Utils'; import { Doc, DocListCast, Field, Opt, StrListCast } from '../../fields/Doc'; import { DocData } from '../../fields/DocSymbols'; import { List } from '../../fields/List'; -import { DocCast, NumCast, StrCast } from '../../fields/Types'; +import { DocCast, StrCast } from '../../fields/Types'; import { DocumentType } from '../documents/DocumentTypes'; import { DragManager } from '../util/DragManager'; import { SnappingManager } from '../util/SnappingManager'; @@ -33,7 +33,7 @@ import { FaceRecognitionHandler } from './search/FaceRecognitionHandler'; */ interface TagItemProps { - doc: Doc; + docs: Doc[]; tag: string; tagDoc: Opt<Doc>; showRemoveUI: boolean; @@ -91,7 +91,7 @@ export class TagItem extends ObservableReactComponent<TagItemProps> { const tagCollection = TagItem.findTagCollectionDoc(tag) ?? TagItem.createTagCollectionDoc(tag); // If the document is of type COLLECTION, make it a smart collection, otherwise, add the tag to the document. - if (doc.type === DocumentType.COL) { + if (doc.type === DocumentType.COL && !doc.annotationOn) { Doc.AddDocToList(tagCollection[DocData], 'collections', doc); // Iterate through the tag Doc collections and add a copy of the document to each collection @@ -172,12 +172,12 @@ export class TagItem extends ObservableReactComponent<TagItemProps> { docData.data = new List<Doc>(newEmbeddings); docData.title = this._props.tag; docData.tags = new List<string>([this._props.tag]); - docData.showTags = true; docData.freeform_fitContentsToBox = true; doc._freeform_panX = doc._freeform_panY = 0; doc._width = 900; doc._height = 900; doc.layout_fitWidth = true; + doc._layout_showTags = true; return doc; })(Doc.MakeCopy(Doc.UserDoc().emptyCollection as Doc, true)); newEmbeddings.forEach(embed => Doc.SetContainer(embed, newCollection)); @@ -207,8 +207,12 @@ export class TagItem extends ObservableReactComponent<TagItemProps> { e.preventDefault(); }; + @computed get doc() { + return this._props.docs.lastElement(); + } + render() { - this._props.tagDoc && setTimeout(() => TagItem.addTagToDoc(this._props.doc, this._props.tag)); // bcz: hack to make sure that Docs are added to their tag Doc collection since metadata can get set anywhere without a guard triggering an add to the collection + this._props.tagDoc && setTimeout(() => this._props.docs.forEach(doc => TagItem.addTagToDoc(doc, this._props.tag))); // bcz: hack to make sure that Docs are added to their tag Doc collection since metadata can get set anywhere without a guard triggering an add to the collection const tag = this._props.tag.replace(/^#/, ''); const metadata = tag.startsWith('@') ? tag.replace(/^@/, '') : ''; return ( @@ -216,7 +220,17 @@ export class TagItem extends ObservableReactComponent<TagItemProps> { {metadata ? ( <span> <b style={{ fontSize: 'smaller' }}>{tag} </b> - {Field.toString(this._props.doc[metadata])} + {typeof this.doc[metadata] === 'boolean' ? ( + <input + type="checkbox" + onClick={e => e.stopPropagation()} + onPointerDown={e => e.stopPropagation()} + onChange={undoable(e => (this.doc[metadata] = !this.doc[metadata]), 'metadata toggle')} + checked={this.doc[metadata] as boolean} + /> + ) : ( + Field.toString(this.doc[metadata]) + )} </span> ) : ( tag @@ -224,7 +238,7 @@ export class TagItem extends ObservableReactComponent<TagItemProps> { {this.props.showRemoveUI && this._props.tagDoc && ( <IconButton tooltip="Remove tag" - onPointerDown={undoable(() => TagItem.removeTagFromDoc(this._props.doc, this._props.tag, this._props.tagDoc), `remove tag ${this._props.tag}`)} + onPointerDown={undoable(() => this._props.docs.forEach(doc => TagItem.removeTagFromDoc(doc, this._props.tag, this._props.tagDoc)), `remove tag ${this._props.tag}`)} icon={<FontAwesomeIcon icon="times" size="sm" />} style={{ width: '8px', height: '8px', marginLeft: '10px' }} /> @@ -235,7 +249,7 @@ export class TagItem extends ObservableReactComponent<TagItemProps> { } interface TagViewProps { - View: DocumentView; + Views: DocumentView[]; } /** @@ -247,15 +261,16 @@ export class TagsView extends ObservableReactComponent<TagViewProps> { super(props); makeObservable(this); } + InsetDist = 25; // how far tag panel is moved up to overlap DocumentView. @observable _panelHeightDirty = 0; @observable _currentInput = ''; - @observable _isEditing = !StrListCast(this._props.View.dataDoc.tags).length; + @observable _isEditing = !StrListCast(this.View.dataDoc.tags).length; _heightDisposer: IReactionDisposer | undefined; componentDidMount() { this._heightDisposer = reaction( - () => this._props.View.screenToContentsTransform(), + () => this.View.screenToContentsTransform(), xf => { this._panelHeightDirty = this._panelHeightDirty + 1; } @@ -265,11 +280,15 @@ export class TagsView extends ObservableReactComponent<TagViewProps> { this._heightDisposer?.(); } + @computed get View() { + return this._props.Views.lastElement(); + } + @computed get currentScale() { - return NumCast(DocCast(this._props.View.Document.embedContainer)?._freeform_scale, 1); + return this._props.Views.length > 1 ? 1 : Math.max(1, 1 / this.View.screenToLocalScale()); } @computed get isEditing() { - return this._isEditing && DocumentView.SelectedDocs().includes(this._props.View.Document); + return this._isEditing && (this._props.Views.length > 1 || DocumentView.SelectedDocs().includes(this.View.Document)); } /** @@ -279,43 +298,48 @@ export class TagsView extends ObservableReactComponent<TagViewProps> { @action setToEditing = (editing = true) => { this._isEditing = editing; - editing && this._props.View.select(false); + editing && this._props.Views.length === 1 && this.View.select(false); }; /** - * Adds the specified tag to the Doc. If the tag is not prefixed with '#', then a '#' prefix is added. - * Whne the tag (after the '#') begins with '@', then a metadata key/value pair is displayed instead of - * just the tag. - * @param tag tag string to add + * Adds the specified tag or metadata to the Doc. If the tag is not prefixed with '#', then a '#' prefix is added. + * When the tag (after the '#') begins with '@', then a metadata key/value pair is displayed instead of + * just the tag. In addition, a suffix of :<value> can be added to set a metadata value + * @param tag tag string to add (format: #<tag> | #@field(:(=)?value)? ) */ submitTag = undoable( action((tag: string) => { - const submittedLabel = tag.trim(); - submittedLabel && TagItem.addTagToDoc(this._props.View.Document, '#' + submittedLabel.replace(/^#/, '')); + const submittedLabel = tag.trim().replace(/^#/, '').split(':'); + if (submittedLabel[0]) { + this._props.Views.forEach(view => { + TagItem.addTagToDoc(view.Document, '#' + submittedLabel[0]); + if (submittedLabel.length > 1) Doc.SetField(view.Document, submittedLabel[0].replace(/^@/, ''), ':' + submittedLabel[1]); + }); + } this._currentInput = ''; // Clear the input box }), 'added doc label' ); /** - * When 'showTags' is set on a Doc, this displays a wrapping panel of tagItemViews corresponding to all the tags set on the Doc). + * When 'layout_showTags' is set on a Doc, this displays a wrapping panel of tagItemViews corresponding to all the tags set on the Doc). * When the dropdown is clicked, this will toggle an extended UI that allows additional tags to be added/removed. */ render() { - const tagsList = new Set<string>(StrListCast(this._props.View.dataDoc.tags)); - const chatTagsList = new Set<string>(StrListCast(this._props.View.dataDoc.tags_chat)); + const tagsList = new Set<string>(StrListCast(this.View.dataDoc.tags)); + const chatTagsList = new Set<string>(StrListCast(this.View.dataDoc.tags_chat)); const facesList = new Set<string>( - DocListCast(this._props.View.dataDoc[Doc.LayoutFieldKey(this._props.View.Document) + '_annotations']) - .concat(this._props.View.Document) + DocListCast(this.View.dataDoc[Doc.LayoutFieldKey(this.View.Document) + '_annotations']) + .concat(this.View.Document) .filter(d => d.face) .map(doc => StrCast(DocCast(doc.face)?.title)) ); this._panelHeightDirty; - return !this._props.View.Document.showTags ? null : ( + return this.View.ComponentView?.isUnstyledView?.() || (!this.View.Document._layout_showTags && this._props.Views.length === 1) ? null : ( <div className="tagsView-container" - ref={r => r && new ResizeObserver(action(() => (this._props.View.TagPanelHeight = r?.getBoundingClientRect().height ?? 0))).observe(r)} + ref={r => r && new ResizeObserver(action(() => this._props.Views.length === 1 && (this.View.TagPanelHeight = Math.max(0, (r?.getBoundingClientRect().height ?? 0) - this.InsetDist)))).observe(r)} style={{ transformOrigin: 'top left', maxWidth: `${100 * this.currentScale}%`, @@ -323,6 +347,8 @@ export class TagsView extends ObservableReactComponent<TagViewProps> { transform: `scale(${1 / this.currentScale})`, backgroundColor: this.isEditing ? Colors.LIGHT_GRAY : Colors.TRANSPARENT, borderColor: this.isEditing ? Colors.BLACK : Colors.TRANSPARENT, + position: 'relative', + top: this._props.Views.length > 1 ? 25 : `calc(-${this.InsetDist} * ${1 / this.currentScale}px)`, }}> <div className="tagsView-content" style={{ width: '100%' }}> <div className="tagsView-list"> @@ -330,10 +356,17 @@ export class TagsView extends ObservableReactComponent<TagViewProps> { <IconButton style={{ width: '8px' }} tooltip="Close Menu" onPointerDown={() => this.setToEditing(!this._isEditing)} icon={<FontAwesomeIcon icon={this._isEditing ? 'chevron-up' : 'chevron-down'} size="sm" />} /> )} {Array.from(tagsList).map((tag, i) => ( - <TagItem key={i} doc={this._props.View.Document} tag={tag} tagDoc={TagItem.findTagCollectionDoc(tag) ?? TagItem.createTagCollectionDoc(tag)} setToEditing={this.setToEditing} showRemoveUI={this.isEditing} /> + <TagItem + key={i} + docs={this._props.Views.map(view => view.Document)} + tag={tag} + tagDoc={TagItem.findTagCollectionDoc(tag) ?? TagItem.createTagCollectionDoc(tag)} + setToEditing={this.setToEditing} + showRemoveUI={this.isEditing} + /> ))} {Array.from(facesList).map((tag, i) => ( - <TagItem key={i} doc={this._props.View.Document} tag={tag} tagDoc={undefined} setToEditing={this.setToEditing} showRemoveUI={this.isEditing} /> + <TagItem key={i} docs={this._props.Views.map(view => view.Document)} tag={tag} tagDoc={undefined} setToEditing={this.setToEditing} showRemoveUI={this.isEditing} /> ))} </div> {this.isEditing ? ( diff --git a/src/client/views/collections/CollectionCalendarView.tsx b/src/client/views/collections/CollectionCalendarView.tsx index 9eb16917b..0ea9f8ebc 100644 --- a/src/client/views/collections/CollectionCalendarView.tsx +++ b/src/client/views/collections/CollectionCalendarView.tsx @@ -38,8 +38,8 @@ export class CollectionCalendarView extends CollectionSubView() { const aDateRangeStr = StrCast(DocListCast(calendarA.data).lastElement()?.date_range); const bDateRangeStr = StrCast(DocListCast(calendarB.data).lastElement()?.date_range); - const [aFromDate, aToDate] = dateRangeStrToDates(aDateRangeStr); - const [bFromDate, bToDate] = dateRangeStrToDates(bDateRangeStr); + const { start: aFromDate, end: aToDate } = dateRangeStrToDates(aDateRangeStr); + const { start: bFromDate, end: bToDate } = dateRangeStrToDates(bDateRangeStr); if (aFromDate > bFromDate) { return -1; // a comes first @@ -82,6 +82,7 @@ export class CollectionCalendarView extends CollectionSubView() { isAnnotationOverlay={false} // select={emptyFunction} What does this mean? isAnyChildContentActive={returnTrue} // ?? + dontCenter="y" // childDocumentsActive={} // whenChildContentsActiveChanged={} childHideDecorationTitle={false} diff --git a/src/client/views/collections/CollectionCardDeckView.scss b/src/client/views/collections/CollectionCardDeckView.scss index a089b248d..cc797d0bd 100644 --- a/src/client/views/collections/CollectionCardDeckView.scss +++ b/src/client/views/collections/CollectionCardDeckView.scss @@ -6,6 +6,15 @@ position: relative; background-color: white; overflow: hidden; + + button { + width: 35px; + height: 35px; + border-radius: 50%; + background-color: $dark-gray; + // border-color: $medium-blue; + margin: 5px; // transform: translateY(-50px); + } } .card-wrapper { @@ -34,15 +43,6 @@ justify-content: start; /* Centers buttons horizontally */ } -button { - width: 35px; - height: 35px; - border-radius: 50%; - background-color: $dark-gray; - // border-color: $medium-blue; - margin: 5px; // transform: translateY(-50px); -} - // button:hover { // transform: translateY(-50px); // } diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx index c799eb3c8..54cc02825 100644 --- a/src/client/views/collections/CollectionCarousel3DView.tsx +++ b/src/client/views/collections/CollectionCarousel3DView.tsx @@ -78,6 +78,7 @@ export class CollectionCarousel3DView extends CollectionSubView() { NativeWidth={returnZero} NativeHeight={returnZero} fitWidth={undefined} + containerViewPath={this.childContainerViewPath} onDoubleClickScript={this.onChildDoubleClick} renderDepth={this._props.renderDepth + 1} LayoutTemplate={this._props.childLayoutTemplate} diff --git a/src/client/views/collections/CollectionCarouselView.scss b/src/client/views/collections/CollectionCarouselView.scss index b402a7a32..c40f471d6 100644 --- a/src/client/views/collections/CollectionCarouselView.scss +++ b/src/client/views/collections/CollectionCarouselView.scss @@ -23,6 +23,21 @@ } } +.collectionCarouselView-addFlashcards { + justify-content: center; + align-items: center; + height: 100%; + z-index: -1; + pointer-events: none; +} +.collectionCarouselView-recentlyMissed { + color: red; + z-index: 999; + position: relative; + left: 10px; + top: 10px; + pointer-events: none; +} .carouselView-back, .carouselView-fwd, .carouselView-star, @@ -68,41 +83,52 @@ right: 52%; } .carouselView-quiz { - position: absolute; + position: relative; display: flex; - top: 5px; - right: 8px; + flex-direction: column; + height: 20px; + align-items: center; + margin: auto; &:hover { color: white; } } .carouselView-practice { - position: absolute; + position: relative; display: flex; - top: 22px; - right: 8px; + flex-direction: column; + height: 20px; + align-items: center; + margin: auto; &:hover { color: white; } } .carouselView-starFilter { - position: absolute; + position: relative; display: flex; - top: 40px; - right: 7px; + height: 20px; + align-items: center; &:hover { color: white; } } +.carouselView-practiceModes { + width: 100%; + height: 40px; + display: flex; + flex-direction: column; +} .carouselView-menu { position: absolute; + flex-direction: column; + align-items: center; display: flex; top: 2px; right: 2px; width: 30; - height: 60; border-radius: 5px; color: rgba(255, 255, 255, 0.5); background: rgba(0, 0, 0, 0.1); diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx index 9f59322e8..91a7c6514 100644 --- a/src/client/views/collections/CollectionCarouselView.tsx +++ b/src/client/views/collections/CollectionCarouselView.tsx @@ -1,13 +1,12 @@ /* eslint-disable react/jsx-props-no-spreading */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; -import { action, computed, makeObservable, observable, trace } from 'mobx'; +import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { StopEvent, returnFalse, returnOne, returnTrue, returnZero } from '../../../ClientUtils'; -import { emptyFunction } from '../../../Utils'; +import { StopEvent, returnOne, returnZero } from '../../../ClientUtils'; import { Doc, Opt } from '../../../fields/Doc'; -import { DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; +import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { DocumentType } from '../../documents/DocumentTypes'; import { Docs } from '../../documents/Documents'; import { DragManager } from '../../util/DragManager'; @@ -19,15 +18,12 @@ import './CollectionCarouselView.scss'; import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; enum cardMode { - // PRACTICE = 'practice', STAR = 'star', - // QUIZ = 'quiz', ALL = 'all', } enum practiceMode { PRACTICE = 'practice', QUIZ = 'quiz', - NORMAL = 'normal', } enum practiceVal { MISSED = 'missed', @@ -36,24 +32,20 @@ enum practiceVal { @observer export class CollectionCarouselView extends CollectionSubView() { private _dropDisposer?: DragManager.DragDropDisposer; - @observable private _practiceMessage: string | undefined; - @observable private _filterMessage: string | undefined; get practiceField() { return this.fieldKey + "_practice"; } // prettier-ignore - get sideField() { return "_" + this.fieldKey + "_usePath"; } // prettier-ignore + get sideField() { return "_" + this.fieldKey + "_usePath"; } // prettier-ignore get starField() { return "star"; } // prettier-ignore + _fadeTimer: NodeJS.Timeout | undefined; + constructor(props: SubCollectionViewProps) { super(props); makeObservable(this); - // this.setModes(); - this.layoutDoc.filterOp = cardMode.ALL; - Doc.setDocFilter(this.Document, 'star', undefined, 'match'); - this.layoutDoc.practiceMode = practiceMode.NORMAL; - this.layoutDoc._carousel_index = 0; - this.carouselItems.forEach(item => { item.layout[this.practiceField] = undefined}); //prettier-ignore - console.log(this.carouselItems.length); } + @observable _last_index = this.carouselIndex; + @observable _last_opacity = 1; + componentWillUnmount() { this._dropDisposer?.(); } @@ -65,65 +57,46 @@ export class CollectionCarouselView extends CollectionSubView() { } }; - @computed get carouselItems() { - this.childLayoutPairs.map(pair => { - pair.layout.embedContainer = this.Document; - }); - return this.childLayoutPairs.filter(pair => pair.layout.type !== DocumentType.LINK); + @computed get practiceMode() { + return this.childDocs.some(doc => doc._layout_isFlashcard) ? StrCast(this.layoutDoc.practiceMode) : ''; } - @computed get marginX() { - return NumCast(this.layoutDoc.caption_xMargin, 50); + @computed get practiceMessage() { + const cardCount = this.carouselItems.length; + if (this.practiceMode) { + if (!Doc.hasDocFilter(this.layoutDoc, 'star') && !cardCount) { + return 'Finished! Click here to view all flashcards.'; + } + } + return ''; } - @action setPracticeMessage = (mes: string | undefined) => { - this._practiceMessage = mes; - }; - @action setFilterMessage = (mes: string | undefined) => { - this._filterMessage = mes; - }; - - setModes = () => { - this.layoutDoc.filterOp = cardMode.ALL; - Doc.setDocFilter(this.Document, 'data_star', undefined, 'match'); - this.layoutDoc.practiceMode = practiceMode.NORMAL; - this.layoutDoc._carousel_index = 0; - }; - - move = (dir: number) => { - const moveToCardWithField = (match: (doc: Doc) => boolean): boolean => { - let startInd = (NumCast(this.layoutDoc._carousel_index) + dir) % this.carouselItems.length; - while (!match(this.carouselItems?.[startInd].layout) && (startInd + this.carouselItems.length) % this.carouselItems.length !== this.layoutDoc._carousel_index) { - startInd = (startInd + dir + this.carouselItems.length) % this.carouselItems.length; + @computed get filterMessage() { + const cardCount = this.carouselItems.length; + if (!this.practiceMessage) { + if (Doc.hasDocFilter(this.layoutDoc, 'star') && !cardCount) { + return 'No starred items. Click here to view all flash cards.'; } - if (match(this.carouselItems?.[startInd].layout)) { - this.layoutDoc._carousel_index = startInd; - return true; + if (this.practiceMode) { + if (!cardCount) return 'No flashcards to show! Click here to leave practice mode'; } - return match(this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout); - }; - - switch (this.layoutDoc.practiceMode && this.layoutDoc.filterOp) { - case practiceMode.PRACTICE && cardMode.ALL: - if (!moveToCardWithField((doc: Doc) => doc[this.practiceField] !== practiceVal.CORRECT)) { - this._practiceMessage = 'Finished! Unselect practice mode to view all flashcards.'; - this.carouselItems.forEach(item => { item.layout[this.practiceField] = undefined}); //prettier-ignore - } - break; - case !practiceMode.PRACTICE && cardMode.STAR: - if (!moveToCardWithField((doc: Doc) => !!doc[this.starField])) { - this._filterMessage = 'No starred items. Unselect this view to see all flashcards and star them.'; - } - break; - case practiceMode.PRACTICE && cardMode.STAR: - if (!moveToCardWithField((doc: Doc) => doc[this.practiceField] !== practiceVal.CORRECT && doc[this.starField] === true)) { - this._filterMessage = 'No flashcards to show! Unselect mode to view all flashcards.'; - this._practiceMessage = undefined; - } - break; - default: - moveToCardWithField(returnTrue); } - }; + return ''; + } + @computed get marginX() { return NumCast(this.layoutDoc.caption_xMargin, 50); } // prettier-ignore + @computed get carouselIndex() { return NumCast(this.layoutDoc._carousel_index) % this.carouselItems.length; } // prettier-ignore + @computed get carouselItems() { return this.childDocs + .filter(doc => doc.type !== DocumentType.LINK) + .filter(doc => !this.practiceMode || (BoolCast(doc?._layout_isFlashcard) && doc[this.practiceField] !== practiceVal.CORRECT))// show only cards that aren't marked as correct + } // prettier-ignore + + /** + * Move forward or backward the specified number of Docs + * @param dir signed number indicating Docs to move forward or backward + */ + move = action((dir: number) => { + this._last_index = this.carouselIndex; + this.layoutDoc._carousel_index = this.carouselItems.length ? (this.carouselIndex + dir + this.carouselItems.length) % this.carouselItems.length : 0; + }); /** * Goes to the next Doc in the stack subject to the currently selected filter option. @@ -142,14 +115,12 @@ export class CollectionCarouselView extends CollectionSubView() { }; /* - * Stars the document when the star button is pressed. + * Toggles whether the 'star' metadata field is set on the current Doc */ - star = (e: React.MouseEvent) => { + toggleStar = (e: React.MouseEvent) => { e.stopPropagation(); - const curDoc = this.carouselItems[NumCast(this.layoutDoc._carousel_index)]; - curDoc.layout[this.starField] = curDoc.layout[this.starField] ? undefined : true; - // if (!curDoc.layout[this.starField]) this.move(1); - // this.layoutDoc._carousel_index = undefined; + const curDoc = this.carouselItems[this.carouselIndex]; + curDoc && (curDoc[this.starField] = curDoc[this.starField] ? undefined : true); }; /* @@ -157,81 +128,110 @@ export class CollectionCarouselView extends CollectionSubView() { */ setPracticeVal = (e: React.MouseEvent, val: string) => { e.stopPropagation(); - const curDoc = this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)]; - curDoc.layout[this.practiceField] = val; + const curDoc = this.carouselItems[this.carouselIndex]; + curDoc && (curDoc[this.practiceField] = val); this.advance(e); }; + /** + * Sets the practice mode answer style for flashcards + * @param mode practiceMode or undefined for no practice + */ + setPracticeMode = (mode: practiceMode | undefined) => { + this.layoutDoc.practiceMode = mode; + this.carouselItems?.map(doc => (doc[this.practiceField] = undefined)); + if (mode === practiceMode.QUIZ) this.carouselItems?.map(doc => (doc[this.sideField] = undefined)); + }; + captionStyleProvider = (doc: Doc | undefined, captionProps: Opt<FieldViewProps>, property: string) => { // first look for properties on the document in the carousel, then fallback to properties on the container const childValue = doc?.['caption_' + property] ? this._props.styleProvider?.(doc, captionProps, property) : undefined; return childValue ?? this._props.styleProvider?.(this.layoutDoc, captionProps, property); }; - panelHeight = () => this._props.PanelHeight() - (StrCast(this.layoutDoc._layout_showCaption) ? 50 : 0); + contentPanelHeight = () => this._props.PanelHeight() - (StrCast(this.layoutDoc._layout_showCaption) ? 50 : 0) - 2 * NumCast(this.layoutDoc.yMargin); onContentDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick); onContentClick = () => ScriptCast(this.layoutDoc.onChildClick); captionWidth = () => this._props.PanelWidth() - 2 * this.marginX; + contentScreentToLocalXf = () => this._props.ScreenToLocalTransform().translate(-NumCast(this.layoutDoc.xMargin), -NumCast(this.layoutDoc.yMargin)); - setPracticeMode = (mode: practiceMode) => { - this.layoutDoc.practiceMode = mode; - this.carouselItems?.map(doc => (doc.layout[this.practiceField] = undefined)); - switch (mode) { - case practiceMode.QUIZ: - this.carouselItems?.map(doc => (doc.layout[this.sideField] = undefined)); - break; - case practiceMode.NORMAL: - this.setPracticeMessage(undefined); - break; - } - }; + contentPanelWidth = () => this._props.PanelWidth() - 2 * NumCast(this.layoutDoc.xMargin); - setFilterMode = (mode: cardMode) => { - this.layoutDoc.filterOp = mode; - switch (mode) { - case cardMode.STAR: - // Doc.setDocFilter(this.Document, 'data_star', true, 'match'); - this.move(1); - break; - default: - this.setFilterMessage(undefined); // prettier-ignore - // Doc.setDocFilter(this.Document, 'data_star', true, 'remove'); - } - }; + isChildContentActive = () => + this._props.isContentActive?.() === false + ? false + : this._props.isDocumentActive?.() && (this._props.childDocumentsActive?.() || BoolCast(this.Document.childDocumentsActive)) + ? true + : this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false + ? false + : undefined; + renderDoc = (doc: Doc, showCaptions: boolean, overlayFunc?: (r: DocumentView | null) => void) => { + return ( + <DocumentView + {...this._props} + ref={overlayFunc} + Document={doc} + NativeWidth={returnZero} + NativeHeight={returnZero} + fitWidth={undefined} + containerViewPath={this.childContainerViewPath} + setContentViewBox={undefined} + onDoubleClickScript={this.onContentDoubleClick} + onClickScript={this.onContentClick} + isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive} + isContentActive={this.isChildContentActive} + hideCaptions={showCaptions} + renderDepth={this._props.renderDepth + 1} + LayoutTemplate={this._props.childLayoutTemplate} + LayoutTemplateString={this._props.childLayoutString} + TemplateDataDocument={DocCast(Doc.Layout(doc).resolvedDataDoc)} + childFilters={this.childDocFilters} + hideDecorations={BoolCast(this.layoutDoc.layout_hideDecorations)} + addDocument={this._props.addDocument} + ScreenToLocalTransform={this.contentScreentToLocalXf} + PanelWidth={this.contentPanelWidth} + PanelHeight={this.contentPanelHeight} + /> + ); + }; + /** + * Display an overlay of the previous card that crossfades to the next card + */ + @computed get overlay() { + const fadeTime = 500; + const lastDoc = this.carouselItems?.[this._last_index]; + return !lastDoc || this.carouselIndex === this._last_index ? null : ( + <div className="collectionCarouselView-image" style={{ opacity: this._last_opacity, position: 'absolute', top: 0, left: 0, transition: `opacity ${fadeTime}ms` }}> + {this.renderDoc( + lastDoc, + false, // hide captions if the carousel is configured to show the captions + action((r: DocumentView | null) => { + if (r) { + this._fadeTimer && clearTimeout(this._fadeTimer); + this._last_opacity = 0; + this._fadeTimer = setTimeout( + action(() => { + this._last_index = -1; + this._last_opacity = 1; + }), + fadeTime + ); + } + }) + )} + </div> + ); + } @computed get content() { - trace(); - if (this.layoutDoc._carousel_index === this.carouselItems.length && this.layoutDoc._carousel_index !== 0) { - this.move(1); - } - const index = NumCast(this.layoutDoc._carousel_index); + const index = this.carouselIndex; const curDoc = this.carouselItems?.[index]; const captionProps = { ...this._props, NativeScaling: returnOne, PanelWidth: this.captionWidth, fieldKey: 'caption', setHeight: undefined, setContentView: undefined }; const carouselShowsCaptions = StrCast(this.layoutDoc._layout_showCaption); - - return !(curDoc?.layout instanceof Doc) ? null : ( + return !curDoc ? null : ( <> - <div className="collectionCarouselView-image"> - <DocumentView - {...this._props} - NativeWidth={returnZero} - NativeHeight={returnZero} - fitWidth={undefined} - setContentViewBox={undefined} - childFilters={this.childDocFilters} - containerViewPath={this._props.docViewPath} - onDoubleClickScript={this.onContentDoubleClick} - onClickScript={this.onContentClick} - isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive} - isContentActive={(this._props.childContentsActive ?? this._props.isContentActive() === false) ? returnFalse : emptyFunction} - addDocument={this._props.addDocument} - hideCaptions={!!carouselShowsCaptions} // hide captions if the carousel is configured to show the captions - renderDepth={this._props.renderDepth + 1} - LayoutTemplate={this._props.childLayoutTemplate} - LayoutTemplateString={this._props.childLayoutString} - Document={curDoc.layout} - TemplateDataDocument={DocCast(curDoc.layout.resolvedDataDoc)} - PanelHeight={this.panelHeight} - /> + <div className="collectionCarouselView-image" key="image"> + {this.renderDoc(curDoc, !!carouselShowsCaptions)} + {this.overlay} </div> {!carouselShowsCaptions ? null : ( <div @@ -244,17 +244,13 @@ export class CollectionCarouselView extends CollectionSubView() { marginLeft: this.marginX, width: `calc(100% - ${this.marginX * 2}px)`, }}> - <FormattedTextBox key={index} {...captionProps} fieldKey={carouselShowsCaptions} styleProvider={this.captionStyleProvider} Document={curDoc.layout} TemplateDataDocument={undefined} /> + <FormattedTextBox key={index} xPadding={10} yPadding={10} {...captionProps} fieldKey={carouselShowsCaptions} styleProvider={this.captionStyleProvider} Document={curDoc} TemplateDataDocument={undefined} /> </div> )} </> ); } - containsDifTypes = (): boolean => { - return this.carouselItems.filter(doc => !doc.layout._layout_isFlashcard).length !== 0; - }; - addFlashcard() { const newDoc = Docs.Create.ComparisonDocument('', { _layout_isFlashcard: true, _width: 300, _height: 300 }); this.addDocument?.(newDoc); @@ -270,30 +266,28 @@ export class CollectionCarouselView extends CollectionSubView() { } @computed get buttons() { - if (!this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)]) return null; + if (!this.carouselItems?.[this.carouselIndex]) return null; return ( <> + <div> + <Tooltip title="star"> + <div key="star" className="carouselView-star" onClick={this.toggleStar}> + <FontAwesomeIcon icon="star" color={this.carouselItems[this.carouselIndex]?.[this.starField] ? 'yellow' : 'gray'} size="1x" /> + </div> + </Tooltip> + {/* <Tooltip title="add new flashcard to pile"> + <div key="add" className="carouselView-add" onClick={this.addFlashcard}> + <FontAwesomeIcon icon="plus" color={this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout[this.starField] ? 'yellow' : 'gray'} size="1x" /> + </div> + </Tooltip> */} + </div> <div key="back" className="carouselView-back" onClick={this.goback}> <FontAwesomeIcon icon="chevron-left" size="2x" /> </div> <div key="fwd" className="carouselView-fwd" onClick={this.advance}> <FontAwesomeIcon icon="chevron-right" size="2x" /> </div> - {!this.containsDifTypes() ? ( - <div> - <Tooltip title="star"> - <div key="star" className="carouselView-star" onClick={this.star}> - <FontAwesomeIcon icon="star" color={this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout[this.starField] ? 'yellow' : 'gray'} size="1x" /> - </div> - </Tooltip> - {/* <Tooltip title="add new flashcard to pile"> - <div key="add" className="carouselView-add" onClick={this.addFlashcard}> - <FontAwesomeIcon icon="plus" color={this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout[this.starField] ? 'yellow' : 'gray'} size="1x" /> - </div> - </Tooltip> */} - </div> - ) : null} - {this.layoutDoc.practiceMode === practiceMode.PRACTICE ? ( + {this.practiceMode ? ( <div> <Tooltip title="Incorrect. View again later."> <div key="remove" className="carouselView-remove" onClick={e => this.setPracticeVal(e, practiceVal.MISSED)}> @@ -311,33 +305,31 @@ export class CollectionCarouselView extends CollectionSubView() { ); } - togglePracticeMode = (mode: practiceMode) => { - if (mode === this.layoutDoc.practiceMode) { - this.setPracticeMode(practiceMode.NORMAL); - // this.setPracticeMessage("undefined"); - } else this.setPracticeMode(mode); - }; - toggleFilterMode = () => { this.layoutDoc.filterOp === cardMode.STAR ? this.setFilterMode(cardMode.ALL) : this.setFilterMode(cardMode.STAR)}; //prettier-ignore + togglePracticeMode = (mode: practiceMode) => this.setPracticeMode(mode === this.practiceMode ? undefined : mode); + toggleFilterMode = () => Doc.setDocFilter(this.Document, 'star', true, 'match', true); setColor = (mode: practiceMode | cardMode, which: string) => { return which === mode ? 'white' : 'light gray'}; //prettier-ignore @computed get menu() { + const curDoc = this.carouselItems?.[this.carouselIndex]; return ( <div className="carouselView-menu"> - <Tooltip title="Practice flashcards using GPT"> - <div key="back" className="carouselView-quiz" onClick={e => this.togglePracticeMode(practiceMode.QUIZ)}> - <FontAwesomeIcon icon="file-pen" color={this.setColor(practiceMode.QUIZ, StrCast(this.layoutDoc.practiceMode))} size="1x" /> - </div> - </Tooltip> - <Tooltip title={this.layoutDoc.practiceMode === practiceMode.PRACTICE ? 'Exit practice mode' : 'Practice flashcards manually'}> - <div key="back" className="carouselView-practice" onClick={e => this.togglePracticeMode(practiceMode.PRACTICE)}> - <FontAwesomeIcon icon="check" color={this.setColor(practiceMode.PRACTICE, StrCast(this.layoutDoc.practiceMode))} size="1x" /> - </div> - </Tooltip> - <Tooltip title={this.layoutDoc.filterOp === cardMode.STAR ? 'Show all cards' : 'Show only starred cards'}> + <Tooltip title={Doc.hasDocFilter(this.Document, 'star') ? 'Show all cards' : 'Show only starred cards'}> <div key="back" className="carouselView-starFilter" onClick={e => this.toggleFilterMode()}> - <FontAwesomeIcon icon="filter" color={this.setColor(cardMode.STAR, StrCast(this.layoutDoc.filterOp))} size="1x" /> + <FontAwesomeIcon icon="filter" color={Doc.hasDocFilter(this.Document, 'star') ? 'white' : 'lightgray'} size="1x" /> </div> </Tooltip> + <div className="carouselView-practiceModes" style={{ display: BoolCast(curDoc?._layout_isFlashcard) ? undefined : 'none' }}> + <Tooltip title="Practice flashcards using GPT"> + <div key="back" className="carouselView-quiz" onClick={e => this.togglePracticeMode(practiceMode.QUIZ)}> + <FontAwesomeIcon icon="file-pen" color={this.setColor(practiceMode.QUIZ, StrCast(this.practiceMode))} size="1x" /> + </div> + </Tooltip> + <Tooltip title={this.practiceMode === practiceMode.PRACTICE ? 'Exit practice mode' : 'Practice flashcards manually'}> + <div key="back" className="carouselView-practice" onClick={e => this.togglePracticeMode(practiceMode.PRACTICE)}> + <FontAwesomeIcon icon="check" color={this.setColor(practiceMode.PRACTICE, StrCast(this.practiceMode))} size="1x" /> + </div> + </Tooltip> + </div> </div> ); } @@ -351,36 +343,22 @@ export class CollectionCarouselView extends CollectionSubView() { background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string, color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string, }}> - {!this._practiceMessage && !this._filterMessage ? ( + {!this.practiceMessage && !this.filterMessage ? ( this.content ) : ( - <p className="message"> - {this._filterMessage} - {'\n'} - {this._practiceMessage} + <p + className="message" + onClick={e => { + if (this.filterMessage || this.practiceMessage) { + this.setPracticeMode(undefined); + Doc.setDocFilter(this.layoutDoc, 'star', undefined, 'remove'); + } + }}> + {this.filterMessage || this.practiceMessage} </p> )} - {!this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)] && this.layoutDoc.practiceMode === practiceMode.PRACTICE ? <p className="message">Add flashcards </p> : null} - <p - className="missed-message" - style={{ - color: 'red', - fontWeight: 'bold', - zIndex: '999', - position: 'relative', - left: '10px', - top: '10px', - width: '10px', - display: this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)] - ? this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout[this.practiceField] === practiceVal.MISSED && this.layoutDoc.practiceMode === practiceMode.PRACTICE && !this._practiceMessage - ? 'block' - : 'none' - : 'none', - }}> - Recently missed! - </p> - {!this.containsDifTypes() && this.carouselItems.length !== 0 ? this.menu : null} - {this.Document._chromeHidden || (!this._filterMessage && !this._practiceMessage) ? this.buttons : null} + {!this.Document._chromeHidden ? this.menu : null} + {!this.Document._chromeHidden ? this.buttons : null} </div> ); } diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index e0aa79c7b..028133a6e 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -31,6 +31,7 @@ import { ScriptingRepl } from '../ScriptingRepl'; import { UndoStack } from '../UndoStack'; import './CollectionDockingView.scss'; import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; +import { TabHTMLElement } from './TabDocView'; @observer export class CollectionDockingView extends CollectionSubView() { @@ -544,7 +545,7 @@ export class CollectionDockingView extends CollectionSubView() { tabCreated = (tab: { contentItem: { element: HTMLElement[] } }) => { this.tabMap.add(tab); // InitTab is added to the tab's HTMLElement in TabDocView - const tabdocviewContent = tab.contentItem.element[0]?.firstChild?.firstChild as unknown as { InitTab?: (tab: object) => void }; + const tabdocviewContent = tab.contentItem.element[0]?.firstChild?.firstChild as unknown as TabHTMLElement; tabdocviewContent?.InitTab?.(tab); // have to explicitly initialize tabs that reuse contents from previous tabs (ie, when dragging a tab around a new tab is created for the old content) }; diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss index 6225cc52a..6400a0a8e 100644 --- a/src/client/views/collections/CollectionStackingView.scss +++ b/src/client/views/collections/CollectionStackingView.scss @@ -54,11 +54,8 @@ } .collectionStackingViewFieldColumn { - height: max-content; - } - - .collectionStackingViewFieldColumnDragging { - height: 100%; + display: flex; + flex-direction: column; } .collectionSchemaView-previewDoc { diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 6402ef16c..e97ee713e 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -35,6 +35,7 @@ import { CollectionMasonryViewFieldRow } from './CollectionMasonryViewFieldRow'; import './CollectionStackingView.scss'; import { CollectionStackingViewFieldColumn } from './CollectionStackingViewFieldColumn'; import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; +import { computedFn } from 'mobx-utils'; export type collectionStackingViewProps = { sortFunc?: (a: Doc, b: Doc) => number; @@ -127,6 +128,9 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection } } + columnWidthFn = () => this.columnWidth; + columnDocHeightFn = (doc: Doc) => () => (this.isStackingView ? this.getDocHeight(doc)() : Math.min(this.getDocHeight(doc)(), this._props.PanelHeight())); + // TODO: plj - these are the children children = (docs: Doc[]) => { // TODO: can somebody explain me to what exactly TraceMobX is? @@ -140,17 +144,14 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection }) ); return docs.map((d, i) => { - const height = () => this.getDocHeight(d); - const width = () => this.getDocWidth(d); - const trans = () => this.getDocTransition(d); // assuming we need to get rowSpan because we might be dealing with many columns. Grid gap makes sense if multiple columns - const rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap); + const rowSpan = Math.ceil((this.getDocHeight(d)() + this.gridGap) / this.gridGap); // just getting the style - const style = this.isStackingView ? { margin: this.Document._stacking_alignCenter ? 'auto' : undefined, transition: trans(), width: width(), marginTop: i ? this.gridGap : 0, height: height() } : { gridRowEnd: `span ${rowSpan}` }; + const style = this.isStackingView ? { margin: undefined, transition: this.getDocTransition(d)(), width: this.columnWidth, marginTop: i ? this.gridGap : 0, height: this.getDocHeight(d)() } : { gridRowEnd: `span ${rowSpan}` }; // So we're choosing whether we're going to render a column or a masonry doc return ( <div className={`collectionStackingView-${this.isStackingView ? 'columnDoc' : 'masonryDoc'}`} key={d[Id]} style={style}> - {this.getDisplayDoc(d, width, trans, i)} + {this.getDisplayDoc(d, this.getDocTransition(d), i)} </div> ); }); @@ -311,26 +312,23 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection : this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false ? false : undefined; + isChildButtonContentActive = () => (this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false ? false : undefined); @observable docRefs = new ObservableMap<Doc, DocumentView>(); childFitWidth = (doc: Doc) => Cast(this.Document.childLayoutFitWidth, 'boolean', this._props.childLayoutFitWidth?.(doc) ?? Cast(doc.layout_fitWidth, 'boolean', null)); // this is what renders the document that you see on the screen // called in Children: this actually adds a document to our children list - getDisplayDoc(doc: Doc, width: () => number, trans: () => string, count: number) { + getDisplayDoc(doc: Doc, trans: () => string, count: number) { const dataDoc = doc.isTemplateDoc || doc.isTemplateForField ? this._props.TemplateDataDocument : undefined; - const height = () => this.getDocHeight(doc); - const panelHeight = () => (this.isStackingView ? height() : Math.min(height(), this._props.PanelHeight())); - const panelWidth = () => this.columnWidth; - const stackedDocTransform = () => this.getDocTransform(doc); - this._docXfs.push({ stackedDocTransform, width, height }); + this._docXfs.push({ stackedDocTransform: this.getDocTransform(doc), width: this.getDocWidth(doc), height: this.getDocHeight(doc) }); return count > this._renderCount ? null : ( <DocumentView ref={action((r: DocumentView) => r?.ContentDiv && this.docRefs.set(doc, r))} Document={doc} TemplateDataDocument={dataDoc} renderDepth={this._props.renderDepth + 1} - PanelWidth={panelWidth} - PanelHeight={panelHeight} + PanelWidth={this.columnWidthFn} + PanelHeight={this.columnDocHeightFn(doc)} pointerEvents={this.DocumentView?.()._props.onClickScript?.() ? returnNone : undefined} // if the stack has an onClick, then we don't want the contents to be interactive (see CollectionPileView) styleProvider={this.styleProvider} containerViewPath={this.childContainerViewPath} @@ -341,16 +339,16 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection isDocumentActive={this.isContentActive} LayoutTemplate={this._props.childLayoutTemplate} LayoutTemplateString={this._props.childLayoutString} - NativeWidth={this._props.childIgnoreNativeSize ? returnZero : this._props.childLayoutFitWidth?.(doc) || (this.childFitWidth(doc) && !Doc.NativeWidth(doc)) ? width : undefined} // explicitly ignore nativeWidth/height if childIgnoreNativeSize is set- used by PresBox - NativeHeight={this._props.childIgnoreNativeSize ? returnZero : this._props.childLayoutFitWidth?.(doc) || (this.childFitWidth(doc) && !Doc.NativeHeight(doc)) ? height : undefined} - dontCenter={this._props.childIgnoreNativeSize ? 'xy' : (StrCast(this.layoutDoc.layout_dontCenter) as 'x' | 'y' | 'xy')} + NativeWidth={this._props.childIgnoreNativeSize ? returnZero : this._props.childLayoutFitWidth?.(doc) || (this.childFitWidth(doc) && !Doc.NativeWidth(doc)) ? this.getDocWidth(doc) : undefined} // explicitly ignore nativeWidth/height if childIgnoreNativeSize is set- used by PresBox + NativeHeight={this._props.childIgnoreNativeSize ? returnZero : this._props.childLayoutFitWidth?.(doc) || (this.childFitWidth(doc) && !Doc.NativeHeight(doc)) ? this.getDocHeight(doc) : undefined} + dontCenter={this.dontCenter} dontRegisterView={BoolCast(this.layoutDoc.childDontRegisterViews, this._props.dontRegisterView)} // used to be true if DataDoc existed, but template textboxes won't layout_autoHeight resize if dontRegisterView is set, but they need to. rootSelected={this.rootSelected} showTitle={this._props.childlayout_showTitle} dragAction={(this.layoutDoc.childDragAction ?? this._props.childDragAction) as dropActionType} onClickScript={this.onChildClickHandler} onDoubleClickScript={this.onChildDoubleClickHandler} - ScreenToLocalTransform={stackedDocTransform} + ScreenToLocalTransform={this.getDocTransform(doc)} focus={this.focusDocument} childFilters={this.childDocFilters} hideDecorationTitle={this._props.childHideDecorationTitle} @@ -371,15 +369,18 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection ); } - getDocTransform(doc: Doc) { + getDocTransform = computedFn((doc: Doc) => () => { + // these must be referenced for document decorations to update when the text box container is scrolled + this._scroll; + this._props.ScreenToLocalTransform(); + const dref = this.docRefs.get(doc); - this._scroll; // must be referenced for document decorations to update when the text box container is scrolled const { translateX, translateY, scale } = ClientUtils.GetScreenTransform(dref?.ContentDiv); return new Transform(-translateX + (dref?.centeringX || 0) * scale, -translateY + (dref?.centeringY || 0) * scale, 1) .scale(1 / scale); // prettier-ignore - } - getDocWidth(d?: Doc) { + }); + getDocWidth = computedFn((d?: Doc) => () => { if (!d) return 0; const childLayoutDoc = Doc.Layout(d, this._props.childLayoutTemplate?.()); const maxWidth = this.columnWidth / this.numGroupColumns; @@ -387,12 +388,9 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection return Math.min(NumCast(d._width), maxWidth); } return maxWidth; - } - getDocTransition(d?: Doc) { - if (!d) return ''; - return StrCast(d.dataTransition); - } - getDocHeight(d?: Doc) { + }); + getDocTransition = computedFn((d?: Doc) => () => StrCast(d?.dataTransition)); + getDocHeight = computedFn((d?: Doc) => () => { if (!d || d.hidden) return 0; const childLayoutDoc = Doc.Layout(d, this._props.childLayoutTemplate?.()); const childDataDoc = !d.isTemplateDoc && !d.isTemplateForField ? undefined : this._props.TemplateDataDocument; @@ -401,13 +399,13 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection const nh = Doc.NativeHeight(childLayoutDoc, childDataDoc) || (!this.childFitWidth(childLayoutDoc) ? NumCast(d._height) : 0); if (nw && nh) { const colWid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1); - const docWid = this.layoutDoc._columnsFill ? colWid : Math.min(this.getDocWidth(d), colWid); + const docWid = this.layoutDoc._columnsFill ? colWid : Math.min(this.getDocWidth(d)(), colWid); return Math.min(maxHeight, (docWid * nh) / nw); } const childHeight = NumCast(childLayoutDoc._height); const panelHeight = this.childFitWidth(childLayoutDoc) ? Number.MAX_SAFE_INTEGER : this._props.PanelHeight() - 2 * this.yMargin; return Math.min(childHeight, maxHeight, panelHeight); - } + }); // This following three functions must be from the view Mehek showed columnDividerDown = (e: React.PointerEvent) => { @@ -531,6 +529,9 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection } }); }; + @computed get dontCenter() { + return this._props.dontCenter ?? (this._props.childIgnoreNativeSize ? 'xy' : (StrCast(this.layoutDoc.layout_dontCenter) as 'x' | 'y' | 'xy')); + } headings = () => Array.from(this.Sections); // what a section looks like if we're in stacking view sectionStacking = (heading: SchemaHeaderField | undefined, docList: Doc[]) => { @@ -565,6 +566,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection type={type} createDropTarget={this.createDashEventsTarget} screenToLocalTransform={this.ScreenToLocalBoxXf} + dontCenter={this.dontCenter} /> ); }; diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index 5ae08e535..ed0cabd0a 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -41,6 +41,7 @@ interface CSVFieldColumnProps { columnWidth: number; numGroupColumns: number; gridGap: number; + dontCenter: 'x' | 'xy' | 'y'; type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | undefined; headings: () => object[]; // I think that stacking view actually has a single column and then supposedly you can add more columns? Unsure @@ -345,15 +346,6 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent< <button type="button" className="collectionStackingView-sectionDelete" onClick={this.deleteColumn}> <FontAwesomeIcon icon="trash" size="lg" /> </button> - {/* {evContents === noValueHeader ? null : ( - <div className="collectionStackingView-sectionOptions"> - <Flyout anchorPoint={anchorPoints.TOP_RIGHT} content={this.renderMenu()}> - <button className="collectionStackingView-sectionOptionButton"> - <FontAwesomeIcon icon="ellipsis-v" size="lg"></FontAwesomeIcon> - </button> - </Flyout> - </div> - )} */} </div> <div className={'collectionStackingView-collapseBar' + (this._props.headingObject.collapsed === true ? ' active' : '')} @@ -368,14 +360,20 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent< <> {this._props.Document._columnsHideIfEmpty ? null : headingView} {this.collapsed ? null : ( - <div> + <div + style={{ + margin: 'auto', + marginTop: this._props.dontCenter.includes('y') ? undefined : 'auto', + marginBottom: this._props.dontCenter.includes('y') ? undefined : 'auto', + width: this._props.columnWidth / (uniqueHeadings.length + (this._props.chromeHidden ? 0 : 1) || 1), + }}> <div key={`${heading}-stack`} className="collectionStackingView-masonrySingle" style={{ padding: `${columnYMargin}px ${0}px ${this._props.yMargin}px ${0}px`, - margin: 'auto', - width: 'max-content', // singleColumn ? undefined : `${cols * (style.columnWidth + style.gridGap) + 2 * style.xMargin - style.gridGap}px`, + margin: this._props.dontCenter.includes('x') ? undefined : 'auto', + // width: 'max-content', // singleColumn ? undefined : `${cols * (style.columnWidth + style.gridGap) + 2 * style.xMargin - style.gridGap}px`, height: 'max-content', position: 'relative', gridGap: this._props.gridGap, @@ -419,11 +417,11 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent< const uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx); return ( <div - className={'collectionStackingViewFieldColumn' + (SnappingManager.IsDragging ? 'Dragging' : '')} + className="collectionStackingViewFieldColumn" key={heading} style={{ width: `${100 / (uniqueHeadings.length + (this._props.chromeHidden ? 0 : 1) || 1)}%`, - height: undefined, // DraggingManager.GetIsDragging() ? "100%" : undefined, + height: undefined, background: this._background, }} ref={this.createColumnDropRef} diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index ab93abab6..c9e934448 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -34,6 +34,7 @@ import { CollectionLinearView } from './collectionLinear'; import { CollectionMulticolumnView } from './collectionMulticolumn/CollectionMulticolumnView'; import { CollectionMultirowView } from './collectionMulticolumn/CollectionMultirowView'; import { CollectionSchemaView } from './collectionSchema/CollectionSchemaView'; +import { CalendarBox } from '../nodes/calendarBox/CalendarBox'; @observer export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewProps>() { @@ -91,7 +92,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr if (type === undefined) return null; switch (type) { case CollectionViewType.Schema: return <CollectionSchemaView key="collview" {...props} />; - case CollectionViewType.Calendar: return <CollectionCalendarView key="collview" {...props} />; + case CollectionViewType.Calendar: return <CalendarBox key="collview" {...props} />; case CollectionViewType.Docking: return <CollectionDockingView key="collview" {...props} />; case CollectionViewType.Tree: return <CollectionTreeView key="collview" {...props} />; case CollectionViewType.Multicolumn: return <CollectionMulticolumnView key="collview" {...props} />; @@ -126,6 +127,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr { description: 'Masonry', event: () => func(CollectionViewType.Masonry), icon: 'columns' }, { description: 'Carousel', event: () => func(CollectionViewType.Carousel), icon: 'columns' }, { description: '3D Carousel', event: () => func(CollectionViewType.Carousel3D), icon: 'columns' }, + { description: 'Calendar', event: () => func(CollectionViewType.Calendar), icon: 'columns' }, { description: 'Pivot/Time', event: () => func(CollectionViewType.Time), icon: 'columns' }, { description: 'Map', event: () => func(CollectionViewType.Map), icon: 'globe-americas' }, { description: 'Grid', event: () => func(CollectionViewType.Grid), icon: 'th-list' }, diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index 31b6be927..f56ea9d76 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -57,6 +57,7 @@ interface TabMiniThumbProps { miniLeft: () => number; } +export type TabHTMLElement = HTMLDivElement & { InitTab?: (tab: object) => void }; @observer class TabMiniThumb extends React.Component<TabMiniThumbProps> { render() { @@ -193,8 +194,9 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { .filter(tv => tv._document) .map(tv => tv._document!); } - _mainCont: HTMLDivElement | null = null; + _mainCont: TabHTMLElement | null = null; _tabReaction: IReactionDisposer | undefined; + _lastSelection = 0; // time when view was last selected - used to re-select views that get invalidated when selected /** * Adds a document to the presentation view @@ -273,19 +275,24 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { setTimeout(batch.end, 500); // need to wait until dockingview (goldenlayout) updates all its structurs } + // Flag indicating that when a tab is activated, it should not select it's document. + // this is used by the link properties menu when it wants to display the link target without selecting the target (which would make the link property window go away since it would no longer be selected) + public static DontSelectOnActivate = 'dontSelectOnActivate'; + + public static IsSelected = (doc?: Doc) => { + return DocumentView.getViews(doc).some(dv => dv?.IsSelected); + }; + static Activate = (tabDoc: Doc) => { const tab = Array.from(CollectionDockingView.Instance?.tabMap ?? []).find(findTab => findTab.DashDoc === tabDoc && !findTab.contentItem.config.props.keyValue); tab?.header.parent.setActiveContentItem(tab.contentItem); // glr: Panning does not work when this is set - (this line is for trying to make a tab that is not topmost become topmost) return tab !== undefined; }; - // static ActivateTabView(doc: Doc) { - // const tabView = Array.from(TabDocView._allTabs).find(view => view._document === doc); - // if (!tabView?._activated && tabView?._document) { - // TabDocView.Activate(tabView?._document); - // return tabView; - // } - // return undefined; - // } + + get stack() { return this._props.glContainer.parent.parent; } // prettier-ignore + get tab() { return this._props.glContainer.tab; } // prettier-ignore + get view() { return this._view; } // prettier-ignore + constructor(props: TabDocViewProps) { super(props); makeObservable(this); @@ -299,37 +306,13 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { @observable _hovering = false; @observable _isActive: boolean = false; @observable _isAnyChildContentActive = false; - public static IsSelected = (doc?: Doc) => { - if (DocumentView.getViews(doc).some(dv => dv?.IsSelected)) { - return true; - } - return false; - }; - @computed get _isUserActivated() { - return TabDocView.IsSelected(this._document) || this._isAnyChildContentActive; - } - get _isContentActive() { - return this._isUserActivated || this._hovering; - } @observable _document: Doc | undefined = undefined; @observable _view: DocumentView | undefined = undefined; + @observable _forceInvalidateScreenToLocal = 0; // screentolocal is computed outside of react using a dom resize ovbserver. this hack allows the resize observer to trigger a react update - @computed get layoutDoc() { - return this._document && Doc.Layout(this._document); - } - - get stack() { - return this._props.glContainer.parent.parent; - } - get tab() { - return this._props.glContainer.tab; - } - get view() { - return this._view; - } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - _lastTab: any; - _lastView: DocumentView | undefined; + @computed get layoutDoc() { return this._document && Doc.Layout(this._document); } // prettier-ignore + @computed get isUserActivated() { return TabDocView.IsSelected(this._document) || this._isAnyChildContentActive; } // prettier-ignore + @computed get isContentActive() { return this.isUserActivated || this._hovering; } // prettier-ignore @action // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -413,7 +396,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { color === variant ? DashColor(color) .fade( - this._isUserActivated + this.isUserActivated ? 0 : this._hovering ? 0.25 @@ -522,19 +505,14 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { this._props.glContainer.layoutManager.off('activeContentItemChanged', this.onActiveContentItemChanged); } - // Flag indicating that when a tab is activated, it should not select it's document. - // this is used by the link properties menu when it wants to display the link target without selecting the target (which would make the link property window go away since it would no longer be selected) - public static DontSelectOnActivate = 'dontSelectOnActivate'; - - @action.bound // eslint-disable-next-line @typescript-eslint/no-explicit-any - private onActiveContentItemChanged(contentItem: any) { + onActiveContentItemChanged = action((contentItem: any) => { if (!contentItem || (this.stack === contentItem.parent && ((contentItem?.tab === this.tab && !this._isActive) || (contentItem?.tab !== this.tab && this._isActive)))) { this._activated = this._isActive = !contentItem || contentItem?.tab === this.tab; if (!this._view && this.tab?.contentItem?.config?.props?.panelName !== TabDocView.DontSelectOnActivate) setTimeout(() => DocumentView.SelectView(this._view, false)); !this._isActive && this._document && Doc.UnBrushDoc(this._document); // bcz: bad -- trying to simulate a pointer leave event when a new tab is opened up on top of an existing one. } - } + }); // adds a tab to the layout based on the locaiton parameter which can be: // close[:{left,right,top,bottom}] - e.g., "close" will close the tab, "close:left" will close the left tab, @@ -551,7 +529,6 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { const whereMods = whereFields.length > 1 ? (whereFields[1] as OpenWhereMod) : OpenWhereMod.none; const panelName = whereFields.length > 1 ? whereFields.lastElement() : ''; if (docs[0]?.dockingConfig && !keyValue) return DashboardView.openDashboard(docs[0]); - // prettier-ignore switch (whereFields[0]) { case undefined: case OpenWhere.lightbox: return LightboxView.Instance.AddDocTab(docs[0], location); @@ -559,7 +536,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { case OpenWhere.replace: return CollectionDockingView.ReplaceTab(docs[0], whereMods, this.stack, panelName, undefined, keyValue); case OpenWhere.toggle: return CollectionDockingView.ToggleSplit(docs[0], whereMods, this.stack, TabDocView.DontSelectOnActivate, keyValue); case OpenWhere.add:default:return CollectionDockingView.AddSplit(docs[0], whereMods, this.stack, undefined, keyValue); - } + } // prettier-ignore }; remDocTab = (doc: Doc | Doc[]) => { if (doc === this._document) { @@ -571,8 +548,6 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { }; getCurrentFrame = () => NumCast(Cast(PresBox.Instance.activeItem.presentation_targetDoc, Doc, null)._currentFrame); - - @action focusFunc = () => { if (!this.tab.header.parent._activeContentItem || this.tab.header.parent._activeContentItem !== this.tab.contentItem) { this.tab.header.parent.setActiveContentItem(this.tab.contentItem); // glr: Panning does not work when this is set - (this line is for trying to make a tab that is not topmost become topmost) @@ -580,7 +555,6 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { return undefined; }; active = () => this._isActive; - @observable _forceInvalidateScreenToLocal = 0; ScreenToLocalTransform = () => { this._forceInvalidateScreenToLocal; const { translateX, translateY } = ClientUtils.GetScreenTransform(this._mainCont?.children?.[0] as HTMLElement); @@ -594,47 +568,44 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { whenChildContentActiveChanges = (isActive: boolean) => { this._isAnyChildContentActive = isActive; }; - isContentActive = () => this._isContentActive; + isContentActiveFunc = () => this.isContentActive; waitForDoubleClick = () => (SnappingManager.ExploreMode ? 'never' : undefined); - @computed get docView() { - return !this._activated || !this._document ? null : ( - <> - <DocumentView - key={this._document[Id]} - ref={action((r: DocumentView) => { - this._lastView && DocumentView.removeView(this._lastView); - this._view = r; - this._lastView = this._view; - })} - renderDepth={0} - LayoutTemplateString={this._props.keyValue ? KeyValueBox.LayoutString() : undefined} - hideTitle={this._props.keyValue} - Document={this._document} - TemplateDataDocument={!Doc.AreProtosEqual(this._document[DocData], this._document) ? this._document[DocData] : undefined} - waitForDoubleClickToClick={this.waitForDoubleClick} - isContentActive={this.isContentActive} - isDocumentActive={returnFalse} - PanelWidth={this.PanelWidth} - PanelHeight={this.PanelHeight} - styleProvider={DefaultStyleProvider} - childFilters={CollectionDockingView.Instance?.childDocFilters ?? returnEmptyFilter} - childFiltersByRanges={CollectionDockingView.Instance?.childDocRangeFilters ?? returnEmptyFilter} - searchFilterDocs={CollectionDockingView.Instance?.searchFilterDocs ?? returnEmptyDoclist} - addDocument={undefined} - removeDocument={this.remDocTab} - addDocTab={this.addDocTab} - suppressSetHeight={!!this._document._layout_fitWidth} - ScreenToLocalTransform={this.ScreenToLocalTransform} - dontCenter="y" - whenChildContentsActiveChanged={this.whenChildContentActiveChanges} - focus={this.focusFunc} - containerViewPath={returnEmptyDocViewList} - pinToPres={TabDocView.PinDoc} - /> - {this.disableMinimap() ? null : <TabMinimapView key="minimap" addDocTab={this.addDocTab} PanelHeight={this.PanelHeight} PanelWidth={this.PanelWidth} background={this.miniMapColor} document={this._document} tabView={this.tabView} />} - </> - ); - } + renderDocView = (doc: Doc) => ( + <DocumentView + key={doc[Id]} + ref={action((r: DocumentView) => { + const now = Date.now(); + this._lastSelection = this._view?.IsSelected ? now : this._lastSelection; + if (this._view) DocumentView.removeView(this._view); + this._view = r; + if (this._view && now - this._lastSelection < 1000) this._view.select(false); + })} + renderDepth={0} + LayoutTemplateString={this._props.keyValue ? KeyValueBox.LayoutString() : undefined} + hideTitle={this._props.keyValue} + Document={doc} + TemplateDataDocument={!Doc.AreProtosEqual(doc[DocData], doc) ? doc[DocData] : undefined} + waitForDoubleClickToClick={this.waitForDoubleClick} + isContentActive={this.isContentActiveFunc} + isDocumentActive={returnFalse} + PanelWidth={this.PanelWidth} + PanelHeight={this.PanelHeight} + styleProvider={DefaultStyleProvider} + childFilters={CollectionDockingView.Instance?.childDocFilters ?? returnEmptyFilter} + childFiltersByRanges={CollectionDockingView.Instance?.childDocRangeFilters ?? returnEmptyFilter} + searchFilterDocs={CollectionDockingView.Instance?.searchFilterDocs ?? returnEmptyDoclist} + addDocument={undefined} + removeDocument={this.remDocTab} + addDocTab={this.addDocTab} + suppressSetHeight={!!doc._layout_fitWidth} + ScreenToLocalTransform={this.ScreenToLocalTransform} + dontCenter="y" + whenChildContentsActiveChanged={this.whenChildContentActiveChanges} + focus={this.focusFunc} + containerViewPath={returnEmptyDocViewList} + pinToPres={TabDocView.PinDoc} + /> + ); render() { return ( @@ -647,23 +618,21 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { onPointerLeave={action(() => { this._hovering = false; })} // prettier-ignore onDragOver={action(() => { this._hovering = true; })} // prettier-ignore onDragLeave={action(() => { this._hovering = false; })} // prettier-ignore - ref={ref => { + ref={(ref: TabHTMLElement) => { + // "add" an InitTab function to this div to call from tabCreated in CollectionDockingView when div is reused this._mainCont = ref; if (this._mainCont) { - if (this._lastTab) { - this._view && DocumentView.removeView(this._view); - } - this._lastTab = this.tab; - (this._mainCont as { InitTab?: (tab: object) => void }).InitTab = (tab: object) => this.init(tab, this._document); - DocServer.GetRefField(this._props.documentId).then( - action(doc => { - doc instanceof Doc && (this._document = doc) && this.tab && this.init(this.tab, this._document); - }) - ); - ref && new ResizeObserver(action(() => this._forceInvalidateScreenToLocal++)).observe(ref); + this._mainCont.InitTab = (tab: object) => this.init(tab, this._document); + DocServer.GetRefField(this._props.documentId).then(action(doc => { + doc instanceof Doc && (this._document = doc) && this.tab && this.init(this.tab, this._document); + })); // prettier-ignore + new ResizeObserver(action(() => this._forceInvalidateScreenToLocal++)).observe(this._mainCont); } }}> - {this.docView} + {!this._activated || !this._document ? null : this.renderDocView(this._document)} + {this.disableMinimap() || !this._document ? null : ( + <TabMinimapView key="minimap" addDocTab={this.addDocTab} PanelHeight={this.PanelHeight} PanelWidth={this.PanelWidth} background={this.miniMapColor} document={this._document} tabView={this.tabView} /> + )} </div> ); } diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index b10a521ca..d2514dfd1 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -474,7 +474,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { }; docTransform = () => this.refTransform(this._dref?.ContentDiv); getTransform = () => this.refTransform(this._tref.current); - embeddedPanelWidth = () => this._props.panelWidth() / (this.treeView._props.NativeDimScaling?.() || 1); + embeddedPanelWidth = () => this._props.panelWidth() / (this.treeView._props.NativeDimScaling?.() || 1) - 3 /* paddingRight for bullet */; embeddedPanelHeight = () => { const layoutDoc = (temp => temp && Doc.expandTemplateLayout(temp, this.Document))(this.treeView._props.childLayoutTemplate?.()) || this.layoutDoc; return Math.min( diff --git a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx index 717081666..534f67927 100644 --- a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx +++ b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx @@ -179,7 +179,7 @@ export class UniqueFaceBox extends ViewBoxBaseComponent<FieldViewProps>() { ele?.addEventListener('wheel', this.onPassiveWheel, { passive: false }); })}> {FaceRecognitionHandler.UniqueFaceImages(this.Document).map((doc, i) => { - const [name, type] = ImageCast(doc[Doc.LayoutFieldKey(doc)]).url.href.split('.'); + const [name, type] = ImageCast(doc[Doc.LayoutFieldKey(doc)])?.url.href.split('.') ?? ['-missing-', '.png']; return ( <div className="image-wrapper" @@ -264,6 +264,7 @@ export class FaceCollectionBox extends ViewBoxBaseComponent<FieldViewProps>() { isContentActive={returnTrue} isAnyChildContentActive={returnTrue} childHideDecorations={true} + dontCenter="y" /> </div> ); diff --git a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx index 94ec59ecb..753685b97 100644 --- a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx +++ b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx @@ -139,9 +139,9 @@ export class ImageLabelBox extends ViewBoxBaseComponent<FieldViewProps>() { toggleDisplayInformation = () => { this._displayImageInformation = !this._displayImageInformation; if (this._displayImageInformation) { - this._selectedImages.forEach(doc => (doc[DocData].showTags = true)); + this._selectedImages.forEach(doc => (doc._layout_showTags = true)); } else { - this._selectedImages.forEach(doc => (doc[DocData].showTags = false)); + this._selectedImages.forEach(doc => (doc._layout_showTags = false)); } }; diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss index 06d78c39e..9ed247d50 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss @@ -1,5 +1,6 @@ .collectionMulticolumnView_drop { height: 100%; + width: 100%; top: 0; left: 0; position: absolute; diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss index 0d49fabaa..91779065d 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss +++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss @@ -1,5 +1,6 @@ .collectionMultirowView_drop { height: 100%; + width: 100%; top: 0; left: 0; position: absolute; diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts index 2c7920bdd..934ed6c3e 100644 --- a/src/client/views/global/globalScripts.ts +++ b/src/client/views/global/globalScripts.ts @@ -133,10 +133,10 @@ ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) { }); // eslint-disable-next-line prefer-arrow-callback -ScriptingGlobals.add(function showFreeform(attr: 'center' | 'grid' | 'snaplines' | 'clusters' | 'viewAll' | 'fitOnce', checkResult?: boolean, persist?: boolean) { +ScriptingGlobals.add(function showFreeform(attr: 'hcenter' | 'vcenter' | 'grid' | 'snaplines' | 'clusters' | 'viewAll' | 'fitOnce', checkResult?: boolean, persist?: boolean) { const selected = DocumentView.SelectedDocs().lastElement(); // prettier-ignore - const map: Map<'flashcards' | 'center' | 'grid' | 'snaplines' | 'clusters' | 'arrange' | 'viewAll' | 'fitOnce' | 'time' | 'docType' | 'color' | 'links' | 'like' | 'star' | 'idea' | 'chat' | '1' | '2' | '3' | '4', + const map: Map<'flashcards' | 'hcenter' | 'vcenter' | 'grid' | 'snaplines' | 'clusters' | 'arrange' | 'viewAll' | 'fitOnce' | 'time' | 'docType' | 'color' | 'links' | 'like' | 'star' | 'idea' | 'chat' | '1' | '2' | '3' | '4', { waitForRender?: boolean; checkResult: (doc: Doc) => boolean; @@ -158,9 +158,13 @@ ScriptingGlobals.add(function showFreeform(attr: 'center' | 'grid' | 'snaplines' else (dv.ComponentView as CollectionFreeFormView)?.fitContentOnce(); }, }], - ['center', { - checkResult: (doc:Doc) => BoolCast(doc?._stacking_alignCenter, false), - setDoc: (doc:Doc) => { doc._stacking_alignCenter = !doc._stacking_alignCenter; }, + ['vcenter', { + checkResult: (doc:Doc) => !StrCast(doc?._layout_dontCenter).includes('y'), + setDoc: (doc:Doc) => { doc._layout_dontCenter = StrCast(doc.layout_dontCenter).includes('y') ? StrCast(doc.layout_dontCenter).replace(/y/,"") : StrCast(doc.layout_dontCenter) + 'y'; }, + }], + ['hcenter', { + checkResult: (doc:Doc) => !StrCast(doc?._layout_dontCenter).includes('x'), + setDoc: (doc:Doc) => { doc._layout_dontCenter = StrCast(doc.layout_dontCenter).includes('x') ? StrCast(doc.layout_dontCenter).replace(/x/,"") : 'x'+ StrCast(doc.layout_dontCenter); }, }], ['clusters', { waitForRender: true, // flags that undo batch should terminate after a re-render giving the script the chance to fire diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index 2d4bba8d1..a410f4994 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -4,7 +4,7 @@ import { Tooltip } from '@mui/material'; import { action, computed, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { returnFalse, returnNone, returnTrue, setupMoveUpEvents } from '../../../ClientUtils'; +import { returnFalse, returnNone, returnTrue, returnZero, setupMoveUpEvents } from '../../../ClientUtils'; import { emptyFunction } from '../../../Utils'; import { Doc, Opt } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; @@ -669,6 +669,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() <DocumentView // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} + fitWidth={undefined} + NativeHeight={returnZero} + NativeWidth={returnZero} ignoreUsePath={layoutString ? true : undefined} renderDepth={this.props.renderDepth + 1} LayoutTemplateString={layoutString} @@ -676,8 +679,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() containerViewPath={this._props.docViewPath} moveDocument={whichSlot.endsWith('1') ? this.moveDoc1 : this.moveDoc2} removeDocument={whichSlot.endsWith('1') ? this.remDoc1 : this.remDoc2} - NativeWidth={this.layoutWidth} - NativeHeight={this.layoutHeight} isContentActive={() => this.childActive} isDocumentActive={returnFalse} dontSelect={returnTrue} diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index c6ebf7d4a..b0033ef39 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -83,6 +83,7 @@ export interface FieldViewSharedProps { // eslint-disable-next-line no-use-before-define onKey?: (e: React.KeyboardEvent, fieldProps: FieldViewProps) => boolean | undefined; fitWidth?: (doc: Doc) => boolean | undefined; + dontCenter?: 'x' | 'y' | 'xy' | undefined; searchFilterDocs: () => Doc[]; showTitle?: () => string; whenChildContentsActiveChanged: (isActive: boolean) => void; diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx index f2f7f39bb..7a09ad9e2 100644 --- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx @@ -345,7 +345,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { @computed get editableText() { const script = ScriptCast(this.Document.script); - const checkResult = script?.script.run({ this: this.Document, value: '', _readOnly_: true }).result; + const checkResult = script?.script.run({ this: this.Document, value: '', _readOnly_: true }).result as string; const setValue = (value: string) => script?.script.run({ this: this.Document, value, _readOnly_: false }).result as boolean; diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 06e7e576b..ed79888ae 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,24 +1,29 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; +import axios from 'axios'; import { Colors } from 'browndash-components'; import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction } from 'mobx'; import { observer } from 'mobx-react'; import { extname } from 'path'; import * as React from 'react'; +import ReactLoading from 'react-loading'; import { ClientUtils, DashColor, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents } from '../../../ClientUtils'; import { Doc, DocListCast, Opt } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { ObjectField } from '../../../fields/ObjectField'; +import { Cast, ImageCast, NumCast, RTFCast, StrCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; import { emptyFunction } from '../../../Utils'; +import { gptAPICall, GPTCallType, gptImageLabel } from '../../apis/gpt/GPT'; import { Docs } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; -import { DocUtils } from '../../documents/DocUtils'; +import { DocUtils, FollowLinkScript } from '../../documents/DocUtils'; import { Networking } from '../../Network'; import { DragManager } from '../../util/DragManager'; +import { dropActionType } from '../../util/DropActionTypes'; import { SnappingManager } from '../../util/SnappingManager'; import { undoBatch } from '../../util/UndoManager'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; @@ -33,20 +38,9 @@ import { StyleProp } from '../StyleProp'; import { DocumentView } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import { FocusViewOptions } from './FocusViewOptions'; -import { DocCast, NumCast, RTFCast, StrCast, ImageCast, Cast, toList } from '../../../fields/Types'; +import { ImageUtility } from './generativeFill/generativeFillUtils/ImageHandler'; import './ImageBox.scss'; import { OpenWhere } from './OpenWhere'; -import { URLField } from '../../../fields/URLField'; -import { gptAPICall, GPTCallType, gptImageLabel } from '../../apis/gpt/GPT'; -import ReactLoading from 'react-loading'; -import { FollowLinkScript } from '../../documents/DocUtils'; -import { basename } from 'path'; -import { ImageUtility } from './generativeFill/generativeFillUtils/ImageHandler'; -import { dropActionType } from '../../util/DropActionTypes'; -import { canvasSize } from './generativeFill/generativeFillUtils/generativeFillConstants'; -import Tesseract from 'tesseract.js'; -import axios from 'axios'; -import { TupleType } from 'typescript'; // import stringSimilarity from 'string-similarity'; enum quizMode { @@ -239,8 +233,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { @undoBatch setNativeSize = action(() => { + const oldnativeWidth = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']); const nscale = NumCast(this._props.PanelWidth()) * NumCast(this.layoutDoc._freeform_scale, 1); - const nw = nscale / NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']); + const nw = nscale / oldnativeWidth; this.dataDoc[this.fieldKey + '_nativeHeight'] = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']) * nw; this.dataDoc[this.fieldKey + '_nativeWidth'] = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']) * nw; this.dataDoc._freeform_panX = nw * NumCast(this.dataDoc._freeform_panX); @@ -249,7 +244,15 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { this.dataDoc._freeform_panX_min = this.dataDoc._freeform_panX_min ? nw * NumCast(this.dataDoc._freeform_panX_min) : undefined; this.dataDoc._freeform_panY_max = this.dataDoc._freeform_panY_max ? nw * NumCast(this.dataDoc._freeform_panY_max) : undefined; this.dataDoc._freeform_panY_min = this.dataDoc._freeform_panY_min ? nw * NumCast(this.dataDoc._freeform_panY_min) : undefined; - return nw; + const newnativeWidth = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']); + DocListCast(this.dataDoc[this.annotationKey]).forEach(doc => { + doc.x = (NumCast(doc.x) / oldnativeWidth) * newnativeWidth; + doc.y = (NumCast(doc.y) / oldnativeWidth) * newnativeWidth; + if (!RTFCast(doc[Doc.LayoutFieldKey(doc)])) { + doc.width = (NumCast(doc.width) / oldnativeWidth) * newnativeWidth; + doc.height = (NumCast(doc.height) / oldnativeWidth) * newnativeWidth; + } + }); }); @undoBatch rotate = action(() => { diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index 95e344004..3daacc9bb 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -84,7 +84,7 @@ export class KeyValueBox extends ViewBoxBaseComponent<FieldViewProps>() { const onDelegate = rawvalue.startsWith('='); rawvalue = onDelegate ? rawvalue.substring(1) : rawvalue; const type: 'computed' | 'script' | false = rawvalue.startsWith(':=') ? 'computed' : rawvalue.startsWith('$=') ? 'script' : false; - rawvalue = type ? rawvalue.substring(2) : rawvalue; + rawvalue = type ? rawvalue.substring(2) : rawvalue.replace(/^:/, ''); rawvalue = rawvalue.replace(/.*\(\((.*)\)\)/, 'dashCallChat(_setCacheResult_, this, `$1`)'); const value = ["'", '"', '`'].includes(rawvalue.length ? rawvalue[0] : '') || !isNaN(+rawvalue) ? rawvalue : '`' + rawvalue + '`'; diff --git a/src/client/views/nodes/calendarBox/CalendarBox.scss b/src/client/views/nodes/calendarBox/CalendarBox.scss new file mode 100644 index 000000000..f8ac4b2d1 --- /dev/null +++ b/src/client/views/nodes/calendarBox/CalendarBox.scss @@ -0,0 +1,25 @@ +.calendarBox { + display: flex; + width: 100%; + height: 100%; + transform-origin: top left; + .calendarBox-wrapper { + width: 100%; + height: 100%; + .fc-timegrid-body { + width: 100% !important; + table { + width: 100% !important; + } + } + .fc-col-header { + width: 100% !important; + } + .fc-daygrid-body { + width: 100% !important; + .fc-scrollgrid-sync-table { + width: 100% !important; + } + } + } +} diff --git a/src/client/views/nodes/calendarBox/CalendarBox.tsx b/src/client/views/nodes/calendarBox/CalendarBox.tsx index bd66941c3..678b7dd0b 100644 --- a/src/client/views/nodes/calendarBox/CalendarBox.tsx +++ b/src/client/views/nodes/calendarBox/CalendarBox.tsx @@ -1,127 +1,207 @@ -import { Calendar, EventSourceInput } from '@fullcalendar/core'; +import { Calendar, DateInput, EventClickArg, EventSourceInput } from '@fullcalendar/core'; import dayGridPlugin from '@fullcalendar/daygrid'; import multiMonthPlugin from '@fullcalendar/multimonth'; -import { makeObservable } from 'mobx'; +import timeGrid from '@fullcalendar/timegrid'; +import interactionPlugin from '@fullcalendar/interaction'; +import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { dateRangeStrToDates } from '../../../../ClientUtils'; import { Doc } from '../../../../fields/Doc'; -import { StrCast } from '../../../../fields/Types'; -import { DocumentType } from '../../../documents/DocumentTypes'; -import { Docs } from '../../../documents/Documents'; -import { ViewBoxBaseComponent } from '../../DocComponent'; -import { FieldView, FieldViewProps } from '../FieldView'; - -type CalendarView = 'month' | 'multi-month' | 'week'; +import { BoolCast, NumCast, StrCast } from '../../../../fields/Types'; +import { CollectionSubView, SubCollectionViewProps } from '../../collections/CollectionSubView'; +import './CalendarBox.scss'; +import { Id } from '../../../../fields/FieldSymbols'; +import { DocServer } from '../../../DocServer'; +import { DocumentView } from '../DocumentView'; +import { OpenWhere } from '../OpenWhere'; +import { DragManager } from '../../../util/DragManager'; +import { DocData } from '../../../../fields/DocSymbols'; + +type CalendarView = 'multiMonth' | 'dayGridMonth' | 'timeGridWeek' | 'timeGridDay'; @observer -export class CalendarBox extends ViewBoxBaseComponent<FieldViewProps>() { - public static LayoutString(fieldKey: string = 'calendar') { - return FieldView.LayoutString(CalendarBox, fieldKey); - } - - constructor(props: FieldViewProps) { +export class CalendarBox extends CollectionSubView() { + _calendarRef: HTMLDivElement | null = null; + _calendar: Calendar | undefined; + _oldWheel: HTMLElement | null = null; + _observer: ResizeObserver | undefined; + _eventsDisposer: IReactionDisposer | undefined; + _selectDisposer: IReactionDisposer | undefined; + + constructor(props: SubCollectionViewProps) { super(props); makeObservable(this); } - componentDidMount(): void {} - - componentWillUnmount(): void {} - - _calendarRef = React.createRef<HTMLElement>(); + @observable _multiMonth = 0; + isMultiMonth: boolean | undefined; - get dateRangeStr() { - return StrCast(this.Document.date_range); + componentDidMount(): void { + this._props.setContentViewBox?.(this); + this._eventsDisposer = reaction( + () => ({ events: this.calendarEvents }), + ({ events }) => this._calendar?.setOption('events', events), + { fireImmediately: true } + ); + this._selectDisposer = reaction( + () => ({ initialDate: this.dateSelect }), + ({ initialDate }) => { + const state = this._calendar?.getCurrentData(); + state && + this._calendar?.dispatch({ + type: 'CHANGE_DATE', + dateMarker: state.dateEnv.createMarker(initialDate.start), + }); + setTimeout(() => (initialDate.start.toISOString() !== initialDate.end.toISOString() ? this._calendar?.select(initialDate.start, initialDate.end) : this._calendar?.select(initialDate.start))); + }, + { fireImmediately: true } + ); } - - // Choose a calendar view based on the date range - get calendarViewType(): CalendarView { - const [fromDate, toDate] = dateRangeStrToDates(this.dateRangeStr); - - if (fromDate.getFullYear() !== toDate.getFullYear() || fromDate.getMonth() !== toDate.getMonth()) return 'multi-month'; - - if (Math.abs(fromDate.getDay() - toDate.getDay()) > 7) return 'month'; - return 'week'; + componentWillUnmount(): void { + this._eventsDisposer?.(); + this._selectDisposer?.(); } - get calendarStartDate() { - return this.dateRangeStr.split('|')[0]; + @computed get calendarEvents(): EventSourceInput | undefined { + return this.childDocs.map(doc => { + const { start, end } = dateRangeStrToDates(StrCast(doc.date_range)); + return { + title: StrCast(doc.title), + start, + end, + groupId: doc[Id], + startEditable: true, + endEditable: true, + allDay: BoolCast(doc.allDay), + classNames: ['mother'], // will determine the style + editable: true, // subject to change in the future + backgroundColor: this.eventToColor(doc), + borderColor: this.eventToColor(doc), + color: 'white', + extendedProps: { + description: StrCast(doc.description), + }, + }; + }); } - get calendarToDate() { - return this.dateRangeStr.split('|')[1]; + @computed get dateRangeStrDates() { + return dateRangeStrToDates(StrCast(this.Document.date_range)); } - - get childDocs(): Doc[] { - return this.childDocs; // get all sub docs for a calendar + get dateSelect() { + return dateRangeStrToDates(StrCast(this.Document.date)); } - docBackgroundColor(type: string): string { - // TODO: Return a different color based on the event type - console.log(type); - return 'blue'; + // Choose a calendar view based on the date range + @computed get calendarViewType(): CalendarView { + if (this.dataDoc[this.fieldKey + '_calendarType']) return StrCast(this.dataDoc[this.fieldKey + '_calendarType']) as CalendarView; + if (this.isMultiMonth) return 'multiMonth'; + const { start, end } = this.dateRangeStrDates; + if (start.getFullYear() !== end.getFullYear() || start.getMonth() !== end.getMonth()) return 'multiMonth'; + if (Math.abs(start.getDay() - end.getDay()) > 7) return 'dayGridMonth'; + return 'timeGridWeek'; } - get calendarEvents(): EventSourceInput | undefined { - if (this.childDocs.length === 0) return undefined; - return this.childDocs.map(doc => { - const docTitle = StrCast(doc.title); - const docDateRange = StrCast(doc.date_range); - const [startDate, endDate] = dateRangeStrToDates(docDateRange); - const docType = doc.type; - const docDescription = doc.description ? StrCast(doc.description) : ''; + // TODO: Return a different color based on the event type + eventToColor(event: Doc): string { + return 'red'; + } - return { - title: docTitle, - start: startDate, - end: endDate, - allDay: false, - classNames: [StrCast(docType)], // will determine the style - editable: false, // subject to change in the future - backgroundColor: this.docBackgroundColor(StrCast(doc.type)), - color: 'white', - extendedProps: { - description: docDescription, - }, - }; + internalDocDrop(e: Event, de: DragManager.DropEvent, docDragData: DragManager.DocumentDragData) { + if (!super.onInternalDrop(e, de)) return false; + de.complete.docDragData?.droppedDocuments.forEach(doc => { + const today = new Date().toISOString(); + if (!doc.date_range) doc[DocData].date_range = `${today}|${today}`; }); + return true; } - handleEventClick = (/* arg: EventClickArg */) => { - // TODO: open popover with event description, option to open CalendarManager and change event date, delete event, etc. + onInternalDrop = (e: Event, de: DragManager.DropEvent): boolean => { + if (de.complete.docDragData?.droppedDocuments.length) return this.internalDocDrop(e, de, de.complete.docDragData); + return false; }; - calendarEl: HTMLElement = document.getElementById('calendar-box-v1')!; + handleEventClick = (arg: EventClickArg) => { + const doc = DocServer.GetCachedRefField(arg.event._def.groupId ?? ''); + DocumentView.DeselectAll(); + if (doc) { + DocumentView.showDocument(doc, { openLocation: OpenWhere.lightboxAlways }); + arg.jsEvent.stopPropagation(); + } + }; // https://fullcalendar.io - get calendar() { - return new Calendar(this.calendarEl, { - plugins: [this.calendarViewType === 'multi-month' ? multiMonthPlugin : dayGridPlugin], - headerToolbar: { - left: 'prev,next today', - center: 'title', - right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek', - }, - initialDate: this.calendarStartDate, - navLinks: true, - editable: false, - displayEventTime: false, - displayEventEnd: false, - events: this.calendarEvents, - eventClick: this.handleEventClick, - }); - } + renderCalendar = () => { + const cal = !this._calendarRef + ? null + : (this._calendar = new Calendar(this._calendarRef, { + plugins: [multiMonthPlugin, dayGridPlugin, timeGrid, interactionPlugin], + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'multiMonth dayGridMonth timeGridWeek timeGridDay', + }, + selectable: true, + initialView: this.calendarViewType === 'multiMonth' ? undefined : this.calendarViewType, + initialDate: this.dateSelect.start, + navLinks: true, + editable: false, + displayEventTime: false, + displayEventEnd: false, + select: info => { + const start = dateRangeStrToDates(info.startStr).start.toISOString(); + const end = dateRangeStrToDates(info.endStr).start.toISOString(); + this.dataDoc.date = start + '|' + end; + }, + aspectRatio: NumCast(this.Document.width) / NumCast(this.Document.height), + events: this.calendarEvents, + eventClick: this.handleEventClick, + })); + cal?.render(); + setTimeout(() => cal?.view.calendar.select(this.dateSelect.start, this.dateSelect.end)); + }; + onPassiveWheel = (e: WheelEvent) => e.stopPropagation(); render() { return ( - <div className="calendar-box-conatiner"> - <div id="calendar-box-v1" /> + <div + key={this.calendarViewType} + className="calendarBox" + onPointerDown={e => { + setTimeout( + action(() => { + const cname = (e.nativeEvent.target as HTMLButtonElement)?.className ?? ''; + if (cname.includes('multiMonth')) this.dataDoc[this.fieldKey + '_calendarType'] = 'multiMonth'; + if (cname.includes('dayGridMonth')) this.dataDoc[this.fieldKey + '_calendarType'] = 'dayGridMonth'; + if (cname.includes('timeGridWeek')) this.dataDoc[this.fieldKey + '_calendarType'] = 'timeGridWeek'; + if (cname.includes('timeGridDay')) this.dataDoc[this.fieldKey + '_calendarType'] = 'timeGridDay'; + }) + ); + }} + style={{ + width: this._props.PanelWidth() / this._props.ScreenToLocalTransform().Scale, + height: this._props.PanelHeight() / this._props.ScreenToLocalTransform().Scale, + transform: `scale(${this._props.ScreenToLocalTransform().Scale})`, + }} + ref={r => { + this.createDashEventsTarget(r); + this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel); + this._oldWheel = r; + // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling + r?.addEventListener('wheel', this.onPassiveWheel, { passive: false }); + + if (r) { + this._observer?.disconnect(); + (this._observer = new ResizeObserver(() => { + this._calendar?.setOption('aspectRatio', NumCast(this.Document.width) / NumCast(this.Document.height)); + this._calendar?.updateSize(); + })).observe(r); + this.renderCalendar(); + } + }}> + <div className="calendarBox-wrapper" ref={r => (this._calendarRef = r)} /> </div> ); } } -Docs.Prototypes.TemplateMap.set(DocumentType.CALENDAR, { - layout: { view: CalendarBox, dataField: 'data' }, - options: { acl: '' }, -}); diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 73b20e6c2..a281479f1 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -2117,7 +2117,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB style={{ cursor: this._props.isContentActive() ? 'text' : undefined, height: this._props.height ? 'max-content' : undefined, - overflow: this.layout_autoHeight ? 'hidden' : undefined, pointerEvents: Doc.ActiveTool === InkTool.None && !SnappingManager.ExploreMode ? undefined : 'none', }} onContextMenu={this.specificContextMenu} @@ -2136,7 +2135,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }} style={{ width: this.noSidebar ? '100%' : `calc(100% - ${this.layout_sidebarWidthPercent})`, - overflow: this.layoutDoc._createDocOnCR ? 'hidden' : this.layoutDoc._layout_autoHeight ? 'visible' : undefined, + overflow: this.layoutDoc._createDocOnCR || this.layoutDoc._layout_hideScroll ? 'hidden' : this.layout_autoHeight ? 'visible' : undefined, }} onScroll={this.onScroll} onDrop={this.ondrop}> diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 738f6d699..3ec799836 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -1,6 +1,6 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; -import { action, computed, IReactionDisposer, makeObservable, observable } from 'mobx'; +import { action, computed, IReactionDisposer, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { lift, toggleMark, wrapIn } from 'prosemirror-commands'; import { Mark, MarkType } from 'prosemirror-model'; @@ -68,7 +68,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { constructor(props: AntimodeMenuProps) { super(props); makeObservable(this); - RichTextMenu._instance.menu = this; + runInAction(() => (RichTextMenu._instance.menu = this)); this.updateMenu(undefined, undefined, props, this.layoutDoc); this._canFade = false; this.Pinned = true; diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index e0d6c7c05..0ef67b4be 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -404,7 +404,7 @@ export class RichTextRules { if (!tags.includes(tag)) { tags.push(tag); this.Document[DocData].tags = new List<string>(tags); - this.Document[DocData].showTags = true; + this.Document._layout_showTags = true; } const fieldView = state.schema.nodes.dashField.create({ fieldKey: '#' + tag }); return state.tr diff --git a/src/client/views/search/FaceRecognitionHandler.tsx b/src/client/views/search/FaceRecognitionHandler.tsx index 4f6f5d314..c507e54b6 100644 --- a/src/client/views/search/FaceRecognitionHandler.tsx +++ b/src/client/views/search/FaceRecognitionHandler.tsx @@ -119,8 +119,7 @@ export class FaceRecognitionHandler { * @param faceDoc - unique face Doc */ public static UniqueFaceRemoveFaceImage = (faceAnno: Doc, faceDoc: Doc) => { - Doc.RemoveDocFromList(faceDoc[DocData], 'face_annos', faceAnno); - faceAnno.face = undefined; + FaceRecognitionHandler.ImageDocFaceAnnos(faceAnno).forEach(face => Doc.RemoveDocFromList(faceDoc[DocData], 'face_annos', face) && (face.face = undefined)); }; constructor() { @@ -221,6 +220,7 @@ export class FaceRecognitionHandler { .then(imgDocFaceDescriptions => { // For each face detected, find a match. const annos = [] as Doc[]; const scale = NumCast(imgDoc.data_nativeWidth) / img.width; + const showTags= imgDocFaceDescriptions.length > 1; imgDocFaceDescriptions.forEach((fd, i) => { const faceDescriptor = new List<number>(Array.from(fd.descriptor)); const matchedUniqueFace = this.findMatchingFaceDoc(fd.descriptor) ?? this.createUniqueFaceDoc(activeDashboard); @@ -234,12 +234,14 @@ export class FaceRecognitionHandler { y: fd.alignedRect.box.top * scale, _width: fd.alignedRect.box.width * scale, _height: fd.alignedRect.box.height * scale, + _layout_showTags: showTags }) FaceRecognitionHandler.UniqueFaceAddFaceImage(faceAnno, matchedUniqueFace); // add image/faceDescriptor to matched unique face annos.push(faceAnno); }); imgDoc[DocData].data_annotations = new List<Doc>(annos); + imgDoc._layout_showTags = annos.length > 0; return imgDocFaceDescriptions; }) ); // prettier-ignore diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 7abba7679..29903cfb5 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -24,6 +24,7 @@ import { listSpec } from './Schema'; import { ComputedField, ScriptField } from './ScriptField'; import { BoolCast, Cast, DocCast, FieldValue, NumCast, StrCast, ToConstructor, toList } from './Types'; import { containedFieldChangedHandler, deleteProperty, GetEffectiveAcl, getField, getter, makeEditable, makeReadOnly, setter, SharingPermissions } from './util'; +import { KEY } from 'google-maps'; export let ObjGetRefField: (id: string, force?: boolean) => Promise<Doc | undefined>; export let ObjGetRefFields: (ids: string[]) => Promise<Map<string, Doc | undefined>>; @@ -1360,6 +1361,13 @@ export namespace Doc { export const FilterAny = '--any--'; export const FilterNone = '--undefined--'; + export function hasDocFilter(container: Opt<Doc>, key: string, fieldPrefix?: string) { + if (!container) return; + const filterField = '_' + (fieldPrefix ? fieldPrefix + '_' : '') + 'childFilters'; + const childFilters = StrListCast(container[filterField]); + return childFilters.some(filter => filter.split(FilterSep)[0] === key); + } + // filters document in a container collection: // all documents with the specified value for the specified key are included/excluded // based on the modifiers :"check", "x", undefined |