diff options
Diffstat (limited to 'src')
190 files changed, 9359 insertions, 10421 deletions
diff --git a/src/.DS_Store b/src/.DS_Store Binary files differindex 299b902c6..02618014e 100644 --- a/src/.DS_Store +++ b/src/.DS_Store diff --git a/src/Utils.ts b/src/Utils.ts index d9a5353e8..cc7ee9537 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -414,6 +414,8 @@ export function returnTrue() { return true; } export function returnFalse() { return false; } +export function returnVal(val1?: number, val2?: number) { return val1 !== undefined ? val1 : val2 !== undefined ? val2 : 0; } + export function returnOne() { return 1; } export function returnZero() { return 0; } @@ -422,6 +424,8 @@ export function returnEmptyString() { return ""; } export function returnEmptyFilter() { return [] as string[]; } +export function returnEmptyDoclist() { return [] as any[]; } + export let emptyPath = []; export function emptyFunction() { } @@ -514,7 +518,8 @@ export function clearStyleSheetRules(sheet: any) { return false; } -export function simulateMouseClick(element: Element, x: number, y: number, sx: number, sy: number, rightClick = true) { +export function simulateMouseClick(element: Element | null | undefined, x: number, y: number, sx: number, sy: number, rightClick = true) { + if (!element) return; ["pointerdown", "pointerup"].map(event => element.dispatchEvent( new PointerEvent(event, { view: window, @@ -547,8 +552,8 @@ export function setupMoveUpEvents( target: object, e: React.PointerEvent, moveEvent: (e: PointerEvent, down: number[], delta: number[]) => boolean, - upEvent: (e: PointerEvent, movement: number[]) => void, - clickEvent: (e: PointerEvent, doubleTap?: boolean) => void, + upEvent: (e: PointerEvent, movement: number[]) => any, + clickEvent: (e: PointerEvent, doubleTap?: boolean) => any, stopPropagation: boolean = true, stopMovePropagation: boolean = true ) { diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index dde75497c..d7dfa4498 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -5,7 +5,7 @@ import { Utils, emptyFunction } from '../Utils'; import { SerializationHelper } from './util/SerializationHelper'; import { RefField } from '../fields/RefField'; import { Id, HandleUpdate, Parent } from '../fields/FieldSymbols'; -import GestureOverlay from './views/GestureOverlay'; +import { GestureOverlay } from './views/GestureOverlay'; import MobileInkOverlay from '../mobile/MobileInkOverlay'; import { runInAction } from 'mobx'; import { ObjectField } from '../fields/ObjectField'; @@ -42,7 +42,7 @@ export namespace DocServer { export var PlaygroundFields: string[]; export function setPlaygroundFields(livePlaygroundFields: string[]) { DocServer.PlaygroundFields = livePlaygroundFields; - livePlaygroundFields.forEach(f => DocServer.setFieldWriteMode(f, DocServer.WriteMode.LivePlayground)); + livePlaygroundFields.forEach(f => DocServer.setFieldWriteMode(f, DocServer.WriteMode.Playground)); } export function setFieldWriteMode(field: string, writeMode: WriteMode) { diff --git a/src/client/apis/GoogleAuthenticationManager.tsx b/src/client/apis/GoogleAuthenticationManager.tsx index 117d1fa1e..cda108058 100644 --- a/src/client/apis/GoogleAuthenticationManager.tsx +++ b/src/client/apis/GoogleAuthenticationManager.tsx @@ -1,17 +1,17 @@ -import { observable, action, reaction, runInAction, IReactionDisposer } from "mobx"; +import { action, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import * as React from "react"; -import MainViewModal from "../views/MainViewModal"; import { Opt } from "../../fields/Doc"; import { Networking } from "../Network"; -import "./GoogleAuthenticationManager.scss"; import { Scripting } from "../util/Scripting"; +import { MainViewModal } from "../views/MainViewModal"; +import "./GoogleAuthenticationManager.scss"; const AuthenticationUrl = "https://accounts.google.com/o/oauth2/v2/auth"; const prompt = "Paste authorization code here..."; @observer -export default class GoogleAuthenticationManager extends React.Component<{}> { +export class GoogleAuthenticationManager extends React.Component<{}> { public static Instance: GoogleAuthenticationManager; private authenticationLink: Opt<string> = undefined; @observable private openState = false; diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts index 92eaf2e73..899e65a16 100644 --- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts +++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts @@ -11,7 +11,7 @@ import { Utils } from "../../../Utils"; import { Docs, DocumentOptions, DocUtils } from "../../documents/Documents"; import { Networking } from "../../Network"; import { FormattedTextBox } from "../../views/nodes/formattedText/FormattedTextBox"; -import GoogleAuthenticationManager from "../GoogleAuthenticationManager"; +import { GoogleAuthenticationManager } from "../GoogleAuthenticationManager"; import Photos = require('googlephotos'); export namespace GooglePhotos { diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts index 1bef6fa08..37a148e55 100644 --- a/src/client/documents/DocumentTypes.ts +++ b/src/client/documents/DocumentTypes.ts @@ -13,7 +13,8 @@ export enum DocumentType { INK = "inks", // ink stroke SCREENSHOT = "screenshot", // view of a desktop application FONTICON = "fonticonbox", // font icon - SEARCH = "search", // search query + FILTER = "filter", + SEARCH = "search", // search query LABEL = "label", // simple text label BUTTON = "button", // onClick button WEBCAM = "webcam", // webcam @@ -32,10 +33,10 @@ export enum DocumentType { YOUTUBE = "youtube", // youtube directory (view of you tube search results) DOCHOLDER = "docholder", // nested document (view of a document) SEARCHITEM = "searchitem", - COMPARISON = "comparison", // before/after view with slider (view of 2 images) + COMPARISON = "comparison", // before/after view with slider (view of 2 images) GROUP = "group", // group of users LINKDB = "linkdb", // database of links ??? why do we have this - SCRIPTDB = "scriptdb", // database of scripts + SCRIPTDB = "scriptdb", // database of scripts GROUPDB = "groupdb" // database of groups }
\ No newline at end of file diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 3316e6b48..47fa7067d 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -1,7 +1,8 @@ -import { runInAction, action } from "mobx"; -import { extname, basename } from "path"; +import { runInAction } from "mobx"; +import { basename, extname } from "path"; import { DateField } from "../../fields/DateField"; import { Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym } from "../../fields/Doc"; +import { Id } from "../../fields/FieldSymbols"; import { HtmlField } from "../../fields/HtmlField"; import { InkField } from "../../fields/InkField"; import { List } from "../../fields/List"; @@ -11,29 +12,31 @@ import { SchemaHeaderField } from "../../fields/SchemaHeaderField"; import { ComputedField, ScriptField } from "../../fields/ScriptField"; import { Cast, NumCast, StrCast } from "../../fields/Types"; import { AudioField, ImageField, PdfField, VideoField, WebField, YoutubeField } from "../../fields/URLField"; +import { SharingPermissions } from "../../fields/util"; import { MessageStore } from "../../server/Message"; +import { Upload } from "../../server/SharedMediaTypes"; import { OmitKeys, Utils } from "../../Utils"; import { YoutubeBox } from "../apis/youtube/YoutubeBox"; import { DocServer } from "../DocServer"; +import { Networking } from "../Network"; import { DocumentManager } from "../util/DocumentManager"; import { dropActionType } from "../util/DragManager"; import { DirectoryImportBox } from "../util/Import & Export/DirectoryImportBox"; import { LinkManager } from "../util/LinkManager"; import { Scripting } from "../util/Scripting"; -import { UndoManager } from "../util/UndoManager"; -import { DocumentType } from "./DocumentTypes"; -import { SearchBox } from "../views/search/SearchBox"; +import { undoBatch, UndoManager } from "../util/UndoManager"; import { CollectionDockingView } from "../views/collections/CollectionDockingView"; import { CollectionView, CollectionViewType } from "../views/collections/CollectionView"; import { ContextMenu } from "../views/ContextMenu"; import { ContextMenuProps } from "../views/ContextMenuItem"; +import { DFLT_IMAGE_NATIVE_DIM } from "../views/globalCssVariables.scss"; import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, InkingStroke } from "../views/InkingStroke"; import { AudioBox } from "../views/nodes/AudioBox"; import { ColorBox } from "../views/nodes/ColorBox"; import { ComparisonBox } from "../views/nodes/ComparisonBox"; import { DocHolderBox } from "../views/nodes/DocHolderBox"; +import { FilterBox } from "../views/nodes/FilterBox"; import { FontIconBox } from "../views/nodes/FontIconBox"; -import { MenuIconBox } from "../views/nodes/MenuIconBox"; import { FormattedTextBox } from "../views/nodes/formattedText/FormattedTextBox"; import { ImageBox } from "../views/nodes/ImageBox"; import { KeyValueBox } from "../views/nodes/KeyValueBox"; @@ -47,14 +50,17 @@ import { SliderBox } from "../views/nodes/SliderBox"; import { VideoBox } from "../views/nodes/VideoBox"; import { WebBox } from "../views/nodes/WebBox"; import { PresElementBox } from "../views/presentationview/PresElementBox"; +import { SearchBox } from "../views/search/SearchBox"; import { DashWebRTCVideo } from "../views/webcam/DashWebRTCVideo"; -import { Networking } from "../Network"; -import { Upload } from "../../server/SharedMediaTypes"; +import { DocumentType } from "./DocumentTypes"; const path = require('path'); +const defaultNativeImageDim = Number(DFLT_IMAGE_NATIVE_DIM.replace("px", "")); export interface DocumentOptions { system?: boolean; _autoHeight?: boolean; + _headerHeight?: number; // height of header of custom notes + _headerFontSize?: number; // font size of header of custom notes _panX?: number; _panY?: number; _width?: number; @@ -72,6 +78,7 @@ export interface DocumentOptions { _scrollTop?: number; // scroll location for pdfs _noAutoscroll?: boolean;// whether collections autoscroll when this item is dragged _chromeStatus?: string; + _searchDoc?: boolean; // is this a search document (used to change UI for search results in schema view) _viewType?: string; // sub type of a collection _gridGap?: number; // gap between items in masonry view _xMargin?: number; // gap between left edge of document and start of masonry/stacking layouts @@ -87,12 +94,14 @@ export interface DocumentOptions { y?: number; z?: number; author?: string; + _hideContextMenu?: boolean; // whether the context menu can be shown dropAction?: dropActionType; childDropAction?: dropActionType; targetDropAction?: dropActionType; layoutKey?: string; type?: string; title?: string; + version?: string; // version identifier for a document label?: string; hidden?: boolean; userDoc?: Doc; // the userDocument @@ -101,12 +110,12 @@ export interface DocumentOptions { page?: number; description?: string; // added for links _viewScale?: number; - isDisplayPanel?: boolean; // whether the panel functions as GoldenLayout "stack" used to display documents forceActive?: boolean; layout?: string | Doc; // default layout string for a document + contentPointerEvents?: string; // pointer events allowed for content of a document view. eg. set to "none" sidebar views that are intended to document "menus" + childLimitHeight?: number; // whether to limit the height of colleciton children. 0 - means height can be no bigger than width childLayoutTemplate?: Doc; // template for collection to use to render its children (see PresBox or Buxton layout in tree view) childLayoutString?: string; // template string for collection to use to render its children - hideFilterView?: boolean; // whether to hide the filter popout on collections hideLinkButton?: boolean; // whether the blue link counter button should be hidden hideAllLinks?: boolean; // whether all individual blue anchor dots should be hidden _columnsHideIfEmpty?: boolean; // whether stacking view column headings should be hidden @@ -128,27 +137,27 @@ export interface DocumentOptions { isAnnotating?: boolean; // whether we web document is annotation mode where links can't be clicked to allow annotations to be created opacity?: number; defaultBackgroundColor?: string; - isBackground?: boolean; + _isBackground?: boolean; + _raiseWhenDragged?: boolean; // whether a document is brought to front when dragged. isLinkButton?: boolean; _columnWidth?: number; _fontSize?: string; _fontWeight?: number; _fontFamily?: string; - curPage?: number; - currentTimecode?: number; // the current timecode of a time-based document (e.g., current time of a video) value is in seconds + _curPage?: number; + _currentTimecode?: number; // the current timecode of a time-based document (e.g., current time of a video) value is in seconds + _currentFrame?: number; // the current frame of a frame-based collection (e.g., progressive slide) displayTimecode?: number; // the time that a document should be displayed (e.g., time an annotation should be displayed on a video) - currentFrame?: number; // the current frame of a frame-based collection (e.g., progressive slide) lastFrame?: number; // the last frame of a frame-based collection (e.g., progressive slide) activeFrame?: number; // the active frame of a document in a frame base collection appearFrame?: number; // the frame in which the document appears presTransition?: number; //the time taken for the transition TO a document presDuration?: number; //the duration of the slide in presentation view presProgressivize?: boolean; - // xArray?: number[]; - // yArray?: number[]; borderRounding?: string; boxShadow?: string; dontRegisterChildViews?: boolean; + dontRegisterView?: boolean; lookupField?: ScriptField; // script that returns the value of a field. This script is passed the rootDoc, layoutDoc, field, and container of the document. see PresBox. "onDoubleClick-rawScript"?: string; // onDoubleClick script in raw text form "onChildDoubleClick-rawScript"?: string; // onChildDoubleClick script in raw text form @@ -178,7 +187,7 @@ export interface DocumentOptions { clickFactory?: Doc; // document to create when clicking on a button with a suitable onClick script 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 clipboard?: Doc; - UseCors?: boolean; + useCors?: boolean; icon?: string; target?: Doc; // available for use in scripts as the primary target document sourcePanel?: Doc; // panel to display in 'targetContainer' as the result of a button onClick script @@ -187,13 +196,20 @@ export interface DocumentOptions { strokeWidth?: number; cloneFieldFilter?: List<string>; // fields not to copy when the document is cloned _stayInCollection?: boolean;// whether the document should remain in its collection when someone tries to drag and drop it elsewhere + freezeChildren?: string; // whether children are now allowed to be added and or removed from a collection treeViewPreventOpen?: boolean; // ignores the treeViewOpen Doc flag which allows a treeViewItem's expand/collapse state to be independent of other views of the same document in the tree view - treeViewHideTitle?: boolean; // whether to hide the title of a tree view + treeViewHideTitle?: boolean; // whether to hide the top document title of a tree view + treeViewHideHeader?: boolean; // whether to hide the header for a document in a tree view treeViewHideHeaderFields?: boolean; // whether to hide the drop down options for tree view items. treeViewOpen?: boolean; // whether this document is expanded in a tree view treeViewExpandedView?: string; // which field/thing is displayed when this item is opened in tree view treeViewChecked?: ScriptField; // script to call when a tree view checkbox is checked treeViewTruncateTitleWidth?: number; + treeViewOutlineMode?: boolean; // whether slide should function as a text outline + treeViewLockExpandedView?: boolean; // whether the expanded view can be changed + treeViewDefaultExpandedView?: string; // default expanded view + sidebarColor?: string; // background color of text sidebar + sidebarViewType?: string; // collection type of text sidebar limitHeight?: number; // maximum height for newly created (eg, from pasting) text documents // [key: string]: Opt<Field>; pointerHack?: boolean; // for buttons, allows onClick handler to fire onPointerDown @@ -243,6 +259,10 @@ export namespace Docs { layout: { view: SearchBox, dataField: defaultDataKey }, options: { _width: 400 } }], + [DocumentType.FILTER, { + layout: { view: FilterBox, dataField: defaultDataKey }, + options: { _width: 400 } + }], [DocumentType.COLOR, { layout: { view: ColorBox, dataField: defaultDataKey }, options: { _nativeWidth: 220, _nativeHeight: 300 } @@ -269,7 +289,7 @@ export namespace Docs { }], [DocumentType.VID, { layout: { view: VideoBox, dataField: defaultDataKey }, - options: { currentTimecode: 0 }, + options: { _currentTimecode: 0 }, }], [DocumentType.AUDIO, { layout: { view: AudioBox, dataField: defaultDataKey }, @@ -277,7 +297,7 @@ export namespace Docs { }], [DocumentType.PDF, { layout: { view: PDFBox, dataField: defaultDataKey }, - options: { curPage: 1 } + options: { _curPage: 1 } }], [DocumentType.IMPORT, { layout: { view: DirectoryImportBox, dataField: defaultDataKey }, @@ -332,7 +352,7 @@ export namespace Docs { }], [DocumentType.INK, { layout: { view: InkingStroke, dataField: defaultDataKey }, - options: { backgroundColor: "transparent" } + options: { _fontFamily: "cursive", backgroundColor: "transparent" } }], [DocumentType.SCREENSHOT, { layout: { view: ScreenshotBox, dataField: defaultDataKey }, @@ -534,7 +554,7 @@ export namespace Docs { Scripting.addGlobal(Buxton); - const delegateKeys = ["x", "y", "system", "layoutKey", "dropAction", "lockedPosiiton", "childDropAction", "isLinkButton", "isBackground", "removeDropProperties", "treeViewOpen"]; + const delegateKeys = ["x", "y", "system", "layoutKey", "dropAction", "lockedPosiiton", "childDropAction", "isLinkButton", "removeDropProperties", "treeViewOpen"]; /** * This function receives the relevant document prototype and uses @@ -584,7 +604,8 @@ export namespace Docs { viewDoc.author = Doc.CurrentUserEmail; viewDoc.type !== DocumentType.LINK && DocUtils.MakeLinkToActiveAudio(viewDoc); - if (Doc.UserDoc()?.defaultAclPrivate) viewDoc["ACL-Public"] = dataDoc["ACL-Public"] = "Not Shared"; + viewDoc["acl-Public"] = dataDoc["acl-Public"] = Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Add; + viewDoc["acl-Override"] = dataDoc["acl-Override"] = "None"; return Doc.assign(viewDoc, delegateProps, true); } @@ -661,6 +682,9 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.COLOR), "", options); } + export function RTFDocument(field: RichTextField, options: DocumentOptions = {}, fieldKey: string = "text") { + return InstanceFromProto(Prototypes.get(DocumentType.RTF), field, options, undefined, fieldKey); + } export function TextDocument(text: string, options: DocumentOptions = {}, fieldKey: string = "text") { const rtf = { doc: { @@ -681,24 +705,26 @@ export namespace Docs { export function LinkDocument(source: { doc: Doc, ctx?: Doc }, target: { doc: Doc, ctx?: Doc }, options: DocumentOptions = {}, id?: string) { const doc = InstanceFromProto(Prototypes.get(DocumentType.LINK), undefined, { - isLinkButton: true, treeViewHideTitle: true, treeViewOpen: false, backgroundColor: "lightBlue", // lightBlue is default color for linking dot and link documents text comment area - removeDropProperties: new List(["isBackground", "isLinkButton"]), ...options + dontRegisterChildViews: true, + isLinkButton: true, treeViewHideTitle: true, backgroundColor: "lightBlue", // lightBlue is default color for linking dot and link documents text comment area + treeViewExpandedView: "fields", removeDropProperties: new List(["_isBackground", "isLinkButton"]), ...options }, id); const linkDocProto = Doc.GetProto(doc); + linkDocProto.treeViewOpen = true;// setting this in the instance creator would set it on the view document. linkDocProto.anchor1 = source.doc; linkDocProto.anchor2 = target.doc; - linkDocProto.anchor1_timecode = source.doc.currentTimecode || source.doc.displayTimecode; - linkDocProto.anchor2_timecode = target.doc.currentTimecode || target.doc.displayTimecode; + linkDocProto.anchor1_timecode = source.doc._currentTimecode || source.doc.displayTimecode; + linkDocProto.anchor2_timecode = target.doc._currentTimecode || target.doc.displayTimecode; if (linkDocProto.linkBoxExcludedKeys === undefined) { - Cast(linkDocProto.proto, Doc, null).linkBoxExcludedKeys = new List(["treeViewExpandedView", "treeViewHideTitle", "removeDropProperties", "linkBoxExcludedKeys", "treeViewOpen", "aliasNumber", "isPrototype", "lastOpened", "creationDate", "author"]); + Cast(linkDocProto.proto, Doc, null).linkBoxExcludedKeys = new List(["treeViewExpandedView", "aliases", "treeViewHideTitle", "removeDropProperties", "linkBoxExcludedKeys", "treeViewOpen", "aliasNumber", "isPrototype", "creationDate", "author"]); Cast(linkDocProto.proto, Doc, null).layoutKey = undefined; } LinkManager.Instance.addLink(doc); - Doc.GetProto(source.doc).links = ComputedField.MakeFunction("links(self)"); - Doc.GetProto(target.doc).links = ComputedField.MakeFunction("links(self)"); + source.doc.links === undefined && (Doc.GetProto(source.doc).links = ComputedField.MakeFunction("links(self)")); + target.doc.links === undefined && (Doc.GetProto(target.doc).links = ComputedField.MakeFunction("links(self)")); return doc; } @@ -720,9 +746,12 @@ export namespace Docs { I._backgroundColor = "transparent"; I._width = options._width; I._height = options._height; + I._fontFamily = "cursive"; I.author = Doc.CurrentUserEmail; I.rotation = 0; I.data = new InkField(points); + I["acl-Public"] = Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Add; + I["acl-Override"] = "None"; return I; } @@ -747,11 +776,11 @@ export namespace Docs { } export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", dontRegisterChildViews: true, ...options, _viewType: CollectionViewType.Freeform }, id); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", ...options, _viewType: CollectionViewType.Freeform }, id); } export function PileDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", backgroundColor: "black", hideFilterView: true, forceActive: true, ...options, _viewType: CollectionViewType.Pile }, id); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", backgroundColor: "black", _noAutoscroll: true, ...options, _viewType: CollectionViewType.Pile }, id); } export function LinearDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) { @@ -779,7 +808,7 @@ export namespace Docs { } export function StackingDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", dontRegisterChildViews: true, ...options, _viewType: CollectionViewType.Stacking }, id); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", ...options, _viewType: CollectionViewType.Stacking }, id); } export function MulticolumnDocument(documents: Array<Doc>, options: DocumentOptions) { @@ -791,7 +820,7 @@ export namespace Docs { export function MasonryDocument(documents: Array<Doc>, options: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", dontRegisterChildViews: true, ...options, _viewType: CollectionViewType.Masonry }); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", ...options, _viewType: CollectionViewType.Masonry }); } export function LabelDocument(options?: DocumentOptions) { @@ -815,14 +844,19 @@ export namespace Docs { export function FontIconDocument(options?: DocumentOptions) { return InstanceFromProto(Prototypes.get(DocumentType.FONTICON), undefined, { hideLinkButton: true, ...(options || {}) }); } + export function FilterDocument(options?: DocumentOptions) { + return InstanceFromProto(Prototypes.get(DocumentType.FILTER), undefined, { ...(options || {}) }); + } export function PresElementBoxDocument(options?: DocumentOptions) { return InstanceFromProto(Prototypes.get(DocumentType.PRESELEMENT), undefined, { ...(options || {}) }); } export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) { - const inst = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Docking, dockingConfig: config }, id); - Doc.GetProto(inst).data = new List<Doc>(documents); + const inst = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { freezeChildren: "remove|add", treeViewDefaultExpandedView: "data", ...options, _viewType: CollectionViewType.Docking, dockingConfig: config }, id); + const tabs = TreeDocument(documents, { title: "On-Screen Tabs", freezeChildren: "remove|add", treeViewLockExpandedView: true, treeViewDefaultExpandedView: "data", system: true }); + const all = TreeDocument([], { title: "Off-Screen Tabs", freezeChildren: "add", treeViewLockExpandedView: true, treeViewDefaultExpandedView: "data", system: true }); + Doc.GetProto(inst).data = new List<Doc>([tabs, all]); return inst; } @@ -842,7 +876,7 @@ export namespace Docs { { type: type, content: [ - ...configs.map(config => CollectionDockingView.makeDocumentConfig(config.doc, config.initialWidth, config.path)) + ...configs.map(config => CollectionDockingView.makeDocumentConfig(config.doc, undefined, config.initialWidth)) ] } ] @@ -857,6 +891,27 @@ export namespace Docs { } export namespace DocUtils { + export function Excluded(d: Doc, docFilters: string[]) { + const filterFacets: { [key: string]: { [value: string]: string } } = {}; // maps each filter key to an object with value=>modifier fields + for (let i = 0; i < docFilters.length; i += 3) { + const [key, value, modifiers] = docFilters.slice(i, i + 3); + if (!filterFacets[key]) { + filterFacets[key] = {}; + } + filterFacets[key][value] = modifiers; + } + + if (d.z) return false; + for (const facetKey of Object.keys(filterFacets)) { + const facet = filterFacets[facetKey]; + const xs = Object.keys(facet).filter(value => facet[value] === "x"); + const failsNotEqualFacets = xs?.some(value => Doc.matchFieldValue(d, facetKey, value)); + if (failsNotEqualFacets) { + return true; + } + } + return false; + } export function FilterDocs(docs: Doc[], docFilters: string[], docRangeFilters: string[], viewSpecScript?: ScriptField) { const childDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs; @@ -870,21 +925,24 @@ export namespace DocUtils { } const filteredDocs = docFilters.length ? childDocs.filter(d => { + if (d.z) return true; for (const facetKey of Object.keys(filterFacets)) { const facet = filterFacets[facetKey]; - const satisfiesFacet = Object.keys(facet).some(value => { - if (facet[value] === "match") { - if (facetKey.startsWith("*")) { // fields starting with a '*' are used to match families of related fields. ie, *lastModified will match text-lastModified, data-lastModified, etc - const allKeys = Array.from(Object.keys(d)); - allKeys.push(...Object.keys(Doc.GetProto(d))); - const keys = allKeys.filter(key => key.includes(facetKey.substring(1))); - return keys.some(key => Field.toString(d[key] as Field).includes(value)); - } - return d[facetKey] === undefined || Field.toString(d[facetKey] as Field).includes(value); + const matches = Object.keys(facet).filter(value => facet[value] === "match"); + const checks = Object.keys(facet).filter(value => facet[value] === "check"); + const xs = Object.keys(facet).filter(value => facet[value] === "x"); + const failsNotEqualFacets = !xs.length ? false : xs.some(value => Doc.matchFieldValue(d, facetKey, value)); + const satisfiesCheckFacets = !checks.length ? true : checks.some(value => Doc.matchFieldValue(d, facetKey, value)); + const satisfiesMatchFacets = !matches.length ? true : matches.some(value => { + if (facetKey.startsWith("*")) { // fields starting with a '*' are used to match families of related fields. ie, *lastModified will match text-lastModified, data-lastModified, etc + const allKeys = Array.from(Object.keys(d)); + allKeys.push(...Object.keys(Doc.GetProto(d))); + const keys = allKeys.filter(key => key.includes(facetKey.substring(1))); + return keys.some(key => Field.toString(d[key] as Field).includes(value)); } - return (facet[value] === "x") !== Doc.matchFieldValue(d, facetKey, value); + return Field.toString(d[facetKey] as Field).includes(value); }); - if (!satisfiesFacet) { + if (!satisfiesCheckFacets || !satisfiesMatchFacets || failsNotEqualFacets) { return false; } } @@ -896,7 +954,7 @@ export namespace DocUtils { const min = Number(docRangeFilters[i + 1]); const max = Number(docRangeFilters[i + 2]); const val = Cast(d[key], "number", null); - if (val !== undefined && (val < min || val > max)) { + if (val === undefined || (val < min || val > max)) { return false; } } @@ -942,9 +1000,9 @@ export namespace DocUtils { DocUtils.ActiveRecordings.map(d => DocUtils.MakeLink({ doc: doc }, { doc: d }, "audio link", "audio timeline")); } - export function MakeLink(source: { doc: Doc }, target: { doc: Doc }, linkRelationship: string = "", description: string = "", id?: string) { + export function MakeLink(source: { doc: Doc }, target: { doc: Doc }, linkRelationship: string = "", description: string = "", id?: string, allowParCollectionLink?: boolean) { const sv = DocumentManager.Instance.getDocumentView(source.doc); - if (sv && sv.props.ContainingCollectionDoc === target.doc) return; + if (!allowParCollectionLink && sv?.props.ContainingCollectionDoc === target.doc) return; if (target.doc === Doc.UserDoc()) return undefined; const linkDoc = Docs.Create.LinkDocument(source, target, { linkRelationship, layoutKey: "layout_linkView", description }, id); @@ -952,6 +1010,7 @@ export namespace DocUtils { Doc.GetProto(linkDoc)["anchor2-useLinkSmallAnchor"] = target.doc.useLinkSmallAnchor; linkDoc.linkDisplay = true; linkDoc.hidden = true; + Doc.GetProto(linkDoc)["acl-Public"] = linkDoc["acl-Public"] = SharingPermissions.Add; linkDoc.layout_linkView = Cast(Cast(Doc.UserDoc()["template-button-link"], Doc, null).dragFactory, Doc, null); Doc.GetProto(linkDoc).title = ComputedField.MakeFunction('self.anchor1?.title +" (" + (self.linkRelationship||"to") +") " + self.anchor2?.title'); @@ -1032,17 +1091,17 @@ export namespace DocUtils { }); } ctor = Docs.Create.WebDocument; - options = { ...options, _nativeWidth: 850, _nativeHeight: 962, _width: 500, _height: 566, title: path, }; + options = { ...options, _fitWidth: true, _nativeWidth: 850, _width: 400, _height: 512, title: path, }; } return ctor ? ctor(path, options) : undefined; } - export function addDocumentCreatorMenuItems(docTextAdder: (d: Doc) => void, docAdder: (d: Doc) => void, x: number, y: number): void { - ContextMenu.Instance.addItem({ + export function addDocumentCreatorMenuItems(docTextAdder: (d: Doc) => void, docAdder: (d: Doc) => void, x: number, y: number, simpleMenu: boolean = false): void { + !simpleMenu && ContextMenu.Instance.addItem({ description: "Add Note ...", subitems: DocListCast((Doc.UserDoc()["template-notes"] as Doc).data).map((note, i) => ({ description: ":" + StrCast(note.title), - event: (args: { x: number, y: number }) => { + event: undoBatch((args: { x: number, y: number }) => { const textDoc = Docs.Create.TextDocument("", { _width: 200, x, y, _autoHeight: note._autoHeight !== false, title: StrCast(note.title) + "#" + (note.aliasCount = NumCast(note.aliasCount) + 1) @@ -1050,24 +1109,25 @@ export namespace DocUtils { textDoc.layoutKey = "layout_" + note.title; textDoc[textDoc.layoutKey] = note; docTextAdder(textDoc); - }, + }), icon: "eye" })) as ContextMenuProps[], icon: "eye" }); ContextMenu.Instance.addItem({ description: "Add Template Doc ...", - subitems: DocListCast(Cast(Doc.UserDoc().myItemCreators, Doc, null)?.data).map(btnDoc => Cast(btnDoc?.dragFactory, Doc, null)).filter(doc => doc).map((dragDoc, i) => ({ + subitems: DocListCast(Cast(Doc.UserDoc().myItemCreators, Doc, null)?.data).filter(btnDoc => !btnDoc.hidden).map(btnDoc => Cast(btnDoc?.dragFactory, Doc, null)).filter(doc => doc && doc !== Doc.UserDoc().emptyPresentation).map((dragDoc, i) => ({ description: ":" + StrCast(dragDoc.title), - event: (args: { x: number, y: number }) => { - const newDoc = Doc.ApplyTemplate(dragDoc); + event: undoBatch((args: { x: number, y: number }) => { + const newDoc = Doc.copyDragFactory(dragDoc); if (newDoc) { newDoc.author = Doc.CurrentUserEmail; newDoc.x = x; newDoc.y = y; + if (newDoc.type === DocumentType.RTF) FormattedTextBox.SelectOnLoad = newDoc[Id]; docAdder(newDoc); } - }, + }), icon: "eye" })) as ContextMenuProps[], icon: "eye" @@ -1171,7 +1231,7 @@ export namespace DocUtils { } const options = optionsCollection as Doc; const targetDoc = doc && Doc.GetProto(Cast(doc.rootDocument, Doc, null) || doc); - const docFind = `options.data.find(doc => doc.title === (this.rootDocument||this)["${enumeratedFieldKey}"])?`; + const docFind = `options.data?.find(doc => doc.title === (this.rootDocument||this)["${enumeratedFieldKey}"])?`; targetDoc && (targetDoc.backgroundColor = ComputedField.MakeFunction(docFind + `._backgroundColor || "white"`, undefined, { options })); targetDoc && (targetDoc.color = ComputedField.MakeFunction(docFind + `.color || "black"`, undefined, { options })); targetDoc && (targetDoc.borderRounding = ComputedField.MakeFunction(docFind + `.borderRounding`, undefined, { options })); @@ -1204,8 +1264,14 @@ export namespace DocUtils { proto.text = result.rawText; proto.fileUpload = basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, ""); if (Upload.isImageInformation(result)) { - proto["data-nativeWidth"] = (result.nativeWidth > result.nativeHeight) ? 400 * result.nativeWidth / result.nativeHeight : 400; - proto["data-nativeHeight"] = (result.nativeWidth > result.nativeHeight) ? 400 : 400 / (result.nativeWidth / result.nativeHeight); + const maxNativeDim = Math.min(Math.max(result.nativeHeight, result.nativeWidth), defaultNativeImageDim); + proto["data-nativeOrientation"] = result.exifData?.data?.image?.Orientation; + proto["data-nativeWidth"] = (result.nativeWidth < result.nativeHeight) ? maxNativeDim * result.nativeWidth / result.nativeHeight : maxNativeDim; + proto["data-nativeHeight"] = (result.nativeWidth < result.nativeHeight) ? maxNativeDim : maxNativeDim / (result.nativeWidth / result.nativeHeight); + if (Number(result.exifData?.data?.image?.Orientation) >= 5) { + proto["data-nativeHeight"] = (result.nativeWidth < result.nativeHeight) ? maxNativeDim * result.nativeWidth / result.nativeHeight : maxNativeDim; + proto["data-nativeWidth"] = (result.nativeWidth < result.nativeHeight) ? maxNativeDim : maxNativeDim / (result.nativeWidth / result.nativeHeight); + } proto.contentSize = result.contentSize; } generatedDocuments.push(doc); diff --git a/src/client/goldenLayout.js b/src/client/goldenLayout.js index 2d4283b02..293762709 100644 --- a/src/client/goldenLayout.js +++ b/src/client/goldenLayout.js @@ -3204,7 +3204,7 @@ * If this was the last content item, remove this node as well */ } else if (!(this instanceof lm.items.Root) && this.config.isClosable === true) { - this.parent.removeChild(this); + if (!this.parent.parent.isRoot || this.parent.contentItems.length > 1) this.parent.removeChild(this); // bcz: added test for last stack } }, diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 5d747584a..7cc35d67a 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -1,30 +1,38 @@ import { computed, observable, reaction } from "mobx"; import * as rp from 'request-promise'; -import { Utils } from "../../Utils"; -import { DocServer } from "../DocServer"; -import { Docs, DocumentOptions, DocUtils } from "../documents/Documents"; -import { UndoManager } from "./UndoManager"; -import { Doc, DocListCast, DocListCastAsync, DataSym } from "../../fields/Doc"; +import { DataSym, Doc, DocListCast, DocListCastAsync } from "../../fields/Doc"; +import { Id } from "../../fields/FieldSymbols"; import { List } from "../../fields/List"; +import { PrefetchProxy } from "../../fields/Proxy"; +import { RichTextField } from "../../fields/RichTextField"; import { listSpec } from "../../fields/Schema"; -import { ScriptField, ComputedField } from "../../fields/ScriptField"; -import { Cast, PromiseValue, StrCast, NumCast, BoolCast } from "../../fields/Types"; +import { SchemaHeaderField } from "../../fields/SchemaHeaderField"; +import { ComputedField, ScriptField } from "../../fields/ScriptField"; +import { BoolCast, Cast, NumCast, PromiseValue, StrCast } from "../../fields/Types"; import { nullAudio } from "../../fields/URLField"; -import { DragManager } from "./DragManager"; -import { Scripting } from "./Scripting"; -import { CollectionViewType, CollectionView } from "../views/collections/CollectionView"; -import { makeTemplate } from "./DropConverter"; -import { RichTextField } from "../../fields/RichTextField"; -import { PrefetchProxy } from "../../fields/Proxy"; -import { FormattedTextBox } from "../views/nodes/formattedText/FormattedTextBox"; -import { MainView } from "../views/MainView"; +import { Utils } from "../../Utils"; +import { DocServer } from "../DocServer"; +import { Docs, DocumentOptions, DocUtils } from "../documents/Documents"; import { DocumentType } from "../documents/DocumentTypes"; -import { SchemaHeaderField } from "../../fields/SchemaHeaderField"; +import { CollectionDockingView } from "../views/collections/CollectionDockingView"; import { DimUnit } from "../views/collections/collectionMulticolumn/CollectionMulticolumnView"; +import { CollectionView, CollectionViewType } from "../views/collections/CollectionView"; +import { MainView } from "../views/MainView"; +import { FormattedTextBox } from "../views/nodes/formattedText/FormattedTextBox"; import { LabelBox } from "../views/nodes/LabelBox"; +import { OverlayView } from "../views/OverlayView"; +import { DocumentManager } from "./DocumentManager"; +import { DragManager } from "./DragManager"; +import { makeTemplate } from "./DropConverter"; +import { HistoryUtil } from "./History"; import { LinkManager } from "./LinkManager"; -import { Id } from "../../fields/FieldSymbols"; +import { Scripting } from "./Scripting"; +import { SearchUtil } from "./SearchUtil"; +import { SelectionManager } from "./SelectionManager"; +import { UndoManager } from "./UndoManager"; + +const headerViewVersion = "0.1"; export class CurrentUserUtils { private static curr_id: string; //TODO tfs: these should be temporary... @@ -36,28 +44,12 @@ export class CurrentUserUtils { @computed public static get UserDocument() { return Doc.UserDoc(); } @observable public static GuestTarget: Doc | undefined; - @observable public static GuestWorkspace: Doc | undefined; + @observable public static GuestDashboard: Doc | undefined; @observable public static GuestMobile: Doc | undefined; - @observable public static propertiesWidth: number = 0; - // sets up the default User Templates - slideView, queryView, descriptionView + // sets up the default User Templates - slideView, headerView static setupUserTemplateButtons(doc: Doc) { - if (doc["template-button-query"] === undefined) { - const queryTemplate = Docs.Create.MulticolumnDocument( - [ - Docs.Create.SearchDocument({ _viewType: CollectionViewType.Schema, ignoreClick: true, forceActive: true, lockedPosition: true, title: "query", _height: 200, system: true }), - Docs.Create.FreeformDocument([], { title: "data", _height: 100, system: true }) - ], - { _width: 400, _height: 300, title: "queryView", _chromeStatus: "disabled", _xMargin: 3, _yMargin: 3, hideFilterView: true, system: true } - ); - queryTemplate.isTemplateDoc = makeTemplate(queryTemplate); - doc["template-button-query"] = CurrentUserUtils.ficon({ - onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'), - dragFactory: new PrefetchProxy(queryTemplate) as any as Doc, - removeDropProperties: new List<string>(["dropAction"]), title: "query view", icon: "question-circle" - }); - } // Prototype for mobile button (not sure if 'Advanced Item Prototypes' is ideal location) if (doc["template-mobile-button"] === undefined) { const queryTemplate = this.mobileButton({ @@ -72,9 +64,8 @@ export class CurrentUserUtils { this.mobileTextContainer({}, [this.mobileButtonText({}, "NEW MOBILE BUTTON"), this.mobileButtonInfo({}, "You can customize this button and make it your own.")])]); doc["template-mobile-button"] = CurrentUserUtils.ficon({ - onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'), - dragFactory: new PrefetchProxy(queryTemplate) as any as Doc, - removeDropProperties: new List<string>(["dropAction"]), title: "mobile button", icon: "mobile" + onDragStart: ScriptField.MakeFunction('copyDragFactory(this.dragFactory)'), + dragFactory: new PrefetchProxy(queryTemplate) as any as Doc, title: "mobile button", icon: "mobile" }); } @@ -84,39 +75,21 @@ export class CurrentUserUtils { Docs.Create.MulticolumnDocument([], { title: "data", _height: 200, system: true }), Docs.Create.TextDocument("", { title: "text", _height: 100, system: true }) ], - { _width: 400, _height: 300, title: "slideView", _chromeStatus: "disabled", _xMargin: 3, _yMargin: 3, hideFilterView: true, system: true } + { _width: 400, _height: 300, title: "slideView", _chromeStatus: "disabled", _xMargin: 3, _yMargin: 3, system: true } ); slideTemplate.isTemplateDoc = makeTemplate(slideTemplate); doc["template-button-slides"] = CurrentUserUtils.ficon({ - onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'), - dragFactory: new PrefetchProxy(slideTemplate) as any as Doc, - removeDropProperties: new List<string>(["dropAction"]), title: "presentation slide", icon: "address-card" - }); - } - - if (doc["template-button-description"] === undefined) { - const descriptionTemplate = Doc.MakeDelegate(Docs.Create.TextDocument(" ", { title: "header", _height: 100, system: true }, "header")); // text needs to be a space to allow templateText to be created - descriptionTemplate.system = true; - descriptionTemplate[DataSym].layout = - "<div>" + - " <FormattedTextBox {...props} height='{this._headerHeight||75}px' background='{this._headerColor||`orange`}' fieldKey={'header'}/>" + - " <FormattedTextBox {...props} position='absolute' top='{(this._headerHeight||75)*scale}px' height='calc({100/scale}% - {this._headerHeight||75}px)' fieldKey={'text'}/>" + - "</div>"; - (descriptionTemplate.proto as Doc).isTemplateDoc = makeTemplate(descriptionTemplate.proto as Doc, true, "descriptionView"); - - doc["template-button-description"] = CurrentUserUtils.ficon({ - onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'), - dragFactory: new PrefetchProxy(descriptionTemplate) as any as Doc, - removeDropProperties: new List<string>(["dropAction"]), title: "description view", icon: "window-maximize", system: true + onDragStart: ScriptField.MakeFunction('copyDragFactory(this.dragFactory)'), + dragFactory: new PrefetchProxy(slideTemplate) as any as Doc, title: "presentation slide", icon: "address-card" }); } if (doc["template-button-link"] === undefined) { // set _backgroundColor to transparent to prevent link dot from obscuring document it's attached to. - const linkTemplate = Doc.MakeDelegate(Docs.Create.TextDocument(" ", { title: "header", _height: 100, system: true }, "header")); // text needs to be a space to allow templateText to be created + const linkTemplate = Doc.MakeDelegate(Docs.Create.TextDocument(" ", { title: "header", _autoHeight: true, system: true }, "header")); // text needs to be a space to allow templateText to be created linkTemplate.system = true; Doc.GetProto(linkTemplate).layout = "<div>" + - " <FormattedTextBox {...props} height='{this._headerHeight||75}px' background='{this._headerColor||`lightGray`}' fieldKey={'header'}/>" + + " <FormattedTextBox {...props} dontSelectOnLoad={'true'} height='{this._headerHeight||75}px' ignoreAutoHeight={'true'} background='{this._headerColor||`lightGray`}' fieldKey={'header'}/>" + " <FormattedTextBox {...props} position='absolute' top='{(this._headerHeight||75)*scale}px' height='calc({100/scale}% - {this._headerHeight||75}px)' fieldKey={'text'}/>" + "</div>"; (linkTemplate.proto as Doc).isTemplateDoc = makeTemplate(linkTemplate.proto as Doc, true, "linkView"); @@ -152,9 +125,8 @@ export class CurrentUserUtils { linkTemplate.header = new RichTextField(JSON.stringify(rtf2), ""); doc["template-button-link"] = CurrentUserUtils.ficon({ - onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'), - dragFactory: new PrefetchProxy(linkTemplate) as any as Doc, - removeDropProperties: new List<string>(["dropAction"]), title: "link view", icon: "window-maximize", system: true + onDragStart: ScriptField.MakeFunction('copyDragFactory(this.dragFactory)'), + dragFactory: new PrefetchProxy(linkTemplate) as any as Doc, title: "link view", icon: "window-maximize", system: true }); } @@ -184,9 +156,8 @@ export class CurrentUserUtils { box.isTemplateDoc = makeTemplate(box, true, "switch"); doc["template-button-switch"] = CurrentUserUtils.ficon({ - onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'), - dragFactory: new PrefetchProxy(box) as any as Doc, - removeDropProperties: new List<string>(["dropAction"]), title: "data switch", icon: "toggle-on", system: true + onDragStart: ScriptField.MakeFunction('copyDragFactory(this.dragFactory)'), + dragFactory: new PrefetchProxy(box) as any as Doc, title: "data switch", icon: "toggle-on", system: true }); } @@ -199,9 +170,9 @@ export class CurrentUserUtils { onChildDoubleClick: openInTarget, backgroundColor: "#9b9b9b3F", system: true }); - const details = TextDocument("", { title: "details", _height: 350, _autoHeight: true, system: true }); - const short = TextDocument("", { title: "shortDescription", treeViewOpen: true, treeViewExpandedView: "layout", _height: 100, _autoHeight: true, system: true }); - const long = TextDocument("", { title: "longDescription", treeViewOpen: false, treeViewExpandedView: "layout", _height: 350, _autoHeight: true, system: true }); + const details = TextDocument("", { title: "details", _height: 200, _autoHeight: true, system: true }); + const short = TextDocument("", { title: "shortDescription", treeViewOpen: true, treeViewExpandedView: "layout", _height: 75, _autoHeight: true, system: true }); + const long = TextDocument("", { title: "longDescription", treeViewOpen: false, treeViewExpandedView: "layout", _height: 150, _autoHeight: true, system: true }); const buxtonFieldKeys = ["year", "originalPrice", "degreesOfFreedom", "company", "attribute", "primaryKey", "secondaryKey", "dimensions"]; const detailedTemplate = { @@ -217,7 +188,7 @@ export class CurrentUserUtils { details.text = new RichTextField(JSON.stringify(detailedTemplate), buxtonFieldKeys.join(" ")); const shared = { _chromeStatus: "disabled", _autoHeight: true, _xMargin: 0 }; - const detailViewOpts = { title: "detailView", _width: 300, _fontFamily: "Arial", _fontSize: "12pt" }; + const detailViewOpts = { title: "detailView", _width: 300, _fontFamily: "Arial", _fontSize: "12px" }; const descriptionWrapperOpts = { title: "descriptions", _height: 300, _columnWidth: -1, treeViewHideTitle: true, _pivotField: "title", system: true }; const descriptionWrapper = MasonryDocument([details, short, long], { ...shared, ...descriptionWrapperOpts }); @@ -234,25 +205,23 @@ export class CurrentUserUtils { long.title = "Long Description"; doc["template-button-detail"] = CurrentUserUtils.ficon({ - onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'), - dragFactory: new PrefetchProxy(detailView) as any as Doc, - removeDropProperties: new List<string>(["dropAction"]), title: "detail view", icon: "window-maximize", system: true + onDragStart: ScriptField.MakeFunction('copyDragFactory(this.dragFactory)'), + dragFactory: new PrefetchProxy(detailView) as any as Doc, title: "detail view", icon: "window-maximize", system: true }); } const requiredTypes = [ doc["template-button-slides"] as Doc, - doc["template-button-description"] as Doc, - doc["template-button-query"] as Doc, doc["template-mobile-button"] as Doc, doc["template-button-detail"] as Doc, doc["template-button-link"] as Doc, - doc["template-button-switch"] as Doc]; + //doc["template-button-switch"] as Doc] + ]; if (doc["template-buttons"] === undefined) { doc["template-buttons"] = new PrefetchProxy(Docs.Create.MasonryDocument(requiredTypes, { title: "Advanced Item Prototypes", _xMargin: 0, _showTitle: "title", hidden: ComputedField.MakeFunction("self.userDoc.noviceMode") as any, - userDoc: doc, + userDoc: doc, _stayInCollection: true, _hideContextMenu: true, _autoHeight: true, _width: 500, _columnWidth: 35, ignoreClick: true, lockedPosition: true, _chromeStatus: "disabled", dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), system: true })); @@ -302,13 +271,11 @@ export class CurrentUserUtils { } if (doc["template-notes"] === undefined) { - doc["template-notes"] = new PrefetchProxy(Docs.Create.TreeDocument([doc["template-note-Note"] as any as Doc, - doc["template-note-Idea"] as any as Doc, doc["template-note-Topic"] as any as Doc, doc["template-note-Todo"] as any as Doc], + doc["template-notes"] = new PrefetchProxy(Docs.Create.TreeDocument([doc["template-note-Note"] as any as Doc, doc["template-note-Idea"] as any as Doc, doc["template-note-Topic"] as any as Doc], // doc["template-note-Todo"] as any as Doc], { title: "Note Layouts", _height: 75, system: true })); } else { const curNoteTypes = Cast(doc["template-notes"], Doc, null); - const requiredTypes = [doc["template-note-Note"] as any as Doc, doc["template-note-Idea"] as any as Doc, - doc["template-note-Topic"] as any as Doc, doc["template-note-Todo"] as any as Doc]; + const requiredTypes = [doc["template-note-Note"] as any as Doc, doc["template-note-Idea"] as any as Doc, doc["template-note-Topic"] as any as Doc];//, doc["template-note-Todo"] as any as Doc]; DocListCastAsync(curNoteTypes.data).then(async curNotes => { await Promise.all(curNotes!); requiredTypes.map(ntype => Doc.AddDocToList(curNoteTypes, "data", ntype)); @@ -394,32 +361,80 @@ export class CurrentUserUtils { }[] { if (doc.emptyPresentation === undefined) { doc.emptyPresentation = Docs.Create.PresDocument(new List<Doc>(), - { title: "Presentation", _viewType: CollectionViewType.Stacking, _width: 400, _height: 500, targetDropAction: "alias", _chromeStatus: "replaced", boxShadow: "0 0", system: true }); + { title: "Untitled Presentation", _viewType: CollectionViewType.Stacking, _width: 400, _height: 500, targetDropAction: "alias", _chromeStatus: "replaced", boxShadow: "0 0", system: true, cloneFieldFilter: new List<string>(["system"]) }); + ((doc.emptyPresentation as Doc).proto as Doc)["dragFactory-count"] = 0; } if (doc.emptyCollection === undefined) { doc.emptyCollection = Docs.Create.FreeformDocument([], { _nativeWidth: undefined, _nativeHeight: undefined, _width: 150, _height: 100, title: "freeform", system: true, cloneFieldFilter: new List<string>(["system"]) }); + ((doc.emptyCollection as Doc).proto as Doc)["dragFactory-count"] = 0; } if (doc.emptyPane === undefined) { - doc.emptyPane = Docs.Create.FreeformDocument([], { _nativeWidth: undefined, _nativeHeight: undefined, title: "Untitled Collection", system: true, cloneFieldFilter: new List<string>(["system"]) }); + doc.emptyPane = Docs.Create.FreeformDocument([], { _nativeWidth: undefined, _nativeHeight: undefined, _width: 500, _height: 800, title: "Untitled Tab", system: true, cloneFieldFilter: new List<string>(["system"]) }); + ((doc.emptyPane as Doc).proto as Doc)["dragFactory-count"] = 0; + } + if (doc.emptySlide === undefined) { + const textDoc = Docs.Create.TextDocument("Slide", { title: "Slide", _viewType: CollectionViewType.Tree, _fontSize: "20px", treeViewOutlineMode: true, _xMargin: 0, _yMargin: 0, _width: 300, _height: 200, _singleLine: true, _backgroundColor: "transparent", system: true, cloneFieldFilter: new List<string>(["system"]) }); + Doc.GetProto(textDoc).layout = CollectionView.LayoutString("data"); + Doc.GetProto(textDoc).title = ComputedField.MakeFunction('self.text?.Text'); + Doc.GetProto(textDoc).data = new List<Doc>([]); + FormattedTextBox.SelectOnLoad = textDoc[Id]; + doc.emptySlide = textDoc; + } + if ((doc.emptyHeader as Doc)?.version !== headerViewVersion) { + const json = { + doc: { + type: "doc", + content: [ + { + type: "paragraph", attrs: {}, content: [{ + type: "dashField", + attrs: { fieldKey: "author", docid: "", hideKey: false }, + marks: [{ type: "strong" }] + }, { + type: "dashField", + attrs: { fieldKey: "creationDate", docid: "", hideKey: false }, + marks: [{ type: "strong" }] + }] + }] + }, + selection: { type: "text", anchor: 1, head: 1 }, + storedMarks: [] + }; + const headerTemplate = Docs.Create.RTFDocument(new RichTextField(JSON.stringify(json), ""), { title: "header", version: headerViewVersion, target: doc, _height: 70, _headerHeight: 12, _headerFontSize: 9, _autoHeight: true, system: true, cloneFieldFilter: new List<string>(["system"]) }, "header"); // text needs to be a space to allow templateText to be created + headerTemplate[DataSym].layout = + "<div style={'height:100%'}>" + + " <FormattedTextBox {...props} fieldKey={'header'} dontSelectOnLoad={'true'} ignoreAutoHeight={'true'} pointerEvents='{this._headerPointerEvents||`none`}' fontSize='{this._headerFontSize}px' height='{this._headerHeight}px' background='{this._headerColor||this.target.userColor}' />" + + " <FormattedTextBox {...props} fieldKey={'text'} position='absolute' top='{(this._headerHeight)*scale}px' height='calc({100/scale}% - {this._headerHeight}px)'/>" + + "</div>"; + (headerTemplate.proto as Doc).isTemplateDoc = makeTemplate(headerTemplate.proto as Doc, true, "headerView"); + doc.emptyHeader = headerTemplate; + ((doc.emptyHeader as Doc).proto as Doc)["dragFactory-count"] = 0; } if (doc.emptyComparison === undefined) { doc.emptyComparison = Docs.Create.ComparisonDocument({ title: "compare", _width: 300, _height: 300, system: true, cloneFieldFilter: new List<string>(["system"]) }); } if (doc.emptyScript === undefined) { doc.emptyScript = Docs.Create.ScriptingDocument(undefined, { _width: 200, _height: 250, title: "script", system: true, cloneFieldFilter: new List<string>(["system"]) }); + ((doc.emptyScript as Doc).proto as Doc)["dragFactory-count"] = 0; } if (doc.emptyScreenshot === undefined) { doc.emptyScreenshot = Docs.Create.ScreenshotDocument("", { _width: 400, _height: 200, title: "screen snapshot", system: true, cloneFieldFilter: new List<string>(["system"]) }); } if (doc.emptyAudio === undefined) { - doc.emptyAudio = Docs.Create.AudioDocument(nullAudio, { _width: 200, title: "ready to record audio", system: true, cloneFieldFilter: new List<string>(["system"]) }); + doc.emptyAudio = Docs.Create.AudioDocument(nullAudio, { _width: 200, title: "audio recording", system: true, cloneFieldFilter: new List<string>(["system"]) }); + ((doc.emptyAudio as Doc).proto as Doc)["dragFactory-count"] = 0; + } + if (doc.emptyNote === undefined) { + doc.emptyNote = Docs.Create.TextDocument("", { _width: 200, title: "text note", system: true, cloneFieldFilter: new List<string>(["system"]) }); + ((doc.emptyNote as Doc).proto as Doc)["dragFactory-count"] = 0; } if (doc.emptyImage === undefined) { doc.emptyImage = Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { _width: 250, _nativeWidth: 250, title: "an image of a cat", system: true }); } if (doc.emptyButton === undefined) { doc.emptyButton = Docs.Create.ButtonDocument({ _width: 150, _height: 50, _xPadding: 10, _yPadding: 10, title: "Button", system: true, cloneFieldFilter: new List<string>(["system"]) }); + ((doc.emptyButton as Doc).proto as Doc)["dragFactory-count"] = 0; } if (doc.emptyDocHolder === undefined) { doc.emptyDocHolder = Docs.Create.DocumentDocument( @@ -427,41 +442,31 @@ export class CurrentUserUtils { { _width: 250, _height: 250, title: "container", system: true, cloneFieldFilter: new List<string>(["system"]) }); } if (doc.emptyWebpage === undefined) { - doc.emptyWebpage = Docs.Create.WebDocument("", { title: "webpage", _nativeWidth: 850, _nativeHeight: 962, _width: 400, UseCors: true, system: true, cloneFieldFilter: new List<string>(["system"]) }); + doc.emptyWebpage = Docs.Create.WebDocument("", { title: "webpage", _nativeWidth: 850, _fitWidth: true, isTemplateDoc: true, _height: 512, _width: 400, useCors: true, system: true, cloneFieldFilter: new List<string>(["system"]) }); } if (doc.activeMobileMenu === undefined) { this.setupActiveMobileMenu(doc); } return [ - { toolTip: "Tap to create a collection in a new pane, drag for a collection", title: "Col", icon: "folder", click: 'openOnRight(getCopy(this.clickFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyCollection as Doc, noviceMode: true, clickFactory: doc.emptyPane as Doc, }, - { toolTip: "Tap to create a webpage in a new pane, drag for a webpage", title: "Web", icon: "globe-asia", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyWebpage as Doc, noviceMode: true }, - { toolTip: "Tap to create a cat image in a new pane, drag for a cat image", title: "Image", icon: "cat", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyImage as Doc }, - { toolTip: "Tap to create a comparison box in a new pane, drag for a comparison box", title: "Compare", icon: "columns", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyComparison as Doc, noviceMode: true }, - { toolTip: "Tap to create a screen grabber in a new pane, drag for a screen grabber", title: "Grab", icon: "photo-video", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyScreenshot as Doc }, - // { title: "Drag a webcam", title: "Cam", icon: "video", ignoreClick: true, drag: 'Docs.Create.WebCamDocument("", { _width: 400, _height: 400, title: "a test cam" })' }, - { toolTip: "Tap to create an audio recorder in a new pane, drag for an audio recorder", title: "Audio", icon: "microphone", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyAudio as Doc, noviceMode: true }, - { toolTip: "Tap to create a button in a new pane, drag for a button", title: "Button", icon: "bolt", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyButton as Doc, noviceMode: true }, - - { toolTip: "Tap to create a presentation in a new pane, drag for a presentation", title: "Present", icon: "tv", click: 'openOnRight(Doc.UserDoc().activePresentation = getCopy(this.dragFactory, true))', drag: `Doc.UserDoc().activePresentation = getCopy(this.dragFactory, true)`, dragFactory: doc.emptyPresentation as Doc, noviceMode: true }, - { toolTip: "Tap to create a search box in a new pane, drag for a search box", title: "Query", icon: "search", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptySearch as Doc }, - { toolTip: "Tap to create a scripting box in a new pane, drag for a scripting box", title: "Script", icon: "terminal", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyScript as Doc }, - // { title: "Drag an import folder", title: "Load", icon: "cloud-upload-alt", ignoreClick: true, drag: 'Docs.Create.DirectoryImportDocument({ title: "Directory Import", _width: 400, _height: 400 })' }, + { toolTip: "Tap to create a note in a new pane, drag for a note", title: "Note", icon: "sticky-note", click: 'openOnRight(copyDragFactory(this.clickFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyNote as Doc, noviceMode: true, clickFactory: doc.emptyNote as Doc, }, + { toolTip: "Tap to create a collection in a new pane, drag for a collection", title: "Col", icon: "folder", click: 'openOnRight(copyDragFactory(this.clickFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyCollection as Doc, noviceMode: true, clickFactory: doc.emptyPane as Doc, }, + { toolTip: "Tap to create a webpage in a new pane, drag for a webpage", title: "Web", icon: "globe-asia", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyWebpage as Doc, noviceMode: true }, + { toolTip: "Tap to create a progressive slide", title: "Slide", icon: "file", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptySlide as Doc, noviceMode: true }, + { toolTip: "Tap to create a cat image in a new pane, drag for a cat image", title: "Image", icon: "cat", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyImage as Doc }, + { toolTip: "Tap to create a comparison box in a new pane, drag for a comparison box", title: "Compare", icon: "columns", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyComparison as Doc, noviceMode: true }, + { toolTip: "Tap to create a screen grabber in a new pane, drag for a screen grabber", title: "Grab", icon: "photo-video", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyScreenshot as Doc }, + { toolTip: "Tap to create an audio recorder in a new pane, drag for an audio recorder", title: "Audio", icon: "microphone", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyAudio as Doc, noviceMode: true }, + { toolTip: "Tap to create a button in a new pane, drag for a button", title: "Button", icon: "bolt", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyButton as Doc }, + { toolTip: "Tap to create a presentation in a new pane, drag for a presentation", title: "Trails", icon: "pres-trail", click: 'openOnRight(Doc.UserDoc().activePresentation = copyDragFactory(this.dragFactory))', drag: `Doc.UserDoc().activePresentation = copyDragFactory(this.dragFactory)`, dragFactory: doc.emptyPresentation as Doc, noviceMode: true }, + { toolTip: "Tap to create a scripting box in a new pane, drag for a scripting box", title: "Script", icon: "terminal", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyScript as Doc }, { toolTip: "Tap to create a mobile view in a new pane, drag for a mobile view", title: "Phone", icon: "mobile", click: 'openOnRight(Doc.UserDoc().activeMobileMenu)', drag: 'this.dragFactory', dragFactory: doc.activeMobileMenu as Doc }, - // { title: "Drag an instance of the device collection", title: "Buxton", icon: "globe-asia", ignoreClick: true, drag: 'Docs.Create.Buxton()' }, - // { title: "use pen", icon: "pen-nib", click: 'activatePen(this.activeInkPen = sameDocs(this.activeInkPen, this) ? undefined : this)', backgroundColor: "blue", ischecked: `sameDocs(this.activeInkPen, this)`, activeInkPen: doc }, - // { title: "use highlighter", icon: "highlighter", click: 'activateBrush(this.activeInkPen = sameDocs(this.activeInkPen, this) ? undefined : this,20,this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activeInkPen, this)`, activeInkPen: doc }, - // { title: "use stamp", icon: "stamp", click: 'activateStamp(this.activeInkPen = sameDocs(this.activeInkPen, this) ? undefined : this)', backgroundColor: "orange", ischecked: `sameDocs(this.activeInkPen, this)`, activeInkPen: doc }, - // { title: "use eraser", icon: "eraser", click: 'activateEraser(this.activeInkPen = sameDocs(this.activeInkPen, this) ? undefined : this);', ischecked: `sameDocs(this.activeInkPen, this)`, backgroundColor: "pink", activeInkPen: doc }, - // { title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activeInkPen = this;', ischecked: `sameDocs(this.activeInkPen, this)`, backgroundColor: "white", activeInkPen: doc }, - { toolTip: "Tap to create a document previewer in a new pane, drag for a document previewer", title: "Prev", icon: "expand", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyDocHolder as Doc }, + { toolTip: "Tap to create a document previewer in a new pane, drag for a document previewer", title: "Prev", icon: "expand", click: 'openOnRight(copyDragFactory(this.dragFactory))', drag: 'copyDragFactory(this.dragFactory)', dragFactory: doc.emptyDocHolder as Doc }, + { toolTip: "Tap to create a custom header note document, drag for a custom header note", title: "Custom", icon: "window-maximize", click: 'openOnRight(delegateDragFactory(this.dragFactory))', drag: 'delegateDragFactory(this.dragFactory)', dragFactory: doc.emptyHeader as Doc }, { toolTip: "Toggle a Calculator REPL", title: "repl", icon: "calculator", click: 'addOverlayWindow("ScriptingRepl", { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" })' }, - { toolTip: "Connect a Google Account", title: "Google Account", icon: "external-link-alt", click: 'GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(true)' }, ]; } - - // setup the "creator" buttons for the sidebar-- eg. the default set of draggable document creation tools static async setupCreatorButtons(doc: Doc) { let alreadyCreatedButtons: string[] = []; @@ -475,7 +480,7 @@ export class CurrentUserUtils { } const buttons = CurrentUserUtils.creatorBtnDescriptors(doc).filter(d => !alreadyCreatedButtons?.includes(d.title)); const creatorBtns = buttons.map(({ title, toolTip, icon, ignoreClick, drag, click, ischecked, activeInkPen, backgroundColor, dragFactory, noviceMode, clickFactory }) => Docs.Create.FontIconDocument({ - _nativeWidth: 50, _nativeHeight: 50, _width: 50, _height: 50, + _nativeWidth: 50, _nativeHeight: 50, _width: 35, _height: 35, icon, title, toolTip, @@ -486,16 +491,19 @@ export class CurrentUserUtils { ischecked: ischecked ? ComputedField.MakeFunction(ischecked) : undefined, activeInkPen, backgroundColor, - removeDropProperties: new List<string>(["dropAction"]), + _hideContextMenu: true, + removeDropProperties: new List<string>(["dropAction", "_stayInCollection"]), + _stayInCollection: true, dragFactory, clickFactory, userDoc: noviceMode ? undefined as any : doc, - hidden: noviceMode ? undefined as any : ComputedField.MakeFunction("self.userDoc.noviceMode"), system: true + hidden: noviceMode ? undefined as any : ComputedField.MakeFunction("self.userDoc.noviceMode"), + system: true })); if (dragCreatorSet === undefined) { doc.myItemCreators = new PrefetchProxy(Docs.Create.MasonryDocument(creatorBtns, { - title: "Basic Item Creators", _showTitle: "title", _xMargin: 0, + title: "Basic Item Creators", _showTitle: "title", _xMargin: 0, _stayInCollection: true, _hideContextMenu: true, _autoHeight: true, _width: 500, _columnWidth: 35, ignoreClick: true, lockedPosition: true, _chromeStatus: "disabled", dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), system: true })); @@ -506,41 +514,45 @@ export class CurrentUserUtils { } static menuBtnDescriptions(doc: Doc): { - title: string, icon: string, click: string, watchedDocuments?: Doc + title: string, target: Doc, icon: string, click: string, watchedDocuments?: Doc }[] { this.setupSharingSidebar(doc); // sets up the right sidebar collection for mobile upload documents and sharing return [ - { title: "Sharing", icon: "users", click: 'selectMainMenu(self)', watchedDocuments: doc["sidebar-sharing"] as Doc }, - { title: "Workspace", icon: "desktop", click: 'selectMainMenu(self)' }, - { title: "Catalog", icon: "file", click: 'selectMainMenu(self)' }, - { title: "Archive", icon: "archive", click: 'selectMainMenu(self)' }, - { title: "Import", icon: "upload", click: 'selectMainMenu(self)' }, - { title: "Tools", icon: "wrench", click: 'selectMainMenu(self)' }, - { title: "Help", icon: "question-circle", click: 'selectMainMenu(self)' }, - { title: "Settings", icon: "cog", click: 'selectMainMenu(self)' }, - { title: "User Doc", icon: "address-card", click: 'selectMainMenu(self)' }, + { title: "Dashboards", target: Cast(doc.myDashboards, Doc, null), icon: "desktop", click: 'selectMainMenu(self)' }, + { title: "Recently Closed", target: Cast(doc.myRecentlyClosedDocs, Doc, null), icon: "archive", click: 'selectMainMenu(self)' }, + { title: "Import", target: Cast(doc.myImportPanel, Doc, null), icon: "upload", click: 'selectMainMenu(self)' }, + { title: "Sharing", target: Cast(doc.mySharedDocs, Doc, null), icon: "users", click: 'selectMainMenu(self)', watchedDocuments: doc.mySharedDocs as Doc }, + { title: "Tools", target: Cast(doc.myTools, Doc, null), icon: "wrench", click: 'selectMainMenu(self)' }, + { title: "Filter", target: Cast(doc.myFilter, Doc, null), icon: "filter", click: 'selectMainMenu(self)' }, + { title: "Pres. Trails", target: Cast(doc.myPresentations, Doc, null), icon: "pres-trail", click: 'selectMainMenu(self)' }, + { title: "Catalog", target: undefined as any, icon: "file", click: 'selectMainMenu(self)' }, + { title: "Help", target: undefined as any, icon: "question-circle", click: 'selectMainMenu(self)' }, + { title: "Settings", target: undefined as any, icon: "cog", click: 'selectMainMenu(self)' }, + { title: "User Doc", target: Cast(doc.myUserDoc, Doc, null), icon: "address-card", click: 'selectMainMenu(self)' }, ]; } static setupSearchPanel(doc: Doc) { - if (doc["search-panel"] === undefined) { - doc["search-panel"] = new PrefetchProxy(Docs.Create.SearchDocument({ - _width: 500, _height: 400, backgroundColor: "dimGray", ignoreClick: true, + if (doc.mySearchPanelDoc === undefined) { + doc.mySearchPanelDoc = new PrefetchProxy(Docs.Create.SearchDocument({ + _width: 500, _height: 300, backgroundColor: "dimGray", ignoreClick: true, _searchDoc: true, childDropAction: "alias", lockedPosition: true, _viewType: CollectionViewType.Schema, _chromeStatus: "disabled", title: "sidebar search stack", system: true })) as any as Doc; } } static setupMenuPanel(doc: Doc) { if (doc.menuStack === undefined) { - const menuBtns = CurrentUserUtils.menuBtnDescriptions(doc).map(({ title, icon, click, watchedDocuments }) => + const menuBtns = CurrentUserUtils.menuBtnDescriptions(doc).map(({ title, target, icon, click, watchedDocuments }) => Docs.Create.FontIconDocument({ icon, iconShape: "square", + _stayInCollection: true, + _hideContextMenu: true, title, + target, _backgroundColor: "black", dropAction: "alias", - removeDropProperties: new List<string>(["dropAction"]), - childDropAction: "same", + removeDropProperties: new List<string>(["dropAction", "_stayInCollection"]), _width: 60, _height: 60, watchedDocuments, @@ -552,6 +564,7 @@ export class CurrentUserUtils { doc.menuStack = new PrefetchProxy(Docs.Create.StackingDocument(menuBtns, { title: "menuItemPanel", + childDropAction: "alias", dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), _backgroundColor: "black", _gridGap: 0, @@ -591,7 +604,7 @@ export class CurrentUserUtils { // SEts up mobile buttons for inside mobile menu static setupMobileButtons(doc?: Doc, buttons?: string[]) { const docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, click?: string, ischecked?: string, activePen?: Doc, backgroundColor?: string, info: string, dragFactory?: Doc }[] = [ - { title: "WORKSPACES", icon: "bars", click: 'switchToMobileLibrary()', backgroundColor: "lightgrey", info: "Access your Workspaces from your mobile, and navigate through all of your documents. " }, + { title: "DASHBOARDS", icon: "bars", click: 'switchToMobileLibrary()', backgroundColor: "lightgrey", info: "Access your Dashboards from your mobile, and navigate through all of your documents. " }, { title: "UPLOAD", icon: "upload", click: 'openMobileUploads()', backgroundColor: "lightgrey", info: "Upload files from your mobile device so they can be accessed on Dash Web." }, { title: "MOBILE UPLOAD", icon: "mobile", click: 'switchToMobileUploadCollection()', backgroundColor: "lightgrey", info: "Access the collection of your mobile uploads." }, { title: "RECORD", icon: "microphone", click: 'openMobileAudio()', backgroundColor: "lightgrey", info: "Use your phone to record, dictate and then upload audio onto Dash Web." }, @@ -627,13 +640,13 @@ export class CurrentUserUtils { // Sets up the title of the button static mobileButtonText = (opts: DocumentOptions, buttonTitle: string) => Docs.Create.TextDocument(buttonTitle, { ...opts, - dropAction: undefined, title: buttonTitle, _fontSize: "37pt", _xMargin: 0, _yMargin: 0, ignoreClick: true, _chromeStatus: "disabled", backgroundColor: "rgba(0,0,0,0)", system: true + dropAction: undefined, title: buttonTitle, _fontSize: "37px", _xMargin: 0, _yMargin: 0, ignoreClick: true, _chromeStatus: "disabled", backgroundColor: "rgba(0,0,0,0)", system: true }) as any as Doc // Sets up the description of the button static mobileButtonInfo = (opts: DocumentOptions, buttonInfo: string) => Docs.Create.TextDocument(buttonInfo, { ...opts, - dropAction: undefined, title: "info", _fontSize: "25pt", _xMargin: 0, _yMargin: 0, ignoreClick: true, _chromeStatus: "disabled", backgroundColor: "rgba(0,0,0,0)", _dimMagnitude: 2, system: true + dropAction: undefined, title: "info", _fontSize: "25px", _xMargin: 0, _yMargin: 0, ignoreClick: true, _chromeStatus: "disabled", backgroundColor: "rgba(0,0,0,0)", _dimMagnitude: 2, system: true }) as any as Doc @@ -688,11 +701,11 @@ export class CurrentUserUtils { } static setupLibrary(userDoc: Doc) { - return CurrentUserUtils.setupWorkspaces(userDoc); + return CurrentUserUtils.setupDashboards(userDoc); } - // setup the Creator button which will display the creator panel. This panel will include the drag creators and the color picker. - // when clicked, this panel will be displayed in the target container (ie, sidebarContainer) + // setup the Creator button which will display the creator panel. This panel will include the drag creators and the color picker. + // when clicked, this panel will be displayed in the target container (ie, sidebarContainer) static async setupToolsBtnPanel(doc: Doc) { // setup a masonry view of all he creators const creatorBtns = await CurrentUserUtils.setupCreatorButtons(doc); @@ -709,98 +722,93 @@ export class CurrentUserUtils { // setup a color picker if (doc.myColorPicker === undefined) { const color = Docs.Create.ColorDocument({ - title: "color picker", _width: 300, dropAction: "alias", forceActive: true, removeDropProperties: new List<string>(["dropAction", "forceActive"]), system: true + title: "color picker", _width: 300, dropAction: "alias", _hideContextMenu: true, _stayInCollection: true, forceActive: true, removeDropProperties: new List<string>(["dropAction", "_stayInCollection", "_hideContextMenu", "forceActive"]), system: true }); doc.myColorPicker = new PrefetchProxy(color); } - if (doc["sidebar-tools"] === undefined) { + if (doc.myTools === undefined) { const toolsStack = new PrefetchProxy(Docs.Create.StackingDocument([doc.myCreators as Doc, doc.myColorPicker as Doc], { - title: "sidebar-tools", _width: 500, _yMargin: 20, lockedPosition: true, _chromeStatus: "disabled", hideFilterView: true, forceActive: true, system: true + title: "My Tools", _width: 500, _yMargin: 20, lockedPosition: true, _chromeStatus: "disabled", forceActive: true, system: true, _stayInCollection: true, _hideContextMenu: true, })) as any as Doc; - doc["sidebar-tools"] = toolsStack; + doc.myTools = toolsStack; } } - static async setupWorkspaces(doc: Doc) { - // setup workspaces library item - await doc.myWorkspaces; - if (doc.myWorkspaces === undefined) { - doc.myWorkspaces = new PrefetchProxy(Docs.Create.TreeDocument([], { - title: "WORKSPACES", _height: 100, forceActive: true, boxShadow: "0 0", lockedPosition: true, treeViewOpen: true, system: true - })); - } - if (doc["sidebar-workspaces"] === undefined) { - const newWorkspace = ScriptField.MakeScript(`createNewWorkspace()`); - (doc.myWorkspaces as Doc).contextMenuScripts = new List<ScriptField>([newWorkspace!]); - (doc.myWorkspaces as Doc).contextMenuLabels = new List<string>(["Create New Workspace"]); - - const workspaces = doc.myWorkspaces as Doc; - - doc["sidebar-workspaces"] = new PrefetchProxy(Docs.Create.TreeDocument([workspaces], { + static async setupDashboards(doc: Doc) { + // setup dashboards library item + await doc.myDashboards; + if (doc.myDashboards === undefined) { + doc.myDashboards = new PrefetchProxy(Docs.Create.TreeDocument([], { + title: "My Dashboards", _height: 400, treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "alias", - treeViewTruncateTitleWidth: 150, hideFilterView: true, treeViewPreventOpen: false, treeViewOpen: true, + treeViewTruncateTitleWidth: 150, treeViewPreventOpen: false, lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true, targetDropAction: "same", system: true - })) as any as Doc; + })); + const newDashboard = ScriptField.MakeScript(`createNewDashboard(Doc.UserDoc())`); + (doc.myDashboards as any as Doc).contextMenuScripts = new List<ScriptField>([newDashboard!]); + (doc.myDashboards as any as Doc).contextMenuLabels = new List<string>(["Create New Dashboard"]); } - return doc.myWorkspaces as any as Doc; + return doc.myDashboards as any as Doc; } - static setupCatalog(doc: Doc) { - doc.myCatalog === undefined; - if (doc.myCatalog === undefined) { - doc.myCatalog = new PrefetchProxy(Docs.Create.SchemaDocument([], [], { - title: "CATALOG", _height: 1000, _fitWidth: true, forceActive: true, boxShadow: "0 0", treeViewPreventOpen: false, - childDropAction: "alias", targetDropAction: "same", _stayInCollection: true, treeViewOpen: true, system: true + static async setupPresentations(doc: Doc) { + await doc.myPresentations; + if (doc.myPresentations === undefined) { + doc.myPresentations = new PrefetchProxy(Docs.Create.TreeDocument([], { + title: "My Presentations", _height: 100, + treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "alias", + treeViewTruncateTitleWidth: 150, treeViewPreventOpen: false, + lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true, targetDropAction: "same", system: true })); + const newPresentations = ScriptField.MakeScript(`createNewPresentation()`); + (doc.myPresentations as any as Doc).contextMenuScripts = new List<ScriptField>([newPresentations!]); + (doc.myPresentations as any as Doc).contextMenuLabels = new List<string>(["Create New Presentation"]); + const presentations = doc.myPresentations as any as Doc; } + return doc.myPresentations as any as Doc; + } - if (doc["sidebar-catalog"] === undefined) { - const catalog = doc.myCatalog as Doc; - - doc["sidebar-catalog"] = new PrefetchProxy(Docs.Create.TreeDocument([catalog], { - title: "sidebar-catalog", + static setupRecentlyClosedDocs(doc: Doc) { + // setup Recently Closed library item + doc.myRecentlyClosedDocs === undefined; + if (doc.myRecentlyClosedDocs === undefined) { + doc.myRecentlyClosedDocs = new PrefetchProxy(Docs.Create.TreeDocument([], { + title: "Recently Closed", _height: 500, treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "alias", - treeViewTruncateTitleWidth: 150, hideFilterView: true, treeViewPreventOpen: false, treeViewOpen: true, + treeViewTruncateTitleWidth: 150, treeViewPreventOpen: false, lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true, targetDropAction: "same", system: true - })) as any as Doc; + })); + const clearAll = ScriptField.MakeScript(`getProto(self).data = new List([])`); + (doc.myRecentlyClosedDocs as any as Doc).contextMenuScripts = new List<ScriptField>([clearAll!]); + (doc.myRecentlyClosedDocs as any as Doc).contextMenuLabels = new List<string>(["Clear All"]); } } - static setupRecentlyClosed(doc: Doc) { + static setupFilterDocs(doc: Doc) { // setup Recently Closed library item - doc.myRecentlyClosed === undefined; - if (doc.myRecentlyClosed === undefined) { - doc.myRecentlyClosed = new PrefetchProxy(Docs.Create.TreeDocument([], { - title: "RECENTLY CLOSED", _height: 75, forceActive: true, boxShadow: "0 0", treeViewPreventOpen: false, treeViewOpen: true, _stayInCollection: true, system: true - })); - } - // this is equivalent to using PrefetchProxies to make sure the recentlyClosed doc is ready - PromiseValue(Cast(doc.myRecentlyClosed, Doc)).then(recent => recent && PromiseValue(recent.data).then(DocListCast)); - if (doc["sidebar-recentlyClosed"] === undefined) { - const clearAll = ScriptField.MakeScript(`self.data = new List([])`); - (doc.myRecentlyClosed as Doc).contextMenuScripts = new List<ScriptField>([clearAll!]); - (doc.myRecentlyClosed as Doc).contextMenuLabels = new List<string>(["Clear All"]); - - const recentlyClosed = doc.myRecentlyClosed as Doc; - - doc["sidebar-recentlyClosed"] = new PrefetchProxy(Docs.Create.TreeDocument([recentlyClosed], { - title: "sidebar-recentlyClosed", - treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "alias", - treeViewTruncateTitleWidth: 150, hideFilterView: true, treeViewPreventOpen: false, treeViewOpen: true, + doc.myFilter === undefined; + if (doc.myFilter === undefined) { + doc.myFilter = new PrefetchProxy(Docs.Create.FilterDocument({ + title: "FilterDoc", _height: 500, + treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "none", + treeViewTruncateTitleWidth: 150, treeViewPreventOpen: false, lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true, targetDropAction: "same", system: true - })) as any as Doc; + })); + const clearAll = ScriptField.MakeScript(`getProto(self).data = new List([])`); + (doc.myFilter as any as Doc).contextMenuScripts = new List<ScriptField>([clearAll!]); + (doc.myFilter as any as Doc).contextMenuLabels = new List<string>(["Clear All"]); } } static setupUserDoc(doc: Doc) { - if (doc["sidebar-userDoc"] === undefined) { + if (doc.myUserDoc === undefined) { doc.treeViewOpen = true; doc.treeViewExpandedView = "fields"; - doc["sidebar-userDoc"] = new PrefetchProxy(Docs.Create.TreeDocument([doc], { - treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, title: "sidebar-userDoc", - treeViewTruncateTitleWidth: 150, hideFilterView: true, treeViewPreventOpen: false, + doc.myUserDoc = new PrefetchProxy(Docs.Create.TreeDocument([doc], { + treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, title: "My UserDoc", + treeViewTruncateTitleWidth: 150, treeViewPreventOpen: false, lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true, targetDropAction: "same", system: true })) as any as Doc; } @@ -810,7 +818,6 @@ export class CurrentUserUtils { if (doc.sidebar === undefined) { const sidebarContainer = new Doc(); sidebarContainer._chromeStatus = "disabled"; - sidebarContainer.onClick = ScriptField.MakeScript("freezeSidebar()"); sidebarContainer.system = true; doc.sidebar = new PrefetchProxy(sidebarContainer); } @@ -821,9 +828,10 @@ export class CurrentUserUtils { static async setupSidebarButtons(doc: Doc) { CurrentUserUtils.setupSidebarContainer(doc); await CurrentUserUtils.setupToolsBtnPanel(doc); - CurrentUserUtils.setupWorkspaces(doc); - CurrentUserUtils.setupCatalog(doc); - CurrentUserUtils.setupRecentlyClosed(doc); + CurrentUserUtils.setupDashboards(doc); + CurrentUserUtils.setupPresentations(doc); + CurrentUserUtils.setupRecentlyClosedDocs(doc); + CurrentUserUtils.setupFilterDocs(doc); CurrentUserUtils.setupUserDoc(doc); } @@ -834,16 +842,16 @@ export class CurrentUserUtils { })) as any as Doc static ficon = (opts: DocumentOptions) => new PrefetchProxy(Docs.Create.FontIconDocument({ - ...opts, dropAction: "alias", removeDropProperties: new List<string>(["dropAction"]), _nativeWidth: 40, _nativeHeight: 40, _width: 40, _height: 40, system: true + ...opts, dropAction: "alias", removeDropProperties: new List<string>(["dropAction", "stayInCollection"]), _nativeWidth: 40, _nativeHeight: 40, _width: 40, _height: 40, system: true })) as any as Doc /// sets up the default list of buttons to be shown in the expanding button menu at the bottom of the Dash window static setupDockedButtons(doc: Doc) { if (doc["dockedBtn-undo"] === undefined) { - doc["dockedBtn-undo"] = CurrentUserUtils.ficon({ onClick: ScriptField.MakeScript("undo()"), toolTip: "click to undo", title: "undo", icon: "undo-alt", system: true }); + doc["dockedBtn-undo"] = CurrentUserUtils.ficon({ onClick: ScriptField.MakeScript("undo()"), _stayInCollection: true, dropAction: "alias", _hideContextMenu: true, removeDropProperties: new List<string>(["dropAction", "_hideContextMenu", "stayInCollection"]), toolTip: "click to undo", title: "undo", icon: "undo-alt", system: true }); } if (doc["dockedBtn-redo"] === undefined) { - doc["dockedBtn-redo"] = CurrentUserUtils.ficon({ onClick: ScriptField.MakeScript("redo()"), toolTip: "click to redo", title: "redo", icon: "redo-alt", system: true }); + doc["dockedBtn-redo"] = CurrentUserUtils.ficon({ onClick: ScriptField.MakeScript("redo()"), _stayInCollection: true, dropAction: "alias", _hideContextMenu: true, removeDropProperties: new List<string>(["dropAction", "_hideContextMenu", "stayInCollection"]), toolTip: "click to redo", title: "redo", icon: "redo-alt", system: true }); } if (doc.dockedBtns === undefined) { doc.dockedBtns = CurrentUserUtils.blist({ title: "docked buttons", ignoreClick: true }, [doc["dockedBtn-undo"] as Doc, doc["dockedBtn-redo"] as Doc]); @@ -851,8 +859,8 @@ export class CurrentUserUtils { } // sets up the default set of documents to be shown in the Overlay layer static setupOverlays(doc: Doc) { - if (doc.myOverlayDocuments === undefined) { - doc.myOverlayDocuments = new PrefetchProxy(Docs.Create.FreeformDocument([], { title: "overlay documents", backgroundColor: "#aca3a6", system: true })); + if (doc.myOverlayDocs === undefined) { + doc.myOverlayDocs = new PrefetchProxy(Docs.Create.FreeformDocument([], { title: "overlay documents", backgroundColor: "#aca3a6", system: true })); } } @@ -867,27 +875,29 @@ export class CurrentUserUtils { // Sharing sidebar is where shared documents are contained static setupSharingSidebar(doc: Doc) { - if (doc["sidebar-sharing"] === undefined) { - doc["sidebar-sharing"] = new PrefetchProxy(Docs.Create.StackingDocument([], { title: "Shared Documents", childDropAction: "alias", system: true, _yMargin: 30, _showTitle: "title", ignoreClick: true, lockedPosition: true })); + if (doc.mySharedDocs === undefined) { + doc.mySharedDocs = new PrefetchProxy(Docs.Create.StackingDocument([], { title: "My SharedDocs", childDropAction: "alias", system: true, contentPointerEvents: "none", childLimitHeight: 0, _yMargin: 50, _gridGap: 15, _showTitle: "title", ignoreClick: true, lockedPosition: true })); } } // Import sidebar is where shared documents are contained static setupImportSidebar(doc: Doc) { - if (doc["sidebar-import-documents"] === undefined) { - doc["sidebar-import-documents"] = new PrefetchProxy(Docs.Create.StackingDocument([], { title: "Imported Documents", forceActive: true, _showTitle: "title", childDropAction: "alias", _autoHeight: true, _yMargin: 30, lockedPosition: true, _chromeStatus: "disabled", system: true })); + if (doc.myImportDocs === undefined) { + doc.myImportDocs = new PrefetchProxy(Docs.Create.StackingDocument([], { + title: "My ImportDocuments", forceActive: true, ignoreClick: true, _showTitle: "title", _stayInCollection: true, _hideContextMenu: true, childLimitHeight: 0, + childDropAction: "alias", _autoHeight: true, _yMargin: 50, _gridGap: 15, lockedPosition: true, _chromeStatus: "disabled", system: true + })); } - if (doc["sidebar-import"] === undefined) { - const uploads = Cast(doc["sidebar-import-documents"], Doc, null); - const newUpload = CurrentUserUtils.ficon({ onClick: ScriptField.MakeScript("importDocument()"), toolTip: "Import External document", _backgroundColor: "black", title: "Import", icon: "upload", system: true }); - doc["sidebar-import"] = new PrefetchProxy(Docs.Create.StackingDocument([newUpload, uploads], { title: "Imported Documents", _yMargin: 20, ignoreClick: true, lockedPosition: true, system: true })); + if (doc.myImportPanel === undefined) { + const uploads = Cast(doc.myImportDocs, Doc, null); + const newUpload = CurrentUserUtils.ficon({ onClick: ScriptField.MakeScript("importDocument()"), toolTip: "Import External document", _backgroundColor: "black", _stayInCollection: true, _hideContextMenu: true, title: "Import", icon: "upload", system: true }); + doc.myImportPanel = new PrefetchProxy(Docs.Create.StackingDocument([newUpload, uploads], { title: "My ImportPanel", _yMargin: 20, ignoreClick: true, _stayInCollection: true, _hideContextMenu: true, lockedPosition: true, system: true })); } } - static setupClickEditorTemplates(doc: Doc) { if (doc["clickFuncs-child"] === undefined) { - // to use this function, select it from the context menu of a collection. then edit the onChildClick script. Add two Doc variables: 'target' and 'thisContainer', then assign 'target' to some target collection. After that, clicking on any document in the initial collection will open it in the target + // to use this function, select it from the context menu of a collection. then edit the onChildClick script. Add two Doc variables: 'target' and 'thisContainer', then assign 'target' to some target collection. After that, clicking on any document in the initial collection will open it in the target const openInTarget = Docs.Create.ScriptingDocument(ScriptField.MakeScript( "docCast(thisContainer.target).then((target) => target && (target.proto.data = new List([self]))) ", { thisContainer: Doc.name }), { @@ -937,6 +947,8 @@ export class CurrentUserUtils { doc.system = true; doc.noviceMode = doc.noviceMode === undefined ? "true" : doc.noviceMode; doc.title = Doc.CurrentUserEmail; + doc.userColor = doc.userColor || "#12121233"; + doc._raiseWhenDragged = true; doc.activeInkPen = doc; doc.activeInkColor = StrCast(doc.activeInkColor, "rgb(0, 0, 0)"); doc.activeInkWidth = StrCast(doc.activeInkWidth, "1"); @@ -945,7 +957,7 @@ export class CurrentUserUtils { doc.activeArrowStart = StrCast(doc.activeArrowStart, ""); doc.activeArrowEnd = StrCast(doc.activeArrowEnd, ""); doc.activeDash = StrCast(doc.activeDash, "0"); - doc.fontSize = StrCast(doc.fontSize, "12pt"); + doc.fontSize = StrCast(doc.fontSize, "12px"); doc.fontFamily = StrCast(doc.fontFamily, "Arial"); doc.fontColor = StrCast(doc.fontColor, "black"); doc.fontHighlight = StrCast(doc.fontHighlight, ""); @@ -960,11 +972,11 @@ export class CurrentUserUtils { this.setupDocTemplates(doc); // sets up the template menu of templates this.setupImportSidebar(doc); this.setupActiveMobileMenu(doc); // sets up the current mobile menu for Dash Mobile - this.setupMenuPanel(doc); this.setupSearchPanel(doc); this.setupOverlays(doc); // documents in overlay layer this.setupDockedButtons(doc); // the bottom bar of font icons await this.setupSidebarButtons(doc); // the pop-out left sidebar of tools/panels + this.setupMenuPanel(doc); doc.globalLinkDatabase = Docs.Prototypes.MainLinkDocument(); doc.globalScriptDatabase = Docs.Prototypes.MainScriptDocument(); doc.globalGroupDatabase = Docs.Prototypes.MainGroupDocument(); @@ -975,8 +987,18 @@ export class CurrentUserUtils { doc["dockedBtn-undo"] && reaction(() => UndoManager.undoStack.slice(), () => Doc.GetProto(doc["dockedBtn-undo"] as Doc).opacity = UndoManager.CanUndo() ? 1 : 0.4, { fireImmediately: true }); doc["dockedBtn-redo"] && reaction(() => UndoManager.redoStack.slice(), () => Doc.GetProto(doc["dockedBtn-redo"] as Doc).opacity = UndoManager.CanRedo() ? 1 : 0.4, { fireImmediately: true }); + // uncomment this to setup a default note style that uses the custom header layout + // PromiseValue(doc.emptyHeader).then(factory => { + // if (Cast(doc.defaultTextLayout, Doc, null)?.version !== headerViewVersion) { + // const deleg = Doc.delegateDragFactory(factory as Doc); + // deleg.title = "header"; + // doc.defaultTextLayout = new PrefetchProxy(deleg); + // Doc.AddDocToList(Cast(doc["template-notes"], Doc, null), "data", deleg); + // } + // }); return doc; } + public static async loadCurrentUser() { return rp.get(Utils.prepend("/getCurrentUser")).then(response => { if (response) { @@ -1000,12 +1022,156 @@ export class CurrentUserUtils { } }); } -} -Scripting.addGlobal(function createNewWorkspace() { return MainView.Instance.createNewWorkspace(); }, - "creates a new workspace when called"); + public static _urlState: HistoryUtil.DocUrl; + + public static openDashboard = (userDoc: Doc, doc: Doc, fromHistory = false) => { + CurrentUserUtils.MainDocId = doc[Id]; + + if (doc) { // this has the side-effect of setting the main container since we're assigning the active/guest dashboard + !("presentationView" in doc) && (doc.presentationView = new List<Doc>([Docs.Create.TreeDocument([], { title: "Presentation" })])); + userDoc ? (userDoc.activeDashboard = doc) : (CurrentUserUtils.GuestDashboard = doc); + } + const state = CurrentUserUtils._urlState; + if (state.sharing === true && !userDoc) { + DocServer.Control.makeReadOnly(); + } else { + fromHistory || HistoryUtil.pushState({ + type: "doc", + docId: doc[Id], + readonly: state.readonly, + nro: state.nro, + sharing: false, + }); + if (state.readonly === true || state.readonly === null) { + DocServer.Control.makeReadOnly(); + } else if (state.safe) { + if (!state.nro) { + DocServer.Control.makeReadOnly(); + } + CollectionView.SetSafeMode(true); + } else if (state.nro || state.nro === null || state.readonly === false) { + } else if (doc.readOnly) { + DocServer.Control.makeReadOnly(); + } else { + DocServer.Control.makeEditable(); + } + } + + return true; + } + public static importDocument = () => { + const input = document.createElement("input"); + input.type = "file"; + input.multiple = true; + input.accept = ".zip, application/pdf, video/*, image/*, audio/*"; + input.onchange = async _e => { + const upload = Utils.prepend("/uploadDoc"); + const formData = new FormData(); + const file = input.files && input.files[0]; + if (file && file.type === 'application/zip') { + formData.append('file', file); + formData.append('remap', "true"); + const response = await fetch(upload, { method: "POST", body: formData }); + const json = await response.json(); + if (json !== "error") { + const doc = await DocServer.GetRefField(json); + if (doc instanceof Doc) { + setTimeout(() => SearchUtil.Search(`{!join from=id to=proto_i}id:link*`, true, {}).then(docs => + docs.docs.forEach(d => LinkManager.Instance.addLink(d))), 2000); // need to give solr some time to update so that this query will find any link docs we've added. + } + } + } else if (input.files && input.files.length !== 0) { + const importDocs = Cast(Doc.UserDoc().myImportDocs, Doc, null); + const disposer = OverlayView.ShowSpinner(); + DocListCastAsync(importDocs.data).then(async list => { + const results = await DocUtils.uploadFilesToDocs(Array.from(input.files || []), {}); + list?.splice(0, 0, ...results); + disposer(); + }); + } else { + console.log("No file selected"); + } + }; + input.click(); + } + + public static snapshotDashboard = (userDoc: Doc) => { + const copy = CollectionDockingView.Copy(CurrentUserUtils.ActiveDashboard); + Doc.AddDocToList(Cast(userDoc.myDashboards, Doc, null), "data", copy); + CurrentUserUtils.openDashboard(userDoc, copy); + } + + public static createNewDashboard = (userDoc: Doc, id?: string) => { + const myPresentations = userDoc.myPresentations as Doc; + const presentation = Doc.MakeCopy(userDoc.emptyPresentation as Doc, true); + const dashboards = Cast(userDoc.myDashboards, Doc) as Doc; + const dashboardCount = DocListCast(dashboards.data).length + 1; + const emptyPane = Cast(userDoc.emptyPane, Doc, null); + emptyPane["dragFactory-count"] = NumCast(emptyPane["dragFactory-count"]) + 1; + const freeformOptions: DocumentOptions = { + x: 0, + y: 400, + _width: 1500, + _height: 1000, + title: `Untitled Tab ${NumCast(emptyPane["dragFactory-count"])}`, + }; + const freeformDoc = CurrentUserUtils.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions); + const dashboardDoc = Docs.Create.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600 }], { title: `Dashboard ${dashboardCount}` }, id, "row"); + Doc.AddDocToList(myPresentations, "data", presentation); + userDoc.activePresentation = presentation; + const toggleTheme = ScriptField.MakeScript(`self.darkScheme = !self.darkScheme`); + const toggleComic = ScriptField.MakeScript(`toggleComicMode()`); + const snapshotDashboard = ScriptField.MakeScript(`snapshotDashboard()`); + const createDashboard = ScriptField.MakeScript(`createNewDashboard()`); + dashboardDoc.contextMenuScripts = new List<ScriptField>([toggleTheme!, toggleComic!, snapshotDashboard!, createDashboard!]); + dashboardDoc.contextMenuLabels = new List<string>(["Toggle Theme Colors", "Toggle Comic Mode", "Snapshot Dashboard", "Create Dashboard"]); + + Doc.AddDocToList(dashboards, "data", dashboardDoc); + CurrentUserUtils.openDashboard(userDoc, dashboardDoc); + } + + public static GetNewTextDoc(title: string, x: number, y: number, width?: number, height?: number, noMargins?: boolean) { + const tbox = Docs.Create.TextDocument("", { + _xMargin: noMargins ? 0 : undefined, _yMargin: noMargins ? 0 : undefined, + _width: width || 200, _height: height || 100, x: x, y: y, _autoHeight: true, _fontSize: StrCast(Doc.UserDoc().fontSize), + _fontFamily: StrCast(Doc.UserDoc().fontFamily), title + }); + const template = FormattedTextBox.DefaultLayout; + if (template instanceof Doc) { + tbox._width = NumCast(template._width); + tbox.layoutKey = "layout_" + StrCast(template.title); + Doc.GetProto(tbox)[StrCast(tbox.layoutKey)] = template; + } + return tbox; + } + + public static get MySearchPanelDoc() { return Cast(Doc.UserDoc().mySearchPanelDoc, Doc, null); } + public static get ActiveDashboard() { return Cast(Doc.UserDoc().activeDashboard, Doc, null); } + public static get ActivePresentation() { return Cast(Doc.UserDoc().activePresentation, Doc, null); } + public static get MyRecentlyClosed() { return Cast(Doc.UserDoc().myRecentlyClosedDocs, Doc, null); } + public static get MyDashboards() { return Cast(Doc.UserDoc().myDashboards, Doc, null); } + public static get EmptyPane() { return Cast(Doc.UserDoc().emptyPane, Doc, null); } +} + +Scripting.addGlobal(function openDragFactory(dragFactory: Doc) { + const copy = Doc.copyDragFactory(dragFactory); + if (copy) { + CollectionDockingView.AddSplit(copy, "right"); + const view = DocumentManager.Instance.getFirstDocumentView(copy); + view && SelectionManager.SelectDoc(view, false); + } +}); +Scripting.addGlobal(function snapshotDashboard() { CurrentUserUtils.snapshotDashboard(Doc.UserDoc()); }, + "creates a snapshot copy of a dashboard"); +Scripting.addGlobal(function createNewDashboard() { return CurrentUserUtils.createNewDashboard(Doc.UserDoc()); }, + "creates a new dashboard when called"); +Scripting.addGlobal(function createNewPresentation() { return MainView.Instance.createNewPresentation(); }, + "creates a new presentation when called"); Scripting.addGlobal(function links(doc: any) { return new List(LinkManager.Instance.getAllRelatedLinks(doc)); }, "returns all the links to the document or its annotations", "(doc: any)"); Scripting.addGlobal(function directLinks(doc: any) { return new List(LinkManager.Instance.getAllDirectLinks(doc)); }, "returns all the links directly to the document", "(doc: any)"); +Scripting.addGlobal(function importDocument() { return CurrentUserUtils.importDocument(); }, + "imports files from device directly into the import sidebar"); diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts index 540540642..231e1fa8d 100644 --- a/src/client/util/DictationManager.ts +++ b/src/client/util/DictationManager.ts @@ -6,7 +6,6 @@ import { DocumentType } from "../documents/DocumentTypes"; import { Doc, Opt } from "../../fields/Doc"; import { List } from "../../fields/List"; import { Docs } from "../documents/Documents"; -import { CollectionViewType } from "../views/collections/CollectionView"; import { Cast, CastCtor } from "../../fields/Types"; import { listSpec } from "../../fields/Schema"; import { AudioField, ImageField } from "../../fields/URLField"; @@ -88,7 +87,7 @@ export namespace DictationManager { export const listen = async (options?: Partial<ListeningOptions>) => { let results: string | undefined; - const overlay = options !== undefined && options.useOverlay; + const overlay = options?.useOverlay; if (overlay) { DictationOverlay.Instance.dictationOverlayVisible = true; DictationOverlay.Instance.isListening = { interim: false }; @@ -100,11 +99,11 @@ export namespace DictationManager { Utils.CopyText(results); if (overlay) { DictationOverlay.Instance.isListening = false; - const execute = options && options.tryExecute; + const execute = options?.tryExecute; DictationOverlay.Instance.dictatedPhrase = execute ? results.toLowerCase() : results; DictationOverlay.Instance.dictationSuccess = execute ? await DictationManager.Commands.execute(results) : true; } - options && options.tryExecute && await DictationManager.Commands.execute(results); + options?.tryExecute && await DictationManager.Commands.execute(results); } } catch (e) { if (overlay) { @@ -129,12 +128,12 @@ export namespace DictationManager { } isListening = true; - const handler = options ? options.interimHandler : undefined; - const continuous = options ? options.continuous : undefined; + const handler = options?.interimHandler; + const continuous = options?.continuous; const indefinite = continuous && continuous.indefinite; - const language = options ? options.language : undefined; - const intra = options && options.delimiters ? options.delimiters.intra : undefined; - const inter = options && options.delimiters ? options.delimiters.inter : undefined; + const language = options?.language; + const intra = options?.delimiters?.intra; + const inter = options?.delimiters?.inter; recognizer.onstart = () => console.log("initiating speech recognition session..."); recognizer.interimResults = handler !== undefined; @@ -154,7 +153,7 @@ export namespace DictationManager { recognizer.onresult = (e: SpeechRecognitionEvent) => { current = synthesize(e, intra); let matchedTerminator: string | undefined; - if (options && options.terminators && (matchedTerminator = options.terminators.find(end => current ? current.trim().toLowerCase().endsWith(end.toLowerCase()) : false))) { + if (options?.terminators && (matchedTerminator = options.terminators.find(end => current ? current.trim().toLowerCase().endsWith(end.toLowerCase()) : false))) { current = matchedTerminator; recognizer.abort(); return complete(); @@ -324,7 +323,7 @@ export namespace DictationManager { ["open fields", { action: (target: DocumentView) => { const kvp = Docs.Create.KVPDocument(target.props.Document, { _width: 300, _height: 300 }); - target.props.addDocTab(kvp, "onRight"); + target.props.addDocTab(kvp, "add:right"); } }], @@ -338,7 +337,7 @@ export namespace DictationManager { const proseMirrorState = `{"doc":{"type":"doc","content":[{"type":"ordered_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"type":"text","text":"${prompt}"}]}]}]}]},"selection":{"type":"text","anchor":${anchor},"head":${head}}}`; proto.data = new RichTextField(proseMirrorState); proto.backgroundColor = "#eeffff"; - target.props.addDocTab(newBox, "onRight"); + target.props.addDocTab(newBox, "add:right"); } }] diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 962294933..2ca29cb7e 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -1,15 +1,15 @@ -import { action, computed, observable } from 'mobx'; -import { Doc, DocListCastAsync, DocListCast, Opt } from '../../fields/Doc'; +import { action, observable } from 'mobx'; +import { Doc, DocListCast, DocListCastAsync, Opt } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; import { Cast, NumCast, StrCast } from '../../fields/Types'; +import { returnFalse } from '../../Utils'; +import { DocumentType } from '../documents/DocumentTypes'; import { CollectionDockingView } from '../views/collections/CollectionDockingView'; import { CollectionView } from '../views/collections/CollectionView'; -import { DocumentView, DocFocusFunc } from '../views/nodes/DocumentView'; +import { DocumentView } from '../views/nodes/DocumentView'; import { LinkManager } from './LinkManager'; import { Scripting } from './Scripting'; import { SelectionManager } from './SelectionManager'; -import { DocumentType } from '../documents/DocumentTypes'; -import { TraceMobx } from '../../fields/util'; export type CreateViewFunc = (doc: Doc, followLinkLocation: string, finished?: () => void) => void; @@ -18,6 +18,7 @@ export class DocumentManager { //global holds all of the nodes (regardless of which collection they're in) @observable public DocumentViews: DocumentView[] = []; + @observable LinkedDocumentViews: { a: DocumentView, b: DocumentView, l: Doc }[] = []; // singleton instance private static _instance: DocumentManager; @@ -31,6 +32,26 @@ export class DocumentManager { private constructor() { } + @action + public AddView = (view: DocumentView) => { + const linksList = DocListCast(view.props.Document.links); + linksList.forEach(link => { + const linkToDoc = link && LinkManager.getOppositeAnchor(link, view.props.Document); + linkToDoc && DocumentManager.Instance.DocumentViews.filter(dv => Doc.AreProtosEqual(dv.props.Document, linkToDoc)).forEach(dv => { + if (dv.props.Document.type !== DocumentType.LINK || dv.props.LayoutTemplateString !== view.props.LayoutTemplateString) { + this.LinkedDocumentViews.push({ a: dv, b: view, l: link }); + } + }); + }); + this.DocumentViews.push(view); + } + public RemoveView = (view: DocumentView) => { + const index = this.DocumentViews.indexOf(view); + index !== -1 && this.DocumentViews.splice(index, 1); + + this.LinkedDocumentViews.slice().forEach(action((pair, i) => pair.a === view || pair.b === view ? this.LinkedDocumentViews.splice(i, 1) : null)); + } + //gets all views public getDocumentViewsById(id: string) { const toReturn: DocumentView[] = []; @@ -86,7 +107,8 @@ export class DocumentManager { } public getFirstDocumentView = (toFind: Doc, originatingDoc: Opt<Doc> = undefined): DocumentView | undefined => { - return this.getDocumentViews(toFind)?.find(view => view.props.Document !== originatingDoc); + const views = this.getDocumentViews(toFind).filter(view => view.props.Document !== originatingDoc); + return views?.find(view => view.props.focus !== returnFalse) || (views.length ? views[0] : undefined); } public getDocumentViews(toFind: Doc): DocumentView[] { const toReturn: DocumentView[] = []; @@ -103,28 +125,9 @@ export class DocumentManager { return toReturn; } - @computed - public get LinkedDocumentViews() { - TraceMobx(); - const pairs = DocumentManager.Instance.DocumentViews.reduce((pairs, dv) => { - const linksList = DocListCast(dv.props.Document.links); - pairs.push(...linksList.reduce((pairs, link) => { - const linkToDoc = link && LinkManager.Instance.getOppositeAnchor(link, dv.props.Document); - linkToDoc && DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 => { - if (dv.props.Document.type !== DocumentType.LINK || dv.props.LayoutTemplateString !== docView1.props.LayoutTemplateString) { - pairs.push({ a: dv, b: docView1, l: link }); - } - }); - return pairs; - }, [] as { a: DocumentView, b: DocumentView, l: Doc }[])); - return pairs; - }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]); - - return pairs; - } static addRightSplit = (doc: Doc, finished?: () => void) => { - CollectionDockingView.AddRightSplit(doc); + CollectionDockingView.AddSplit(doc, "right"); finished?.(); } public jumpToDocument = async ( @@ -160,6 +163,7 @@ export class DocumentManager { docView.props.Document.hidden = !docView.props.Document.hidden; } else { + docView.select(false); docView.props.Document.hidden && (docView.props.Document.hidden = undefined); docView.props.focus(docView.props.Document, willZoom, undefined, focusAndFinish); highlight(); @@ -174,36 +178,35 @@ export class DocumentManager { highlight(); } else { // otherwise try to get a view of the context of the target const targetDocContextView = getFirstDocView(targetDocContext); - targetDocContext._scrollY = 0; // this will force PDFs to activate and load their annotations / allow scrolling + targetDocContext._scrollY = NumCast(targetDocContext._scrollTop, 0); // this will force PDFs to activate and load their annotations / allow scrolling if (targetDocContextView) { // we found a context view and aren't forced to create a new one ... focus on the context first.. targetDocContext._viewTransition = "transform 500ms"; targetDocContextView.props.focus(targetDocContextView.props.Document, willZoom); // now find the target document within the context if (targetDoc.displayTimecode) { // if the target has a timecode, it should show up once the (presumed) video context scrubs to the display timecode; - targetDocContext.currentTimecode = targetDoc.displayTimecode; + targetDocContext._currentTimecode = targetDoc.displayTimecode; finished?.(); } else { // no timecode means we need to find the context view and focus on our target - setTimeout(() => { + const findView = (delay: number) => { const retryDocView = getFirstDocView(targetDoc); // test again for the target view snce we presumably created the context above by focusing on it if (retryDocView) { // we found the target in the context retryDocView.props.focus(targetDoc, willZoom, undefined, focusAndFinish); // focus on the target in the context - } else { // we didn't find the target, so it must have moved out of the context. Go back to just creating it. + highlight(); + } + if (delay > 2500) { + // we didn't find the target, so it must have moved out of the context. Go back to just creating it. if (closeContextIfNotFound) targetDocContextView.props.removeDocument?.(targetDocContextView.props.Document); - targetDoc.layout && createViewFunc(Doc.BrushDoc(targetDoc), finished); // create a new view of the target + // targetDoc.layout && createViewFunc(Doc.BrushDoc(targetDoc), finished); // create a new view of the target + } else { + setTimeout(() => findView(delay + 250), 250); } - highlight(); - }, 0); + }; + findView(0); } - } else { // there's no context view so we need to create one first and try again - createViewFunc(targetDocContext); // so first we create the target, but don't pass finished because we still need to create the target - setTimeout(() => { - const finalDocView = getFirstDocView(targetDoc); - const finalDocContextView = getFirstDocView(targetDocContext); - setTimeout(() => // if not, wait a bit to see if the context can be loaded (e.g., a PDF). wait interval heurisitic tries to guess how we're animating based on what's just become visible - this.jumpToDocument(targetDoc, willZoom, createViewFunc, undefined, linkDoc, true, undefined, finished), // pass true this time for closeContextIfNotFound - finalDocView ? 0 : finalDocContextView ? 250 : 2000); // so call jump to doc again and if the doc isn't found, it will be created. - }, 0); + } else { // there's no context view so we need to create one first and try again when that finishes + createViewFunc(targetDocContext, // after creating the context, this calls the finish function that will retry looking for the target + () => this.jumpToDocument(targetDoc, willZoom, createViewFunc, undefined, linkDoc, true /* if we don't find the target, we want to get rid of the context just created */, undefined, finished)); } } } @@ -212,8 +215,8 @@ export class DocumentManager { public async FollowLink(link: Opt<Doc>, doc: Doc, createViewFunc: CreateViewFunc, zoom = false, currentContext?: Doc, finished?: () => void, traverseBacklink?: boolean) { const linkDocs = link ? [link] : DocListCast(doc.links); SelectionManager.DeselectAll(); - const firstDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor1 as Doc, doc)); // link docs where 'doc' is anchor1 - const secondDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor2 as Doc, doc)); // link docs where 'doc' is anchor2 + const firstDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor1 as Doc, doc) || Doc.AreProtosEqual((linkDoc.anchor1 as Doc).annotationOn as Doc, doc)); // link docs where 'doc' is anchor1 + const secondDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor2 as Doc, doc) || Doc.AreProtosEqual((linkDoc.anchor2 as Doc).annotationOn as Doc, doc)); // link docs where 'doc' is anchor2 const fwdLinkWithoutTargetView = firstDocs.find(d => DocumentManager.Instance.getDocumentViews(d.anchor2 as Doc).length === 0); const backLinkWithoutTargetView = secondDocs.find(d => DocumentManager.Instance.getDocumentViews(d.anchor1 as Doc).length === 0); const linkWithoutTargetDoc = traverseBacklink === undefined ? fwdLinkWithoutTargetView || backLinkWithoutTargetView : traverseBacklink ? backLinkWithoutTargetView : fwdLinkWithoutTargetView; @@ -222,17 +225,16 @@ export class DocumentManager { followLinks.forEach(async linkDoc => { if (linkDoc) { const target = (doc === linkDoc.anchor1 ? linkDoc.anchor2 : doc === linkDoc.anchor2 ? linkDoc.anchor1 : - (Doc.AreProtosEqual(doc, linkDoc.anchor1 as Doc) ? linkDoc.anchor2 : linkDoc.anchor1)) as Doc; + (Doc.AreProtosEqual(doc, linkDoc.anchor1 as Doc) || Doc.AreProtosEqual((linkDoc.anchor1 as Doc).annotationOn as Doc, doc) ? linkDoc.anchor2 : linkDoc.anchor1)) as Doc; const targetTimecode = (doc === linkDoc.anchor1 ? Cast(linkDoc.anchor2_timecode, "number") : doc === linkDoc.anchor2 ? Cast(linkDoc.anchor1_timecode, "number") : - (Doc.AreProtosEqual(doc, linkDoc.anchor1 as Doc) ? Cast(linkDoc.anchor2_timecode, "number") : Cast(linkDoc.anchor1_timecode, "number"))); + (Doc.AreProtosEqual(doc, linkDoc.anchor1 as Doc) || Doc.AreProtosEqual((linkDoc.anchor1 as Doc).annotationOn as Doc, doc) ? Cast(linkDoc.anchor2_timecode, "number") : Cast(linkDoc.anchor1_timecode, "number"))); if (target) { const containerDoc = (await Cast(target.annotationOn, Doc)) || target; - containerDoc.currentTimecode = targetTimecode; + containerDoc._currentTimecode = targetTimecode; const targetContext = await target?.context as Doc; const targetNavContext = !Doc.AreProtosEqual(targetContext, currentContext) ? targetContext : undefined; - console.log(targetNavContext); - DocumentManager.Instance.jumpToDocument(target, zoom, (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, "onRight"), finished), targetNavContext, linkDoc, undefined, doc, finished); + DocumentManager.Instance.jumpToDocument(target, zoom, (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, "add:right"), finished), targetNavContext, linkDoc, undefined, doc, finished); } else { finished?.(); } diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 0cca61841..3a0f306f3 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -13,7 +13,7 @@ import * as globalCssVariables from "../views/globalCssVariables.scss"; import { UndoManager } from "./UndoManager"; import { SnappingManager } from "./SnappingManager"; -export type dropActionType = "alias" | "copy" | "move" | "same" | undefined; // undefined = move +export type dropActionType = "alias" | "copy" | "move" | "same" | "none" | undefined; // undefined = move export function SetupDrag( _reference: React.RefObject<HTMLElement>, docFunc: () => Doc | Promise<Doc> | undefined, @@ -117,10 +117,11 @@ export namespace DragManager { } export class DocumentDragData { - constructor(dragDoc: Doc[]) { + constructor(dragDoc: Doc[], dropAction?: dropActionType) { this.draggedDocuments = dragDoc; this.droppedDocuments = []; this.offset = [0, 0]; + this.dropAction = dropAction; } draggedDocuments: Doc[]; droppedDocuments: Doc[]; @@ -128,9 +129,11 @@ export namespace DragManager { treeViewDoc?: Doc; dontHideOnDrop?: boolean; offset: number[]; - dropAction: dropActionType; + canEmbed?: boolean; + userDropAction: dropActionType; // the user requested drop action -- this will be honored as specified by modifier keys + defaultDropAction?: dropActionType; // an optionally specified default drop action when there is no user drop actionl - this will be honored if there is no user drop action + dropAction: dropActionType; // a drop action request by the initiating code. the actual drop action may be different -- eg, if the request is 'alias', but the document is dropped within the same collection, the drop action will be switched to 'move' removeDropProperties?: string[]; - userDropAction: dropActionType; moveDocument?: MoveFunction; removeDocument?: RemoveFunction; isSelectionMove?: boolean; // indicates that an explicitly selected Document is being dragged. this will suppress onDragStart scripts @@ -143,7 +146,7 @@ export namespace DragManager { linkSourceDocument: Doc; dontClearTextBox?: boolean; linkDocument?: Doc; - linkDropCallback?: (data: LinkDragData) => void; + linkDropCallback?: (data: { linkDocument?: Doc }) => void; } export class ColumnDragData { constructor(colKey: SchemaHeaderField) { @@ -160,7 +163,7 @@ export namespace DragManager { this.annotationDocument = annotationDoc; this.offset = [0, 0]; } - linkedToDoc?: boolean; + linkDocument?: Doc; targetContext: Doc | undefined; dragDocument: Doc; annotationDocument: Doc; @@ -168,6 +171,7 @@ export namespace DragManager { offset: number[]; dropAction: dropActionType; userDropAction: dropActionType; + linkDropCallback?: (data: { linkDocument?: Doc }) => void; } export function MakeDropTarget( @@ -221,6 +225,7 @@ export namespace DragManager { }; dragData.draggedDocuments.map(d => d.dragFactory); // does this help? trying to make sure the dragFactory Doc is loaded StartDrag(eles, dragData, downX, downY, options, finishDrag); + return true; } // drag a button template and drop a new button @@ -317,9 +322,10 @@ export namespace DragManager { export let docsBeingDragged: Doc[] = []; export let CanEmbed = false; export function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: DragCompleteEvent) => void) { + if (dragData.dropAction === "none") return; const batch = UndoManager.StartBatch("dragging"); eles = eles.filter(e => e); - CanEmbed = false; + CanEmbed = dragData.canEmbed || false; if (!dragDiv) { dragDiv = document.createElement("div"); dragDiv.className = "dragManager-dragDiv"; @@ -327,14 +333,13 @@ export namespace DragManager { dragLabel = document.createElement("div"); dragLabel.className = "dragManager-dragLabel"; dragLabel.style.zIndex = "100001"; - dragLabel.style.fontSize = "10pt"; + dragLabel.style.fontSize = "10px"; dragLabel.style.position = "absolute"; // dragLabel.innerText = "press 'a' to embed on drop"; // bcz: need to move this to a status bar dragDiv.appendChild(dragLabel); DragManager.Root().appendChild(dragDiv); } dragLabel.style.display = ""; - SnappingManager.SetIsDragging(true); const scaleXs: number[] = []; const scaleYs: number[] = []; const xs: number[] = []; @@ -405,6 +410,7 @@ export namespace DragManager { const hideSource = options?.hideSource ? true : false; eles.map(ele => ele.parentElement && ele.parentElement?.className === dragData.dragDivName ? (ele.parentElement.hidden = hideSource) : (ele.hidden = hideSource)); + SnappingManager.SetIsDragging(true); let lastX = downX; let lastY = downY; const xFromLeft = downX - elesCont.left; @@ -415,7 +421,7 @@ export namespace DragManager { const moveHandler = (e: PointerEvent) => { e.preventDefault(); // required or dragging text menu link item ends up dragging the link button as native drag/drop if (dragData instanceof DocumentDragData) { - dragData.userDropAction = e.ctrlKey && e.altKey ? "copy" : e.ctrlKey ? "alias" : undefined; + dragData.userDropAction = e.ctrlKey && e.altKey ? "copy" : e.ctrlKey ? "alias" : dragData.defaultDropAction; } if (e?.shiftKey && dragData.draggedDocuments.length === 1) { dragData.dropAction = dragData.userDropAction || "same"; @@ -434,7 +440,7 @@ export namespace DragManager { const target = document.elementFromPoint(e.x, e.y); - if (target && !options?.noAutoscroll && !dragData.draggedDocuments?.some((d: any) => d._noAutoscroll)) { + if (target && !Doc.UserDoc()._noAutoscroll && !options?.noAutoscroll && !dragData.draggedDocuments?.some((d: any) => d._noAutoscroll)) { const autoScrollHandler = () => { target.dispatchEvent( new CustomEvent<React.DragEvent>("dashDragAutoScroll", { diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts index f1848f7e5..32817eefd 100644 --- a/src/client/util/DropConverter.ts +++ b/src/client/util/DropConverter.ts @@ -2,12 +2,13 @@ import { DragManager } from "./DragManager"; import { Doc, DocListCast, Opt } from "../../fields/Doc"; import { DocumentType } from "../documents/DocumentTypes"; import { ObjectField } from "../../fields/ObjectField"; -import { StrCast } from "../../fields/Types"; +import { StrCast, Cast } from "../../fields/Types"; import { Docs } from "../documents/Documents"; import { ScriptField, ComputedField } from "../../fields/ScriptField"; import { RichTextField } from "../../fields/RichTextField"; import { ImageField } from "../../fields/URLField"; import { Scripting } from "./Scripting"; +import { listSpec } from "../../fields/Schema"; // // converts 'doc' into a template that can be used to render other documents. @@ -37,11 +38,7 @@ export function makeTemplate(doc: Doc, first: boolean = true, rename: Opt<string } }); if (first) { - if (docs.length) { // bcz: feels hacky : if the root level document has items, it's not a field template, but we still want its caption to be a textTemplate - if (doc.caption instanceof RichTextField && !doc.caption.Empty()) { - doc["caption-textTemplate"] = ComputedField.MakeFunction(`copyField(this.caption)`); - } - } else { + if (!docs.length) { // bcz: feels hacky : if the root level document has items, it's not a field template any = Doc.MakeMetadataFieldTemplate(doc, Doc.GetProto(layoutDoc)) || any; } } @@ -58,9 +55,15 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) { let dbox = doc; // bcz: isButtonBar is intended to allow a collection of linear buttons to be dropped and nested into another collection of buttons... it's not being used yet, and isn't very elegant if (doc.type === DocumentType.FONTICON || StrCast(Doc.Layout(doc).layout).includes("FontIconBox")) { - //dbox = Doc.MakeAlias(doc); // don't need to do anything if dropping an icon doc onto an icon bar since there should be no layout data for an icon + if (data.removeDropProperties || dbox.removeDropProperties) { + //dbox = Doc.MakeAlias(doc); // don't need to do anything if dropping an icon doc onto an icon bar since there should be no layout data for an icon + dbox = Doc.MakeAlias(dbox); + const dragProps = Cast(dbox.removeDropProperties, listSpec("string"), []); + const remProps = (data.removeDropProperties || []).concat(Array.from(dragProps)); + remProps.map(prop => dbox[prop] = undefined); + } } else if (!doc.onDragStart && !doc.isButtonBar) { - const layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc; + const layoutDoc = doc;// doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc; if (layoutDoc.type !== DocumentType.FONTICON) { !layoutDoc.isTemplateDoc && makeTemplate(layoutDoc); } diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx index d03989675..cb512bca8 100644 --- a/src/client/util/GroupManager.tsx +++ b/src/client/util/GroupManager.tsx @@ -1,24 +1,19 @@ -import * as React from "react"; -import { observable, action, runInAction, computed } from "mobx"; -import { SelectionManager } from "./SelectionManager"; -import MainViewModal from "../views/MainViewModal"; -import { observer } from "mobx-react"; -import { Doc, DocListCast, Opt, DocListCastAsync } from "../../fields/Doc"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import * as fa from '@fortawesome/free-solid-svg-icons'; -import { library } from "@fortawesome/fontawesome-svg-core"; -import SharingManager, { User } from "./SharingManager"; -import { Utils } from "../../Utils"; -import * as RequestPromise from "request-promise"; +import { action, computed, observable, runInAction } from "mobx"; +import { observer } from "mobx-react"; +import * as React from "react"; import Select from 'react-select'; -import "./GroupManager.scss"; -import { StrCast, Cast } from "../../fields/Types"; -import GroupMemberView from "./GroupMemberView"; +import * as RequestPromise from "request-promise"; +import { Doc, DocListCast, DocListCastAsync, Opt } from "../../fields/Doc"; +import { Cast, StrCast } from "../../fields/Types"; import { setGroups } from "../../fields/util"; +import { Utils } from "../../Utils"; import { DocServer } from "../DocServer"; +import { MainViewModal } from "../views/MainViewModal"; import { TaskCompletionBox } from "../views/nodes/TaskCompletedBox"; - -library.add(fa.faPlus, fa.faTimes, fa.faInfoCircle, fa.faCaretUp, fa.faCaretRight, fa.faCaretDown); +import "./GroupManager.scss"; +import { GroupMemberView } from "./GroupMemberView"; +import { SharingManager, User } from "./SharingManager"; /** * Interface for options for the react-select component @@ -29,7 +24,7 @@ export interface UserOptions { } @observer -export default class GroupManager extends React.Component<{}> { +export class GroupManager extends React.Component<{}> { static Instance: GroupManager; @observable isOpen: boolean = false; // whether the GroupManager is to be displayed or not. @@ -71,7 +66,7 @@ export default class GroupManager extends React.Component<{}> { const evaluating = raw.map(async user => { const userDocument = await DocServer.GetRefField(user.userDocumentId); if (userDocument instanceof Doc) { - const notificationDoc = await Cast(userDocument["sidebar-sharing"], Doc); + const notificationDoc = await Cast(userDocument.mySharedDocs, Doc); runInAction(() => { if (notificationDoc instanceof Doc) { this.users.push(user.email); @@ -92,6 +87,7 @@ export default class GroupManager extends React.Component<{}> { const members: string[] = JSON.parse(StrCast(group.members)); if (members.includes(Doc.CurrentUserEmail)) this.currentUserGroups.push(StrCast(group.groupName)); }); + this.currentUserGroups.push("Public"); setGroups(this.currentUserGroups); }); } @@ -121,6 +117,7 @@ export default class GroupManager extends React.Component<{}> { close = () => { this.isOpen = false; this.currentGroup = undefined; + this.selectedUsers = null; // this.users = []; this.createGroupModalOpen = false; TaskCompletionBox.taskCompleted = false; @@ -162,7 +159,7 @@ export default class GroupManager extends React.Component<{}> { * @returns the members of the admin group. */ get adminGroupMembers(): string[] { - return this.getGroup("admin") ? JSON.parse(StrCast(this.getGroup("admin")!.members)) : ""; + return this.getGroup("Admin") ? JSON.parse(StrCast(this.getGroup("Admin")!.members)) : ""; } /** @@ -182,7 +179,7 @@ export default class GroupManager extends React.Component<{}> { */ createGroupDoc(groupName: string, memberEmails: string[] = []) { const groupDoc = new Doc; - groupDoc.groupName = groupName; + groupDoc.groupName = groupName.toLowerCase() === "admin" ? "Admin" : groupName; groupDoc.owners = JSON.stringify([Doc.CurrentUserEmail]); groupDoc.members = JSON.stringify(memberEmails); if (memberEmails.includes(Doc.CurrentUserEmail)) { @@ -281,17 +278,24 @@ export default class GroupManager extends React.Component<{}> { */ @action createGroup = () => { - if (!this.inputRef.current?.value) { + const { value } = this.inputRef.current!; + if (!value) { alert("Please enter a group name"); return; } - if (this.getAllGroups().find(group => group.groupName === this.inputRef.current!.value)) { // why do I need a null check here? + if (["admin", "public", "override"].includes(value.toLowerCase())) { + if (value.toLowerCase() !== "admin" || (value.toLowerCase() === "admin" && this.getGroup("Admin"))) { + alert(`You cannot override the ${value.charAt(0).toUpperCase() + value.slice(1)} group`); + return; + } + } + if (this.getGroup(value)) { alert("Please select a unique group name"); return; } - this.createGroupDoc(this.inputRef.current.value, this.selectedUsers?.map(user => user.value)); + this.createGroupDoc(value, this.selectedUsers?.map(user => user.value)); this.selectedUsers = null; - this.inputRef.current.value = ""; + this.inputRef.current!.value = ""; this.buttonColour = "#979797"; const { left, width, top } = this.createGroupButtonRef.current!.getBoundingClientRect(); @@ -314,7 +318,7 @@ export default class GroupManager extends React.Component<{}> { <div className={"close-button"} onClick={action(() => { this.createGroupModalOpen = false; TaskCompletionBox.taskCompleted = false; })}> - <FontAwesomeIcon icon={fa.faTimes} color={"black"} size={"lg"} /> + <FontAwesomeIcon icon={"times"} color={"black"} size={"lg"} /> </div> </div> <input @@ -398,19 +402,19 @@ export default class GroupManager extends React.Component<{}> { <div className="group-heading"> <p><b>Manage Groups</b></p> <button onClick={action(() => this.createGroupModalOpen = true)}> - <FontAwesomeIcon icon={fa.faPlus} size={"sm"} /> Create Group + <FontAwesomeIcon icon={"plus-hexagon"} size={"sm"} /> Create Group </button> <div className={"close-button"} onClick={this.close}> - <FontAwesomeIcon icon={fa.faTimes} color={"black"} size={"lg"} /> + <FontAwesomeIcon icon={"times"} color={"black"} size={"lg"} /> </div> </div> <div className="main-container"> <div className="sort-groups" onClick={action(() => this.groupSort = this.groupSort === "ascending" ? "descending" : this.groupSort === "descending" ? "none" : "ascending")}> - Name {this.groupSort === "ascending" ? <FontAwesomeIcon icon={fa.faCaretUp} size={"xs"} /> - : this.groupSort === "descending" ? <FontAwesomeIcon icon={fa.faCaretDown} size={"xs"} /> - : <FontAwesomeIcon icon={fa.faCaretRight} size={"xs"} /> + Name {this.groupSort === "ascending" ? <FontAwesomeIcon icon={"caret-up"} size={"xs"} /> + : this.groupSort === "descending" ? <FontAwesomeIcon icon={"caret-down"} size={"xs"} /> + : <FontAwesomeIcon icon={"caret-right"} size={"xs"} /> } </div> <div className="group-body"> @@ -421,7 +425,7 @@ export default class GroupManager extends React.Component<{}> { > <div className="group-name" >{group.groupName}</div> <div className="group-info" onClick={action(() => this.currentGroup = group)}> - <FontAwesomeIcon icon={fa.faInfoCircle} color={"#e8e8e8"} size={"sm"} style={{ backgroundColor: "#1e89d7", borderRadius: "100%", border: "1px solid #1e89d7" }} /> + <FontAwesomeIcon icon={"info-circle"} color={"#e8e8e8"} size={"sm"} style={{ backgroundColor: "#1e89d7", borderRadius: "100%", border: "1px solid #1e89d7" }} /> </div> </div> )} diff --git a/src/client/util/GroupMemberView.tsx b/src/client/util/GroupMemberView.tsx index 531ef988a..4ead01e9f 100644 --- a/src/client/util/GroupMemberView.tsx +++ b/src/client/util/GroupMemberView.tsx @@ -1,25 +1,21 @@ -import * as React from "react"; -import MainViewModal from "../views/MainViewModal"; -import { observer } from "mobx-react"; -import GroupManager, { UserOptions } from "./GroupManager"; -import { library } from "@fortawesome/fontawesome-svg-core"; -import { StrCast } from "../../fields/Types"; -import { action, observable } from "mobx"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import * as fa from '@fortawesome/free-solid-svg-icons'; +import { action, observable } from "mobx"; +import { observer } from "mobx-react"; +import * as React from "react"; import Select from "react-select"; import { Doc } from "../../fields/Doc"; +import { StrCast } from "../../fields/Types"; +import { MainViewModal } from "../views/MainViewModal"; +import { GroupManager, UserOptions } from "./GroupManager"; import "./GroupMemberView.scss"; -library.add(fa.faTimes, fa.faTrashAlt); - interface GroupMemberViewProps { group: Doc; onCloseButtonClick: () => void; } @observer -export default class GroupMemberView extends React.Component<GroupMemberViewProps> { +export class GroupMemberView extends React.Component<GroupMemberViewProps> { @observable private memberSort: "ascending" | "descending" | "none" = "none"; @@ -43,7 +39,7 @@ export default class GroupMemberView extends React.Component<GroupMemberViewProp > </input> <div className={"memberView-closeButton"} onClick={action(this.props.onCloseButtonClick)}> - <FontAwesomeIcon icon={fa.faTimes} color={"black"} size={"lg"} /> + <FontAwesomeIcon icon={"times"} color={"black"} size={"lg"} /> </div> {GroupManager.Instance.hasEditAccess(this.props.group) ? <div className="group-buttons"> @@ -88,7 +84,7 @@ export default class GroupMemberView extends React.Component<GroupMemberViewProp </div> {hasEditAccess ? <div className={"remove-button"} onClick={() => GroupManager.Instance.removeMemberFromGroup(this.props.group, member)}> - <FontAwesomeIcon icon={fa.faTrashAlt} size={"sm"} /> + <FontAwesomeIcon icon={"trash-alt"} size={"sm"} /> </div> : null} </div> diff --git a/src/client/util/History.ts b/src/client/util/History.ts index 7b7d4b835..cbe36b401 100644 --- a/src/client/util/History.ts +++ b/src/client/util/History.ts @@ -1,8 +1,8 @@ import { Doc } from "../../fields/Doc"; import { DocServer } from "../DocServer"; -import { MainView } from "../views/MainView"; import * as qs from 'query-string'; import { Utils, OmitKeys } from "../../Utils"; +import { CurrentUserUtils } from "./CurrentUserUtils"; export namespace HistoryUtil { export interface DocInitializerList { @@ -197,7 +197,7 @@ export namespace HistoryUtil { await Promise.all(Object.keys(init).map(id => initDoc(id, init[id]))); } if (field instanceof Doc) { - MainView.Instance.openWorkspace(field, true); + CurrentUserUtils.openDashboard(Doc.UserDoc(), field, true); } } diff --git a/src/client/util/HypothesisUtils.ts b/src/client/util/HypothesisUtils.ts index 04e937878..f4cf336e2 100644 --- a/src/client/util/HypothesisUtils.ts +++ b/src/client/util/HypothesisUtils.ts @@ -21,7 +21,7 @@ export namespace Hypothesis { export const getSourceWebDoc = async (uri: string) => { const result = await findWebDoc(uri); console.log(result ? "existing doc found" : "existing doc NOT found"); - return result || Docs.Create.WebDocument(uri, { title: uri, _nativeWidth: 850, _nativeHeight: 962, _width: 400, UseCors: true }); // create and return a new Web doc with given uri if no matching docs are found + return result || Docs.Create.WebDocument(uri, { title: uri, _fitWidth: true, _nativeWidth: 850, _height: 512, _width: 400, useCors: true }); // create and return a new Web doc with given uri if no matching docs are found }; @@ -60,6 +60,7 @@ export namespace Hypothesis { DocumentLinksButton.AnnotationId = annotationId; DocumentLinksButton.AnnotationUri = annotationUri; DocumentLinksButton.StartLink = sourceDoc; + DocumentLinksButton.StartLinkView = undefined; }); } else { // if a link has already been started, complete the link to sourceDoc runInAction(() => { diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx index 77f13e9f4..7f01966b9 100644 --- a/src/client/util/Import & Export/DirectoryImportBox.tsx +++ b/src/client/util/Import & Export/DirectoryImportBox.tsx @@ -1,5 +1,3 @@ -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faCloudUploadAlt, faPlus, faTag } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { BatchedArray } from "array-batcher"; import "fs"; @@ -13,7 +11,7 @@ import { List } from "../../../fields/List"; import { listSpec } from "../../../fields/Schema"; import { SchemaHeaderField } from "../../../fields/SchemaHeaderField"; import { BoolCast, Cast, NumCast } from "../../../fields/Types"; -import { AcceptibleMedia, Upload } from "../../../server/SharedMediaTypes"; +import { AcceptableMedia, Upload } from "../../../server/SharedMediaTypes"; import { Utils } from "../../../Utils"; import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils"; import { Docs, DocumentOptions, DocUtils } from "../../documents/Documents"; @@ -47,7 +45,6 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> { constructor(props: FieldViewProps) { super(props); - library.add(faTag, faPlus); const doc = this.props.Document; this.editingMetadata = this.editingMetadata || false; this.persistent = this.persistent || false; @@ -90,7 +87,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> { const file = files.item(i); if (file && !unsupported.includes(file.type)) { const ext = path.extname(file.name).toLowerCase(); - if (AcceptibleMedia.imageFormats.includes(ext)) { + if (AcceptableMedia.imageFormats.includes(ext)) { validated.push(file); } } @@ -301,7 +298,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> { opacity: uploading ? 0 : 1, transition: "0.4s opacity ease" }}> - <FontAwesomeIcon icon={faCloudUploadAlt} color="#FFFFFF" size={"2x"} /> + <FontAwesomeIcon icon={"cloud-upload-alt"} color="#FFFFFF" size={"2x"} /> </div> <img style={{ @@ -366,7 +363,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> { opacity: uploading ? 0 : 1, transition: "0.4s opacity ease" }} - icon={isEditing ? faCloudUploadAlt : faTag} + icon={isEditing ? "cloud-upload-alt" : "tag"} color="#FFFFFF" size={"1x"} /> @@ -399,7 +396,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> { marginLeft: 6.4, marginTop: 5.2 }} - icon={faPlus} + icon={"plus"} size={"1x"} /> </div> diff --git a/src/client/util/Import & Export/ImportMetadataEntry.tsx b/src/client/util/Import & Export/ImportMetadataEntry.tsx index dcb94e2e0..45d8c0c63 100644 --- a/src/client/util/Import & Export/ImportMetadataEntry.tsx +++ b/src/client/util/Import & Export/ImportMetadataEntry.tsx @@ -3,8 +3,6 @@ import { observer } from "mobx-react"; import { EditableView } from "../../views/EditableView"; import { action, computed } from "mobx"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faPlus } from "@fortawesome/free-solid-svg-icons"; -import { library } from '@fortawesome/fontawesome-svg-core'; import { Doc } from "../../../fields/Doc"; import { StrCast, BoolCast } from "../../../fields/Types"; @@ -24,11 +22,6 @@ export default class ImportMetadataEntry extends React.Component<KeyValueProps> private valueRef = React.createRef<EditableView>(); private checkRef = React.createRef<HTMLInputElement>(); - constructor(props: KeyValueProps) { - super(props); - library.add(faPlus); - } - @computed public get valid() { return (this.key.length > 0 && this.key !== keyPlaceholder) && (this.value.length > 0 && this.value !== valuePlaceholder); @@ -132,7 +125,7 @@ export default class ImportMetadataEntry extends React.Component<KeyValueProps> </div> <div onClick={() => this.props.remove(this)} title={"Delete Entry"}> <FontAwesomeIcon - icon={faPlus} + icon={"plus"} color={"red"} size={"1x"} style={{ diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx index ae3b3e064..f58277717 100644 --- a/src/client/util/InteractionUtils.tsx +++ b/src/client/util/InteractionUtils.tsx @@ -132,7 +132,6 @@ export namespace InteractionUtils { if (isNaN(scaley)) { scaley = 1; } - console.log(pts.length); return pts; } @@ -140,7 +139,7 @@ export namespace InteractionUtils { export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number, color: string, width: number, strokeWidth: number, bezier: string, fill: string, arrowStart: string, arrowEnd: string, - dash: string, scalex: number, scaley: number, shape: string, pevents: string, drawHalo: boolean, nodefs: boolean) { + dash: string | undefined, scalex: number, scaley: number, shape: string, pevents: string, drawHalo: boolean, nodefs: boolean) { let pts: { X: number; Y: number; }[] = []; if (shape) { //if any of the shape are true pts = makePolygon(shape, points); @@ -182,13 +181,13 @@ export namespace InteractionUtils { const strpts = pts.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${(pt.X - left - width / 2) * scalex + width / 2}, ${(pt.Y - top - width / 2) * scaley + width / 2} `, ""); - const dashArray = String(Number(width) * Number(dash)); + const dashArray = dash && Number(dash) ? String(Number(width) * Number(dash)) : undefined; const defGuid = Utils.GenerateGuid(); const arrowDim = Math.max(0.5, 8 / Math.log(Math.max(2, strokeWidth))); const addables = pts.map((pts, i) => <svg height="10" width="10"> - <circle cx={(pts.X - left - width / 2) * scalex + width / 2} cy={(pts.Y - top - width / 2) * scaley + width / 2} r={strokeWidth / 2} stroke="black" stroke-width={1} fill="blue" + <circle cx={(pts.X - left - width / 2) * scalex + width / 2} cy={(pts.Y - top - width / 2) * scaley + width / 2} r={strokeWidth / 2} stroke="black" strokeWidth={1} fill="blue" onDoubleClick={(e) => { console.log(i); }} pointerEvents="all" cursor="all-scroll" /> </svg>); @@ -210,7 +209,7 @@ export namespace InteractionUtils { points={strpts} style={{ filter: drawHalo ? "url(#inkSelectionHalo)" : undefined, - fill: fill ? fill : "transparent", + fill: fill ? fill : "none", opacity: strokeWidth !== width ? 0.5 : undefined, pointerEvents: pevents as any, stroke: color ?? "rgb(0, 0, 0)", @@ -230,7 +229,7 @@ export namespace InteractionUtils { export function makePolygon(shape: string, points: { X: number, Y: number }[]) { if (points.length > 1 && points[points.length - 1].X === points[0].X && points[points.length - 1].Y + 1 === points[0].Y) { //pointer is up (first and last points are the same) - if (shape === "arrow" || shape === "line") { + if (shape === "arrow" || shape === "line" || shape === "circle") { //if arrow or line, the two end points should be the starting and the ending point var left = points[0].X; var top = points[0].Y; @@ -252,7 +251,7 @@ export namespace InteractionUtils { left = points[0].X; bottom = points[points.length - 1].Y; top = points[0].Y; - if (shape !== "arrow" && shape !== "line") { + if (shape !== "arrow" && shape !== "line" && shape !== "circle") { //switch left/right and top/bottom if needed if (left > right) { const temp = right; @@ -300,19 +299,36 @@ export namespace InteractionUtils { return points; case "circle": - const centerX = (right + left) / 2; - const centerY = (bottom + top) / 2; - const radius = bottom - centerY; - for (var y = top; y < bottom; y++) { - const x = Math.sqrt(Math.pow(radius, 2) - (Math.pow((y - centerY), 2))) + centerX; - points.push({ X: x, Y: y }); - } - for (var y = bottom; y > top; y--) { - const x = Math.sqrt(Math.pow(radius, 2) - (Math.pow((y - centerY), 2))) + centerX; - const newX = centerX - (x - centerX); - points.push({ X: newX, Y: y }); + + + const centerX = (Math.max(left, right) + Math.min(left, right)) / 2; + const centerY = (Math.max(top, bottom) + Math.min(top, bottom)) / 2; + const radius = Math.max(centerX - Math.min(left, right), centerY - Math.min(top, bottom)); + if (centerX - Math.min(left, right) < centerY - Math.min(top, bottom)) { + for (var y = Math.min(top, bottom); y < Math.max(top, bottom); y++) { + const x = Math.sqrt(Math.pow(radius, 2) - (Math.pow((y - centerY), 2))) + centerX; + points.push({ X: x, Y: y }); + } + for (var y = Math.max(top, bottom); y > Math.min(top, bottom); y--) { + const x = Math.sqrt(Math.pow(radius, 2) - (Math.pow((y - centerY), 2))) + centerX; + const newX = centerX - (x - centerX); + points.push({ X: newX, Y: y }); + } + points.push({ X: Math.sqrt(Math.pow(radius, 2) - (Math.pow((Math.min(top, bottom) - centerY), 2))) + centerX, Y: Math.min(top, bottom) }); + + } else { + for (var x = Math.min(left, right); x < Math.max(left, right); x++) { + const y = Math.sqrt(Math.pow(radius, 2) - (Math.pow((x - centerX), 2))) + centerY; + points.push({ X: x, Y: y }); + } + for (var x = Math.max(left, right); x > Math.min(left, right); x--) { + const y = Math.sqrt(Math.pow(radius, 2) - (Math.pow((x - centerX), 2))) + centerY; + const newY = centerY - (y - centerY); + points.push({ X: x, Y: newY }); + } + points.push({ X: Math.min(left, right), Y: Math.sqrt(Math.pow(radius, 2) - (Math.pow((Math.min(left, right) - centerX), 2))) + centerY }); + } - points.push({ X: Math.sqrt(Math.pow(radius, 2) - (Math.pow((top - centerY), 2))) + centerX, Y: top }); return points; // case "arrow": // const x1 = left; @@ -333,7 +349,6 @@ export namespace InteractionUtils { // points.push({ X: x2, Y: y2 }); // return points; case "line": - points.push({ X: left, Y: top }); points.push({ X: right, Y: bottom }); return points; diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index 223f0e7ef..269de08a1 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -2,6 +2,7 @@ import { Doc, DocListCast, Opt } from "../../fields/Doc"; import { List } from "../../fields/List"; import { listSpec } from "../../fields/Schema"; import { Cast, StrCast } from "../../fields/Types"; +import { CurrentUserUtils } from "./CurrentUserUtils"; /* * link doc: @@ -61,10 +62,12 @@ export class LinkManager { // finds all links that contain the given anchor public getAllDirectLinks(anchor: Doc): Doc[] { - const related = LinkManager.Instance.getAllLinks().filter(link => { - const protomatch1 = Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, null)); - const protomatch2 = Doc.AreProtosEqual(anchor, Cast(link.anchor2, Doc, null)); - return protomatch1 || protomatch2 || Doc.AreProtosEqual(link, anchor); + const related = LinkManager.Instance.getAllLinks().filter(link => link).filter(link => { + const a1 = Cast(link.anchor1, Doc, null); + const a2 = Cast(link.anchor2, Doc, null); + const protomatch1 = Doc.AreProtosEqual(anchor, a1); + const protomatch2 = Doc.AreProtosEqual(anchor, a2); + return ((a1?.author !== undefined && a2?.author !== undefined) || link.author === Doc.CurrentUserEmail) && (protomatch1 || protomatch2 || Doc.AreProtosEqual(link, anchor)); }); return related; } @@ -200,7 +203,7 @@ export class LinkManager { // finds the opposite anchor of a given anchor in a link //TODO This should probably return undefined if there isn't an opposite anchor //TODO This should also await the return value of the anchor so we don't filter out promises - public getOppositeAnchor(linkDoc: Doc, anchor: Doc): Doc | undefined { + public static getOppositeAnchor(linkDoc: Doc, anchor: Doc): Doc | undefined { const a1 = Cast(linkDoc.anchor1, Doc, null); const a2 = Cast(linkDoc.anchor2, Doc, null); if (Doc.AreProtosEqual(anchor, a1)) return a2; diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts index ce96ab67b..08ad49dcc 100644 --- a/src/client/util/SearchUtil.ts +++ b/src/client/util/SearchUtil.ts @@ -23,7 +23,7 @@ export namespace SearchUtil { } export interface SearchParams { - hl?: boolean; + hl?: string; "hl.fl"?: string; start?: number; rows?: number; @@ -39,10 +39,12 @@ export namespace SearchUtil { export async function Search(query: string, returnDocs: boolean, options: SearchParams = {}) { query = query || "*"; //If we just have a filter query, search for * as the query const rpquery = Utils.prepend("/dashsearch"); - let replacedQuery = query.replace(/type_t:([^ )])/g, (substring, arg) => `{!join from=id to=proto_i}type_t:${arg}`); + let replacedQuery = query.replace(/type_t:([^ )])/g, (substring, arg) => `{!join from=id to=proto_i}*:* AND ${arg}`); if (options.onlyAliases) { - replacedQuery = `{!join from=id to=proto_i}DEFAULT:${replacedQuery}`; + const header = query.match(/_[atnb]?:/) ? replacedQuery : "DEFAULT:" + replacedQuery; + replacedQuery = `{!join from=id to=proto_i}* AND ${header}`; } + console.log("Q: " + replacedQuery + " fq: " + options.fq); const gotten = await rp.get(rpquery, { qs: { ...options, q: replacedQuery } }); const result: IdSearchResult = gotten.startsWith("<") ? { ids: [], docs: [], numFound: 0, lines: [] } : JSON.parse(gotten); if (!returnDocs) { @@ -58,12 +60,14 @@ export namespace SearchUtil { const fileids = txtresult ? txtresult.ids : []; const newIds: string[] = []; const newLines: string[][] = []; - await Promise.all(fileids.map(async (tr: string, i: number) => { - const docQuery = "fileUpload_t:" + tr.substr(0, 7); //If we just have a filter query, search for * as the query - const docResult = JSON.parse(await rp.get(Utils.prepend("/dashsearch"), { qs: { ...options, q: docQuery } })); - newIds.push(...docResult.ids); - newLines.push(...docResult.ids.map((dr: any) => txtresult.lines[i])); - })); + if (fileids) { + await Promise.all(fileids.map(async (tr: string, i: number) => { + const docQuery = "fileUpload_t:" + tr.substr(0, 7); //If we just have a filter query, search for * as the query + const docResult = JSON.parse(await rp.get(Utils.prepend("/dashsearch"), { qs: { ...options, q: docQuery } })); + newIds.push(...docResult.ids); + newLines.push(...docResult.ids.map((dr: any) => txtresult.lines[i])); + })); + } const theDocs: Doc[] = []; @@ -85,10 +89,12 @@ export namespace SearchUtil { if (testDoc instanceof Doc && testDoc.type !== DocumentType.KVP && (options.allowAliases || testDoc.proto === undefined || theDocs.findIndex(d => Doc.AreProtosEqual(d, testDoc)) === -1)) { theDocs.push(testDoc); theLines.push([]); + } else { + result.numFound--; } } - return { docs: theDocs, numFound: result.numFound, highlighting, lines: theLines }; + return { docs: theDocs, numFound: Math.max(0, result.numFound), highlighting, lines: theLines }; } export async function GetAliasesOfDocument(doc: Doc): Promise<Doc[]>; diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index 113278593..008ce281c 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -1,10 +1,10 @@ import { observable, action, runInAction, ObservableMap } from "mobx"; -import { Doc } from "../../fields/Doc"; +import { Doc, Opt } from "../../fields/Doc"; import { DocumentView } from "../views/nodes/DocumentView"; import { computedFn } from "mobx-utils"; import { List } from "../../fields/List"; -import { Scripting } from "./Scripting"; -import { DocumentManager } from "./DocumentManager"; +import { CollectionSchemaView } from "../views/collections/CollectionSchemaView"; +import { CollectionViewType } from "../views/collections/CollectionView"; export namespace SelectionManager { @@ -12,10 +12,16 @@ export namespace SelectionManager { @observable IsDragging: boolean = false; SelectedDocuments: ObservableMap<DocumentView, boolean> = new ObservableMap(); + @observable SelectedSchemaDocument: Doc | undefined; + @observable SelectedSchemaCollection: CollectionSchemaView | undefined; @action + SelectSchemaDoc(collectionView: Opt<CollectionSchemaView>, doc: Opt<Doc>) { + manager.SelectedSchemaDocument = doc; + manager.SelectedSchemaCollection = collectionView; + } + @action SelectDoc(docView: DocumentView, ctrlPressed: boolean): void { - // if doc is not in SelectedDocuments, add it if (!manager.SelectedDocuments.get(docView)) { if (!ctrlPressed) { @@ -26,10 +32,11 @@ export namespace SelectionManager { docView.props.whenActiveChanged(true); } else if (!ctrlPressed && Array.from(manager.SelectedDocuments.entries()).length > 1) { Array.from(manager.SelectedDocuments.keys()).map(dv => dv !== docView && dv.props.whenActiveChanged(false)); + manager.SelectedSchemaDocument = undefined; + manager.SelectedSchemaCollection = undefined; manager.SelectedDocuments.clear(); manager.SelectedDocuments.set(docView, true); } - Doc.UserDoc().activeSelection = new List(SelectionManager.SelectedDocuments().map(dv => dv.props.Document)); } @action DeselectDoc(docView: DocumentView): void { @@ -37,15 +44,14 @@ export namespace SelectionManager { if (manager.SelectedDocuments.get(docView)) { manager.SelectedDocuments.delete(docView); docView.props.whenActiveChanged(false); - Doc.UserDoc().activeSelection = new List(SelectionManager.SelectedDocuments().map(dv => dv.props.Document)); } } @action DeselectAll(): void { - + manager.SelectedSchemaCollection = undefined; + manager.SelectedSchemaDocument = undefined; Array.from(manager.SelectedDocuments.keys()).map(dv => dv.props.whenActiveChanged(false)); manager.SelectedDocuments.clear(); - Doc.UserDoc().activeSelection = new List<Doc>([]); } } @@ -57,12 +63,15 @@ export namespace SelectionManager { export function SelectDoc(docView: DocumentView, ctrlPressed: boolean): void { manager.SelectDoc(docView, ctrlPressed); } + export function SelectSchemaDoc(colSchema: Opt<CollectionSchemaView>, document: Opt<Doc>): void { + manager.SelectSchemaDoc(colSchema, document); + } // computed functions, such as used in IsSelected generate errors if they're called outside of a // reaction context. Specifying the context with 'outsideReaction' allows an efficiency feature // to avoid unnecessary mobx invalidations when running inside a reaction. - export function IsSelected(doc: DocumentView, outsideReaction?: boolean): boolean { - return outsideReaction ? + export function IsSelected(doc: DocumentView | undefined, outsideReaction?: boolean): boolean { + return !doc ? false : outsideReaction ? manager.SelectedDocuments.get(doc) ? true : false : // get() accesses a hashtable -- setting anything in the hashtable generates a mobx invalidation for every get() computedFn(function isSelected(doc: DocumentView) { // wraapping get() in a computedFn only generates mobx() invalidations when the return value of the function for the specific get parameters has changed return manager.SelectedDocuments.get(doc) ? true : false; @@ -82,6 +91,12 @@ export namespace SelectionManager { } export function SelectedDocuments(): Array<DocumentView> { - return Array.from(manager.SelectedDocuments.keys()); + return Array.from(manager.SelectedDocuments.keys()).filter(dv => dv.props.Document._viewType !== CollectionViewType.Docking); + } + export function SelectedSchemaDoc(): Doc | undefined { + return manager.SelectedSchemaDocument; + } + export function SelectedSchemaCollection(): CollectionSchemaView | undefined { + return manager.SelectedSchemaCollection; } }
\ No newline at end of file diff --git a/src/client/util/SettingsManager.scss b/src/client/util/SettingsManager.scss index ec513e5d5..badba35f4 100644 --- a/src/client/util/SettingsManager.scss +++ b/src/client/util/SettingsManager.scss @@ -4,7 +4,6 @@ //background-color: whitesmoke !important; color: grey; width: 450px; - height: 300px; button { background: #315a96; @@ -142,7 +141,7 @@ .colorFlyout { margin-top: 2px; - margin-right: 25px; + margin-right: 18px; &:hover { cursor: pointer; @@ -160,13 +159,15 @@ .preferences-content { display: flex; margin-top: 4px; + color: black; + font-size: 11; .preferences-color { display: flex; + margin-top: 2px; + width: 55; .preferences-color-text { - color: black; - font-size: 11; margin-top: 4; margin-right: 4; } @@ -174,10 +175,11 @@ .preferences-font { display: flex; + height: 23px; + margin-top: 2px; .preferences-font-text { color: black; - font-size: 11; margin-top: 4; margin-right: 4; } @@ -194,6 +196,16 @@ } } + .preferences-check { + color: black; + font-size: 9; + /* margin-top: 4; */ + margin-right: 4; + margin-bottom: -3; + margin-left: 5; + margin-top: -1px; + } + .size-select { width: 60px; color: black; diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index b4778d3eb..cd01fea5a 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -1,27 +1,25 @@ -import { observable, runInAction, action, computed } from "mobx"; -import * as React from "react"; -import MainViewModal from "../views/MainViewModal"; -import { observer } from "mobx-react"; -import * as fa from '@fortawesome/free-solid-svg-icons'; -import { SelectionManager } from "./SelectionManager"; -import "./SettingsManager.scss"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Networking } from "../Network"; -import { CurrentUserUtils } from "./CurrentUserUtils"; -import { Utils, addStyleSheet, addStyleSheetRule, removeStyleSheetRule } from "../../Utils"; +import { action, computed, observable, runInAction } from "mobx"; +import { observer } from "mobx-react"; +import * as React from "react"; +import { ColorState, SketchPicker } from "react-color"; import { Doc } from "../../fields/Doc"; -import GroupManager from "./GroupManager"; -import GoogleAuthenticationManager from "../apis/GoogleAuthenticationManager"; +import { BoolCast, StrCast, Cast } from "../../fields/Types"; +import { addStyleSheet, addStyleSheetRule, Utils } from "../../Utils"; +import { GoogleAuthenticationManager } from "../apis/GoogleAuthenticationManager"; import { DocServer } from "../DocServer"; -import { BoolCast, StrCast, NumCast } from "../../fields/Types"; +import { Networking } from "../Network"; +import { MainViewModal } from "../views/MainViewModal"; +import { CurrentUserUtils } from "./CurrentUserUtils"; +import { GroupManager } from "./GroupManager"; +import "./SettingsManager.scss"; import { undoBatch } from "./UndoManager"; -import { ColorState, SketchPicker } from "react-color"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @observer -export default class SettingsManager extends React.Component<{}> { +export class SettingsManager extends React.Component<{}> { public static Instance: SettingsManager; static _settingsStyle = addStyleSheet(); @observable private isOpen = false; @@ -32,7 +30,7 @@ export default class SettingsManager extends React.Component<{}> { @observable private new_password = ""; @observable private new_confirm = ""; - @computed get backgroundColor() { return Doc.UserDoc().defaultColor; } + @computed get backgroundColor() { return Doc.UserDoc().activeCollectionBackground; } constructor(props: {}) { super(props); @@ -54,9 +52,11 @@ export default class SettingsManager extends React.Component<{}> { } @undoBatch selectUserMode = action((e: React.ChangeEvent) => Doc.UserDoc().noviceMode = (e.currentTarget as any)?.value === "Novice"); + @undoBatch changeShowTitle = action((e: React.ChangeEvent) => Doc.UserDoc().showTitle = (e.currentTarget as any).value ? "title" : undefined); @undoBatch changeFontFamily = action((e: React.ChangeEvent) => Doc.UserDoc().fontFamily = (e.currentTarget as any).value); @undoBatch changeFontSize = action((e: React.ChangeEvent) => Doc.UserDoc().fontSize = (e.currentTarget as any).value); - @undoBatch switchColor = action((color: ColorState) => Doc.UserDoc().activeCollectionBackground = String(color.hex)); + @undoBatch switchActiveBackgroundColor = action((color: ColorState) => Doc.UserDoc().activeCollectionBackground = String(color.hex)); + @undoBatch switchUserColor = action((color: ColorState) => Doc.UserDoc().userColor = String(color.hex)); @undoBatch playgroundModeToggle = action(() => { this.playgroundMode = !this.playgroundMode; @@ -68,13 +68,20 @@ export default class SettingsManager extends React.Component<{}> { }); @computed get preferencesContent() { - const colorBox = <SketchPicker onChange={this.switchColor} color={StrCast(this.backgroundColor)} + const colorBox = (func: (color: ColorState) => void) => <SketchPicker onChange={func} color={StrCast(this.backgroundColor)} presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']} />; const colorFlyout = <div className="colorFlyout"> - <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={colorBox}> + <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={colorBox(this.switchActiveBackgroundColor)}> + <div className="colorFlyout-button" style={{ backgroundColor: StrCast(this.backgroundColor) }} onPointerDown={e => e.stopPropagation()} > + <FontAwesomeIcon icon="palette" size="sm" color={StrCast(this.backgroundColor)} /> + </div> + </Flyout> + </div>; + const userColorFlyout = <div className="colorFlyout"> + <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={colorBox(this.switchUserColor)}> <div className="colorFlyout-button" style={{ backgroundColor: StrCast(this.backgroundColor) }} onPointerDown={e => e.stopPropagation()} > <FontAwesomeIcon icon="palette" size="sm" color={StrCast(this.backgroundColor)} /> </div> @@ -82,21 +89,39 @@ export default class SettingsManager extends React.Component<{}> { </div>; const fontFamilies = ["Times New Roman", "Arial", "Georgia", "Comic Sans MS", "Tahoma", "Impact", "Crimson Text"]; - const fontSizes = ["7pt", "8pt", "9pt", "10pt", "12pt", "14pt", "16pt", "18pt", "20pt", "24pt", "32pt", "48pt", "72pt"]; + const fontSizes = ["7px", "8px", "9px", "10px", "12px", "14px", "16px", "18px", "20px", "24px", "32px", "48px", "72px"]; return <div className="preferences-content"> <div className="preferences-color"> - <div className="preferences-color-text">Background Color</div> + <div className="preferences-color-text">Back. Color</div> {colorFlyout} </div> + <div className="preferences-color"> + <div className="preferences-color-text">User Color</div> + {userColorFlyout} + </div> <div className="preferences-font"> - <div className="preferences-font-text">Default Font</div> + <div className="preferences-font-text">Font</div> <select className="font-select" onChange={this.changeFontFamily} value={StrCast(Doc.UserDoc().fontFamily, "Times New Roman")} > {fontFamilies.map(font => <option key={font} value={font} defaultValue={StrCast(Doc.UserDoc().fontFamily)}> {font} </option>)} </select> - <select className="size-select" onChange={this.changeFontSize} value={StrCast(Doc.UserDoc().fontSize, "7pt")}> + <select className="size-select" style={{ marginRight: "10px" }} onChange={this.changeFontSize} value={StrCast(Doc.UserDoc().fontSize, "7px")}> {fontSizes.map(size => <option key={size} value={size} defaultValue={StrCast(Doc.UserDoc().fontSize)}> {size} </option>)} </select> + <div> + <div className="preferences-check">Show header</div> + <input type="checkbox" onChange={e => Doc.UserDoc().showTitle = Doc.UserDoc().showTitle ? undefined : "creationDate"} checked={Doc.UserDoc().showTitle !== undefined} /> + </div> + <div> + <div className="preferences-check">Full Toolbar</div> + <input type="checkbox" onChange={e => Doc.UserDoc()["documentLinksButton-fullMenu"] = !Doc.UserDoc()["documentLinksButton-fullMenu"]} + checked={BoolCast(Doc.UserDoc()["documentLinksButton-fullMenu"])} /> + </div> + <div> + <div className="preferences-check">Raise on drag</div> + <input type="checkbox" onChange={e => Doc.UserDoc()._raiseWhenDragged = !Doc.UserDoc()._raiseWhenDragged} + checked={BoolCast(Doc.UserDoc()._raiseWhenDragged)} /> + </div> </div> </div>; } @@ -158,10 +183,10 @@ export default class SettingsManager extends React.Component<{}> { <div className="settings-title">Settings</div> <div className="settings-username">{Doc.CurrentUserEmail}</div> <button className="logout-button" onClick={() => window.location.assign(Utils.prepend("/logout"))} > - {CurrentUserUtils.GuestWorkspace ? "Exit" : "Log Out"} + {CurrentUserUtils.GuestDashboard ? "Exit" : "Log Out"} </button> <div className="close-button" onClick={this.close}> - <FontAwesomeIcon icon={fa.faTimes} color="black" size={"lg"} /> + <FontAwesomeIcon icon={"times"} color="black" size={"lg"} /> </div> </div> <div className="settings-content"> @@ -180,6 +205,6 @@ export default class SettingsManager extends React.Component<{}> { isDisplayed={this.isOpen} interactive={true} closeOnExternalClick={this.close} - dialogueBoxStyle={{ width: "600px", height: "340px" }} />; + dialogueBoxStyle={{ width: "600px", background: Cast(Doc.UserDoc().userColor, "string", null) }} />; } }
\ No newline at end of file diff --git a/src/client/util/SharingManager.scss b/src/client/util/SharingManager.scss index 42c300712..9dc57dd1e 100644 --- a/src/client/util/SharingManager.scss +++ b/src/client/util/SharingManager.scss @@ -40,7 +40,7 @@ .permissions-select { z-index: 1; - margin-left: -100; + margin-left: -115; border: none; outline: none; text-align: justify; // for Edge @@ -59,6 +59,7 @@ margin-top: -17px; margin-bottom: 10px; font-size: 10px; + min-height: 40px; input { height: 10px; @@ -71,21 +72,25 @@ } } - .layoutDoc-acls { + .acl-container { display: flex; - flex-direction: column; float: right; - margin-right: 12; - margin-top: -15; - align-items: center; + align-items: baseline; + margin-top: -12; - label { - font-weight: normal; - font-style: italic; - } + .layoutDoc-acls, + .myDocs-acls { + flex-direction: column; + margin-right: 12; - input { - cursor: pointer; + label { + font-weight: normal; + font-style: italic; + } + + input { + cursor: pointer; + } } } } @@ -121,7 +126,7 @@ overflow-y: scroll; overflow-x: hidden; text-align: left; - display: flex; + display: block; align-content: center; align-items: center; text-align: center; diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index 5a863c813..bcd7d4056 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -1,29 +1,27 @@ -import { observable, runInAction, action } from "mobx"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { action, observable, runInAction, computed } from "mobx"; +import { observer } from "mobx-react"; import * as React from "react"; -import MainViewModal from "../views/MainViewModal"; -import { Doc, Opt, AclAdmin, AclPrivate, DocListCast, DataSym } from "../../fields/Doc"; -import { DocServer } from "../DocServer"; -import { Cast, StrCast } from "../../fields/Types"; +import Select from "react-select"; import * as RequestPromise from "request-promise"; +import { AclAdmin, AclPrivate, DataSym, Doc, DocListCast, Opt, AclSym, AclAddonly, AclEdit, AclReadonly } from "../../fields/Doc"; +import { List } from "../../fields/List"; +import { Cast, StrCast } from "../../fields/Types"; +import { distributeAcls, GetEffectiveAcl, SharingPermissions, TraceMobx } from "../../fields/util"; import { Utils } from "../../Utils"; -import "./SharingManager.scss"; -import { observer } from "mobx-react"; -import * as fa from '@fortawesome/free-solid-svg-icons'; -import { DocumentView } from "../views/nodes/DocumentView"; -import { SelectionManager } from "./SelectionManager"; -import { DocumentManager } from "./DocumentManager"; +import { DocServer } from "../DocServer"; import { CollectionView } from "../views/collections/CollectionView"; import { DictationOverlay } from "../views/DictationOverlay"; -import GroupManager, { UserOptions } from "./GroupManager"; -import GroupMemberView from "./GroupMemberView"; -import Select from "react-select"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { List } from "../../fields/List"; -import { distributeAcls, SharingPermissions, GetEffectiveAcl } from "../../fields/util"; +import { MainViewModal } from "../views/MainViewModal"; +import { DocumentView } from "../views/nodes/DocumentView"; import { TaskCompletionBox } from "../views/nodes/TaskCompletedBox"; -import { library } from "@fortawesome/fontawesome-svg-core"; - -library.add(fa.faInfoCircle, fa.faCaretUp, fa.faCaretRight, fa.faCaretDown); +import { DocumentManager } from "./DocumentManager"; +import { GroupManager, UserOptions } from "./GroupManager"; +import { GroupMemberView } from "./GroupMemberView"; +import "./SharingManager.scss"; +import { SelectionManager } from "./SelectionManager"; +import { intersection } from "lodash"; +import { SearchBox } from "../views/search/SearchBox"; export interface User { email: string; @@ -54,14 +52,15 @@ const storage = "data"; interface ValidatedUser { user: User; notificationDoc: Doc; + userColor: string; } @observer -export default class SharingManager extends React.Component<{}> { +export class SharingManager extends React.Component<{}> { public static Instance: SharingManager; @observable private isOpen = false; // whether the SharingManager modal is open or not - @observable private users: ValidatedUser[] = []; // the list of users with notificationDocs + @observable public users: ValidatedUser[] = []; // the list of users with notificationDocs @observable private targetDoc: Doc | undefined; // the document being shared @observable private targetDocView: DocumentView | undefined; // the DocumentView of the document being shared // @observable private copied = false; @@ -72,33 +71,33 @@ export default class SharingManager extends React.Component<{}> { @observable private individualSort: "ascending" | "descending" | "none" = "none"; // sorting options for the list of individuals @observable private groupSort: "ascending" | "descending" | "none" = "none"; // sorting options for the list of groups private shareDocumentButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); // ref for the share button, used for the position of the popup + private distributeAclsButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); // ref for the distribute button, used for the position of the popup // if both showUserOptions and showGroupOptions are false then both are displayed @observable private showUserOptions: boolean = false; // whether to show individuals as options when sharing (in the react-select component) @observable private showGroupOptions: boolean = false; // // whether to show groups as options when sharing (in the react-select component) private populating: boolean = false; // whether the list of users is populating or not @observable private layoutDocAcls: boolean = false; // whether the layout doc or data doc's acls are to be used + @observable private myDocAcls: boolean = false; // private get linkVisible() { // return this.sharingDoc ? this.sharingDoc[PublicKey] !== SharingPermissions.None : false; // } - public open = (target: DocumentView) => { + public open = (target?: DocumentView, target_doc?: Doc) => { runInAction(() => this.users = []); - // SelectionManager.DeselectAll(); this.populateUsers(); runInAction(() => { this.targetDocView = target; - this.targetDoc = target.props.Document; + this.targetDoc = target_doc || target?.props.Document; DictationOverlay.Instance.hasActiveModal = true; - this.isOpen = true; - this.permissions = SharingPermissions.Edit; + this.isOpen = this.targetDoc !== undefined; + this.permissions = SharingPermissions.Add; }); - this.targetDoc!.author === Doc.CurrentUserEmail && !this.targetDoc![`ACL-${Doc.CurrentUserEmail.replace(".", "_")}`] && distributeAcls(`ACL-${Doc.CurrentUserEmail.replace(".", "_")}`, SharingPermissions.Admin, this.targetDoc!); } public close = action(() => { this.isOpen = false; - this.selectedUsers = null; // resets the list of users and seleected users (in the react-select component) + this.selectedUsers = null; // resets the list of users and selected users (in the react-select component) TaskCompletionBox.taskCompleted = false; setTimeout(action(() => { // this.copied = false; @@ -120,7 +119,7 @@ export default class SharingManager extends React.Component<{}> { } /** - * Populates the list of validated users (this.users) by adding registered users which have a sidebar-sharing. + * Populates the list of validated users (this.users) by adding registered users which have a mySharedDocs. */ populateUsers = async () => { if (!this.populating) { @@ -133,10 +132,11 @@ export default class SharingManager extends React.Component<{}> { if (isCandidate) { const userDocument = await DocServer.GetRefField(user.userDocumentId); if (userDocument instanceof Doc) { - const notificationDoc = await Cast(userDocument["sidebar-sharing"], Doc); + const notificationDoc = await Cast(userDocument.mySharedDocs, Doc); + const userColor = StrCast(userDocument.userColor); runInAction(() => { if (notificationDoc instanceof Doc) { - this.users.push({ user, notificationDoc }); + this.users.push({ user, notificationDoc, userColor }); } }); } @@ -151,22 +151,30 @@ export default class SharingManager extends React.Component<{}> { * @param group * @param permission */ - setInternalGroupSharing = (group: Doc, permission: string, targetDoc?: Doc) => { - const members: string[] = JSON.parse(StrCast(group.members)); - const users: ValidatedUser[] = this.users.filter(({ user: { email } }) => members.includes(email)); + setInternalGroupSharing = (group: Doc | { groupName: string }, permission: string, targetDoc?: Doc) => { const target = targetDoc || this.targetDoc!; const key = StrCast(group.groupName).replace(".", "_"); - const ACL = `ACL-${key}`; + const acl = `acl-${key}`; + + const docs = SelectionManager.SelectedDocuments().length < 2 ? [target] : SelectionManager.SelectedDocuments().map(docView => docView.props.Document); - GetEffectiveAcl(target) === AclAdmin && distributeAcls(ACL, permission as SharingPermissions, target); + docs.forEach(doc => { + doc.author === Doc.CurrentUserEmail && !doc[`acl-${Doc.CurrentUserEmail.replace(".", "_")}`] && distributeAcls(`acl-${Doc.CurrentUserEmail.replace(".", "_")}`, SharingPermissions.Admin, doc); + distributeAcls(acl, permission as SharingPermissions, doc); - // if documents have been shared, add the target to that list if it doesn't already exist, otherwise create a new list with the target - group.docsShared ? Doc.IndexOf(target, DocListCast(group.docsShared)) === -1 && (group.docsShared as List<Doc>).push(target) : group.docsShared = new List<Doc>([target]); + if (group instanceof Doc) { + const members: string[] = JSON.parse(StrCast(group.members)); + const users: ValidatedUser[] = this.users.filter(({ user: { email } }) => members.includes(email)); + + // if documents have been shared, add the doc to that list if it doesn't already exist, otherwise create a new list with the doc + group.docsShared ? Doc.IndexOf(doc, DocListCast(group.docsShared)) === -1 && (group.docsShared as List<Doc>).push(doc) : group.docsShared = new List<Doc>([doc]); - users.forEach(({ user, notificationDoc }) => { - if (permission !== SharingPermissions.None) Doc.IndexOf(target, DocListCast(notificationDoc[storage])) === -1 && Doc.AddDocToList(notificationDoc, storage, target); // add the target to the notificationDoc if it hasn't already been added - else GetEffectiveAcl(target, undefined, user.email) === AclPrivate && Doc.IndexOf((target.aliasOf as Doc || target), DocListCast(notificationDoc[storage])) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, (target.aliasOf as Doc || target)); // remove the target from the list if it already exists + users.forEach(({ user, notificationDoc }) => { + if (permission !== SharingPermissions.None) Doc.IndexOf(doc, DocListCast(notificationDoc[storage])) === -1 && Doc.AddDocToList(notificationDoc, storage, doc); // add the doc to the notificationDoc if it hasn't already been added + else GetEffectiveAcl(doc, undefined, user.email) === AclPrivate && Doc.IndexOf((doc.aliasOf as Doc || doc), DocListCast(notificationDoc[storage])) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, (doc.aliasOf as Doc || doc)); // remove the doc from the list if it already exists + }); + } }); } @@ -183,13 +191,19 @@ export default class SharingManager extends React.Component<{}> { /** * Called from the properties sidebar to change permissions of a user. */ - shareFromPropertiesSidebar = (shareWith: string, permission: SharingPermissions, target: Doc) => { - if (shareWith !== "Public") { + shareFromPropertiesSidebar = (shareWith: string, permission: SharingPermissions, docs: Doc[]) => { + if (shareWith !== "Public" && shareWith !== "Override") { const user = this.users.find(({ user: { email } }) => email === (shareWith === "Me" ? Doc.CurrentUserEmail : shareWith)); - if (user) this.setInternalSharing(user, permission, target); - else this.setInternalGroupSharing(GroupManager.Instance.getGroup(shareWith)!, permission, target); + docs.forEach(doc => { + if (user) this.setInternalSharing(user, permission, doc); + else this.setInternalGroupSharing(GroupManager.Instance.getGroup(shareWith)!, permission, doc); + }); + } + else { + docs.forEach(doc => { + if (GetEffectiveAcl(doc) === AclAdmin) distributeAcls(`acl-${shareWith}`, permission, doc); + }); } - else if (GetEffectiveAcl(target) === AclAdmin) distributeAcls("ACL-Public", permission, target); } /** @@ -214,9 +228,9 @@ export default class SharingManager extends React.Component<{}> { removeGroup = (group: Doc) => { if (group.docsShared) { DocListCast(group.docsShared).forEach(doc => { - const ACL = `ACL-${StrCast(group.groupName)}`; + const acl = `acl-${StrCast(group.groupName)}`; - distributeAcls(ACL, SharingPermissions.None, doc); + distributeAcls(acl, SharingPermissions.None, doc); const members: string[] = JSON.parse(StrCast(group.members)); const users: ValidatedUser[] = this.users.filter(({ user: { email } }) => members.includes(email)); @@ -233,13 +247,17 @@ export default class SharingManager extends React.Component<{}> { const { user, notificationDoc } = recipient; const target = targetDoc || this.targetDoc!; const key = user.email.replace('.', '_'); - const ACL = `ACL-${key}`; + const acl = `acl-${key}`; - GetEffectiveAcl(target) === AclAdmin && distributeAcls(ACL, permission as SharingPermissions, target); + const docs = SelectionManager.SelectedDocuments().length < 2 ? [target] : SelectionManager.SelectedDocuments().map(docView => docView.props.Document); - if (permission !== SharingPermissions.None) Doc.IndexOf(target, DocListCast(notificationDoc[storage])) === -1 && Doc.AddDocToList(notificationDoc, storage, target); - else GetEffectiveAcl(target, undefined, user.email) === AclPrivate && Doc.IndexOf((target.aliasOf as Doc || target), DocListCast(notificationDoc[storage])) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, (target.aliasOf as Doc || target)); + docs.forEach(doc => { + doc.author === Doc.CurrentUserEmail && !doc[`acl-${Doc.CurrentUserEmail.replace(".", "_")}`] && distributeAcls(`acl-${Doc.CurrentUserEmail.replace(".", "_")}`, SharingPermissions.Admin, doc); + distributeAcls(acl, permission as SharingPermissions, doc); + if (permission !== SharingPermissions.None) Doc.IndexOf(doc, DocListCast(notificationDoc[storage])) === -1 && Doc.AddDocToList(notificationDoc, storage, doc); + else GetEffectiveAcl(doc, undefined, user.email) === AclPrivate && Doc.IndexOf((doc.aliasOf as Doc || doc), DocListCast(notificationDoc[storage])) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, (doc.aliasOf as Doc || doc)); + }); } @@ -269,11 +287,14 @@ export default class SharingManager extends React.Component<{}> { /** * Returns the SharingPermissions (Admin, Can Edit etc) access that's used to share */ - private get sharingOptions() { - return Object.values(SharingPermissions).map(permission => + private sharingOptions(uniform: boolean, override?: boolean) { + const dropdownValues: string[] = Object.values(SharingPermissions); + if (!uniform) dropdownValues.unshift("-multiple-"); + if (override) dropdownValues.unshift("None"); + return dropdownValues.filter(permission => permission !== SharingPermissions.View).map(permission => ( <option key={permission} value={permission}> - {permission} + {permission === SharingPermissions.Add ? "Can Augment" : permission} </option> ) ); @@ -281,27 +302,30 @@ export default class SharingManager extends React.Component<{}> { private focusOn = (contents: string) => { const title = this.targetDoc ? StrCast(this.targetDoc.title) : ""; + const docs = SelectionManager.SelectedDocuments().length > 1 ? SelectionManager.SelectedDocuments().map(docView => docView.props.Document) : [this.targetDoc]; return ( <span className={"focus-span"} title={title} onClick={() => { let context: Opt<CollectionView>; - if (this.targetDoc && this.targetDocView && (context = this.targetDocView.props.ContainingCollectionView)) { + if (this.targetDoc && this.targetDocView && docs.length === 1 && (context = this.targetDocView.props.ContainingCollectionView)) { DocumentManager.Instance.jumpToDocument(this.targetDoc, true, undefined, context.props.Document); } }} onPointerEnter={action(() => { - if (this.targetDoc) { - Doc.BrushDoc(this.targetDoc); + if (docs.length) { + docs.forEach(doc => doc && Doc.BrushDoc(doc)); this.dialogueBoxOpacity = 0.1; this.overlayOpacity = 0.1; } })} onPointerLeave={action(() => { - this.targetDoc && Doc.UnBrushDoc(this.targetDoc); - this.dialogueBoxOpacity = 1; - this.overlayOpacity = 0.4; + if (docs.length) { + docs.forEach(doc => doc && Doc.UnBrushDoc(doc)); + this.dialogueBoxOpacity = 1; + this.overlayOpacity = 0.4; + } })} > {contents} @@ -351,6 +375,25 @@ export default class SharingManager extends React.Component<{}> { } } + distributeOverCollection = (targetDoc?: Doc) => { + const AclMap = new Map<symbol, string>([ + [AclPrivate, SharingPermissions.None], + [AclReadonly, SharingPermissions.View], + [AclAddonly, SharingPermissions.Add], + [AclEdit, SharingPermissions.Edit], + [AclAdmin, SharingPermissions.Admin] + ]); + + const target = targetDoc || this.targetDoc!; + + const docs = SelectionManager.SelectedDocuments().length < 2 ? [target] : SelectionManager.SelectedDocuments().map(docView => docView.props.Document); + docs.forEach(doc => { + for (const [key, value] of Object.entries(doc[AclSym])) { + distributeAcls(key, AclMap.get(value)! as SharingPermissions, target); + } + }); + } + /** * Sorting algorithm to sort users. */ @@ -372,67 +415,66 @@ export default class SharingManager extends React.Component<{}> { /** * @returns the main interface of the SharingManager. */ - private get sharingInterface() { - + @computed get sharingInterface() { + TraceMobx(); const groupList = GroupManager.Instance?.getAllGroups() || []; - const sortedUsers = this.users.slice().sort(this.sortUsers) - .map(({ user: { email } }) => ({ label: email, value: indType + email })); - const sortedGroups = groupList.slice().sort(this.sortGroups) - .map(({ groupName }) => ({ label: StrCast(groupName), value: groupType + StrCast(groupName) })); + const sortedUsers = this.users.slice().sort(this.sortUsers).map(({ user: { email } }) => ({ label: email, value: indType + email })); + const sortedGroups = groupList.slice().sort(this.sortGroups).map(({ groupName }) => ({ label: StrCast(groupName), value: groupType + StrCast(groupName) })); // the next block handles the users shown (individuals/groups/both) const options: GroupedOptions[] = []; if (GroupManager.Instance) { if ((this.showUserOptions && this.showGroupOptions) || (!this.showUserOptions && !this.showGroupOptions)) { - options.push({ - label: 'Individuals', - options: sortedUsers - }, - { - label: 'Groups', - options: sortedGroups - }); - } - else if (this.showUserOptions) { - options.push({ - label: 'Individuals', - options: sortedUsers - }); - } - else { - options.push({ - label: 'Groups', - options: sortedGroups - }); + options.push( + { label: 'Individuals', options: sortedUsers }, + { label: 'Groups', options: sortedGroups }); } + else if (this.showUserOptions) options.push({ label: 'Individuals', options: sortedUsers }); + else options.push({ label: 'Groups', options: sortedGroups }); } const users = this.individualSort === "ascending" ? this.users.slice().sort(this.sortUsers) : this.individualSort === "descending" ? this.users.slice().sort(this.sortUsers).reverse() : this.users; const groups = this.groupSort === "ascending" ? groupList.slice().sort(this.sortGroups) : this.groupSort === "descending" ? groupList.slice().sort(this.sortGroups).reverse() : groupList; - const targetDoc = this.layoutDocAcls ? this.targetDoc : this.targetDoc?.[DataSym]; + // handles the case where multiple documents are selected + let docs = SelectionManager.SelectedDocuments().length < 2 ? + [this.layoutDocAcls ? this.targetDoc : this.targetDoc?.[DataSym]] + : SelectionManager.SelectedDocuments().map(docView => this.layoutDocAcls ? docView.props.Document : docView.props.Document?.[DataSym]); - const effectiveAcl = targetDoc ? GetEffectiveAcl(targetDoc) : AclPrivate; + if (this.myDocAcls) { + const newDocs: Doc[] = []; + SearchBox.foreachRecursiveDoc(docs, doc => newDocs.push(doc)); + docs = newDocs.filter(doc => GetEffectiveAcl(doc) === AclAdmin); + } + + const targetDoc = docs[0]; + + // tslint:disable-next-line: no-unnecessary-callback-wrapper + const admin = this.myDocAcls ? Boolean(docs.length) : docs.map(doc => GetEffectiveAcl(doc)).every(acl => acl === AclAdmin); // if the user has admin access to all selected docs + + // users in common between all docs + const commonKeys = intersection(...docs.map(doc => this.layoutDocAcls ? doc?.[AclSym] && Object.keys(doc[AclSym]) : doc?.[DataSym]?.[AclSym] && Object.keys(doc[DataSym][AclSym]))); // the list of users shared with - const userListContents: (JSX.Element | null)[] = users.map(({ user, notificationDoc }) => { - const userKey = user.email.replace('.', '_'); - const permissions = StrCast(targetDoc?.[`ACL-${userKey}`]); + const userListContents: (JSX.Element | null)[] = users.filter(({ user }) => docs.length > 1 ? commonKeys.includes(`acl-${user.email.replace('.', '_')}`) : docs[0]?.author !== user.email).map(({ user, notificationDoc, userColor }) => { + const userKey = `acl-${user.email.replace('.', '_')}`; + const uniform = docs.every(doc => this.layoutDocAcls ? doc?.[AclSym]?.[userKey] === docs[0]?.[AclSym]?.[userKey] : doc?.[DataSym]?.[AclSym]?.[userKey] === docs[0]?.[DataSym]?.[AclSym]?.[userKey]); + const permissions = uniform ? StrCast(targetDoc?.[userKey]) : "-multiple-"; - return !permissions || user.email === targetDoc?.author ? (null) : ( + return !permissions ? (null) : ( <div key={userKey} className={"container"} > <span className={"padding"}>{user.email}</span> <div className="edit-actions"> - {effectiveAcl === AclAdmin ? ( + {admin || this.myDocAcls ? ( <select className={"permissions-dropdown"} value={permissions} - onChange={e => this.setInternalSharing({ user, notificationDoc }, e.currentTarget.value)} + onChange={e => this.setInternalSharing({ user, notificationDoc, userColor }, e.currentTarget.value)} > - {this.sharingOptions} + {this.sharingOptions(uniform)} </select> ) : ( <div className={"permissions-dropdown"}> @@ -444,22 +486,26 @@ export default class SharingManager extends React.Component<{}> { ); }); + // checks if every doc has the same author + const sameAuthor = docs.every(doc => doc?.author === docs[0]?.author); + // the owner of the doc and the current user are placed at the top of the user list. userListContents.unshift( - ( - <div - key={"owner"} - className={"container"} - > - <span className={"padding"}>{targetDoc?.author === Doc.CurrentUserEmail ? "Me" : targetDoc?.author}</span> - <div className="edit-actions"> - <div className={"permissions-dropdown"}> - Owner + sameAuthor ? + ( + <div + key={"owner"} + className={"container"} + > + <span className={"padding"}>{targetDoc?.author === Doc.CurrentUserEmail ? "Me" : targetDoc?.author}</span> + <div className="edit-actions"> + <div className={"permissions-dropdown"}> + Owner + </div> </div> </div> - </div> - ), - targetDoc?.author !== Doc.CurrentUserEmail ? + ) : null, + sameAuthor && targetDoc?.author !== Doc.CurrentUserEmail ? ( <div key={"me"} @@ -468,42 +514,52 @@ export default class SharingManager extends React.Component<{}> { <span className={"padding"}>Me</span> <div className="edit-actions"> <div className={"permissions-dropdown"}> - {targetDoc?.[`ACL-${Doc.CurrentUserEmail.replace(".", "_")}`]} + {targetDoc?.[`acl-${Doc.CurrentUserEmail.replace(".", "_")}`]} </div> </div> </div> ) : null ); - // the list of groups shared with - const groupListContents = groups.map(group => { - const permissions = StrCast(targetDoc?.[`ACL-${StrCast(group.groupName)}`]); - return !permissions ? null : ( + // the list of groups shared with + const groupListMap: (Doc | { groupName: string })[] = groups.filter(({ groupName }) => docs.length > 1 ? commonKeys.includes(`acl-${StrCast(groupName).replace('.', '_')}`) : true); + groupListMap.unshift({ groupName: "Public" }, { groupName: "Override" }); + const groupListContents = groupListMap.map(group => { + const groupKey = `acl-${StrCast(group.groupName)}`; + const uniform = docs.every(doc => this.layoutDocAcls ? doc?.[AclSym]?.[groupKey] === docs[0]?.[AclSym]?.[groupKey] : doc?.[DataSym]?.[AclSym]?.[groupKey] === docs[0]?.[DataSym]?.[AclSym]?.[groupKey]); + const permissions = uniform ? StrCast(targetDoc?.[`acl-${StrCast(group.groupName)}`]) : "-multiple-"; + + return !permissions ? (null) : ( <div - key={StrCast(group.groupName)} + key={groupKey} className={"container"} > <div className={"padding"}>{group.groupName}</div> - <div className="group-info" onClick={action(() => GroupManager.Instance.currentGroup = group)}> - <FontAwesomeIcon icon={fa.faInfoCircle} color={"#e8e8e8"} size={"sm"} style={{ backgroundColor: "#1e89d7", borderRadius: "100%", border: "1px solid #1e89d7" }} /> - </div> + {group instanceof Doc ? + (<div className="group-info" onClick={action(() => GroupManager.Instance.currentGroup = group)}> + <FontAwesomeIcon icon={"info-circle"} color={"#e8e8e8"} size={"sm"} style={{ backgroundColor: "#1e89d7", borderRadius: "100%", border: "1px solid #1e89d7" }} /> + </div>) + : (null)} <div className="edit-actions"> - <select - className={"permissions-dropdown"} - value={permissions} - onChange={e => this.setInternalGroupSharing(group, e.currentTarget.value)} - > - {this.sharingOptions} - </select> + {admin || this.myDocAcls ? ( + <select + className={"permissions-dropdown"} + value={permissions} + onChange={e => this.setInternalGroupSharing(group, e.currentTarget.value)} + > + {this.sharingOptions(uniform, group.groupName === "Override")} + </select> + ) : ( + <div className={"permissions-dropdown"}> + {permissions} + </div> + )} </div> </div> ); }); - // don't display the group list if all groups are null - const displayGroupList = !groupListContents?.every(group => group === null); - return ( <div className={"sharing-interface"}> {GroupManager.Instance?.currentGroup ? @@ -512,38 +568,8 @@ export default class SharingManager extends React.Component<{}> { onCloseButtonClick={action(() => GroupManager.Instance.currentGroup = undefined)} /> : null} - {/* <p className={"share-link"}>Manage the public link to {this.focusOn("this document...")}</p> - {!this.linkVisible ? (null) : - <div className={"link-container"}> - <div className={"link-box"} onClick={this.copy}>{this.sharingUrl}</div> - <div - title={"Copy link to clipboard"} - className={"copy"} - style={{ backgroundColor: this.copied ? "lawngreen" : "gainsboro" }} - onClick={this.copy} - > - <FontAwesomeIcon icon={fa.faCopy} /> - </div> - </div> - } - <div className={"people-with-container"}> - {!this.linkVisible ? (null) : <p className={"people-with"}>People with this link</p>} - <select - className={"people-with-select"} - value={this.sharingDoc ? StrCast(this.sharingDoc[PublicKey], SharingPermissions.None) : SharingPermissions.None} - style={{ - marginLeft: this.linkVisible ? 10 : 0, - color: this.sharingDoc ? ColorMapping.get(StrCast(this.sharingDoc[PublicKey], SharingPermissions.None)) : DefaultColor, - borderColor: this.sharingDoc ? ColorMapping.get(StrCast(this.sharingDoc[PublicKey], SharingPermissions.None)) : DefaultColor - }} - onChange={e => this.setExternalSharing(e.currentTarget.value)} - > - {this.sharingOptions} - </select> - </div> - <div className={"hr-substitute"} /> */} <div className="sharing-contents"> - <p className={"share-title"}><b>Share </b>{this.focusOn(StrCast(targetDoc?.title, "this document"))}</p> + <p className={"share-title"}><b>Share </b>{this.focusOn(docs.length < 2 ? StrCast(targetDoc?.title, "this document") : "-multiple-")}</p> <div className={"close-button"} onClick={this.close}> <FontAwesomeIcon icon={"times"} color={"black"} size={"lg"} /> </div> @@ -564,7 +590,7 @@ export default class SharingManager extends React.Component<{}> { }} /> <select className="permissions-select" onChange={this.handlePermissionsChange} value={this.permissions}> - {this.sharingOptions} + {this.sharingOptions(true)} </select> <button ref={this.shareDocumentButtonRef} className="share-button" onClick={this.share}> Share @@ -574,8 +600,18 @@ export default class SharingManager extends React.Component<{}> { <input type="checkbox" onChange={action(() => this.showUserOptions = !this.showUserOptions)} /> <label style={{ marginRight: 10 }}>Individuals</label> <input type="checkbox" onChange={action(() => this.showGroupOptions = !this.showGroupOptions)} /> <label>Groups</label> </div> - <div className="layoutDoc-acls"> - <input type="checkbox" onChange={action(() => this.layoutDocAcls = !this.layoutDocAcls)} checked={this.layoutDocAcls} /> <label>Layout</label> + + <div className="acl-container"> + <div className="myDocs-acls"> + <input type="checkbox" onChange={action(() => this.myDocAcls = !this.myDocAcls)} checked={this.myDocAcls} /> <label>My Docs</label> + </div> + {Doc.UserDoc().noviceMode ? (null) : + <div className="layoutDoc-acls"> + <input type="checkbox" onChange={action(() => this.layoutDocAcls = !this.layoutDocAcls)} checked={this.layoutDocAcls} /> <label>Layout</label> + </div>} + <button className="distribute-button" onClick={() => this.distributeOverCollection()}> + Distribute + </button> </div> </div> } @@ -584,11 +620,11 @@ export default class SharingManager extends React.Component<{}> { <div className="user-sort" onClick={action(() => this.individualSort = this.individualSort === "ascending" ? "descending" : this.individualSort === "descending" ? "none" : "ascending")}> - Individuals {this.individualSort === "ascending" ? <FontAwesomeIcon icon={fa.faCaretUp} size={"xs"} /> - : this.individualSort === "descending" ? <FontAwesomeIcon icon={fa.faCaretDown} size={"xs"} /> - : <FontAwesomeIcon icon={fa.faCaretRight} size={"xs"} />} + Individuals {this.individualSort === "ascending" ? <FontAwesomeIcon icon={"caret-up"} size={"xs"} /> + : this.individualSort === "descending" ? <FontAwesomeIcon icon={"caret-down"} size={"xs"} /> + : <FontAwesomeIcon icon={"caret-right"} size={"xs"} />} </div> - <div className={"users-list"} style={{ display: "block" }}>{/*200*/} + <div className={"users-list"}> {userListContents} </div> </div> @@ -596,23 +632,13 @@ export default class SharingManager extends React.Component<{}> { <div className="user-sort" onClick={action(() => this.groupSort = this.groupSort === "ascending" ? "descending" : this.groupSort === "descending" ? "none" : "ascending")}> - Groups {this.groupSort === "ascending" ? <FontAwesomeIcon icon={fa.faCaretUp} size={"xs"} /> - : this.groupSort === "descending" ? <FontAwesomeIcon icon={fa.faCaretDown} size={"xs"} /> - : <FontAwesomeIcon icon={fa.faCaretRight} size={"xs"} />} + Groups {this.groupSort === "ascending" ? <FontAwesomeIcon icon={"caret-up"} size={"xs"} /> + : this.groupSort === "descending" ? <FontAwesomeIcon icon={"caret-down"} size={"xs"} /> + : <FontAwesomeIcon icon={"caret-right"} size={"xs"} />} </div> - <div className={"groups-list"} style={{ display: !displayGroupList ? "flex" : "block" }}>{/*200*/} - { - !displayGroupList ? - <div - className={"none"} - > - There are no groups this document has been shared with. - </div> - : - groupListContents - } - + <div className={"groups-list"}> + {groupListContents} </div> </div> </div> @@ -623,16 +649,13 @@ export default class SharingManager extends React.Component<{}> { } render() { - return ( - <MainViewModal - contents={this.sharingInterface} - isDisplayed={this.isOpen} - interactive={true} - dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity} - overlayDisplayedOpacity={this.overlayOpacity} - closeOnExternalClick={this.close} - /> - ); + return <MainViewModal + contents={this.sharingInterface} + isDisplayed={this.isOpen} + interactive={true} + dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity} + overlayDisplayedOpacity={this.overlayOpacity} + closeOnExternalClick={this.close} + />; } - }
\ No newline at end of file diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts index c7b7bb215..0f7ad6d0a 100644 --- a/src/client/util/UndoManager.ts +++ b/src/client/util/UndoManager.ts @@ -128,6 +128,7 @@ export namespace UndoManager { } export function StartBatch(batchName: string): Batch { + // console.log("Start " + batchCounter + " " + batchName); batchCounter++; if (batchCounter > 0 && currentBatch === undefined) { currentBatch = []; @@ -137,6 +138,7 @@ export namespace UndoManager { const EndBatch = action((cancel: boolean = false) => { batchCounter--; + // console.log("End " + batchCounter); if (batchCounter === 0 && currentBatch?.length) { if (!cancel) { undoStack.push(currentBatch); diff --git a/src/client/views/AntimodeMenu.tsx b/src/client/views/AntimodeMenu.tsx index ccc54058d..5acb3e4c8 100644 --- a/src/client/views/AntimodeMenu.tsx +++ b/src/client/views/AntimodeMenu.tsx @@ -1,5 +1,5 @@ import React = require("react"); -import { observable, action } from "mobx"; +import { observable, action, runInAction } from "mobx"; import "./AntimodeMenu.scss"; export interface AntimodeMenuProps { } @@ -8,7 +8,7 @@ export interface AntimodeMenuProps { * This is an abstract class that serves as the base for a PDF-style or Marquee-style * menu. To use this class, look at PDFMenu.tsx or MarqueeOptionsMenu.tsx for an example. */ -export default abstract class AntimodeMenu<T extends AntimodeMenuProps> extends React.Component<T, {}> { +export abstract class AntimodeMenu<T extends AntimodeMenuProps> extends React.Component<T, {}> { protected _offsetY: number = 0; protected _offsetX: number = 0; protected _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); @@ -86,7 +86,7 @@ export default abstract class AntimodeMenu<T extends AntimodeMenuProps> extends @action protected togglePin = (e: React.MouseEvent) => { - this.Pinned = !this.Pinned; + runInAction(() => this.Pinned = !this.Pinned); } protected dragStart = (e: React.PointerEvent) => { diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx index 07f7b8e6d..1f7abf61a 100644 --- a/src/client/views/ContextMenu.tsx +++ b/src/client/views/ContextMenu.tsx @@ -3,14 +3,9 @@ import { ContextMenuItem, ContextMenuProps, OriginalMenuProps } from "./ContextM import { observable, action, computed, runInAction, IReactionDisposer, reaction } from "mobx"; import { observer } from "mobx-react"; import "./ContextMenu.scss"; -import { library } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faSearch, faCircle } from '@fortawesome/free-solid-svg-icons'; import Measure from "react-measure"; -library.add(faSearch); -library.add(faCircle); - @observer export class ContextMenu extends React.Component { static Instance: ContextMenu; @@ -100,7 +95,7 @@ export class ContextMenu extends React.Component { } @action moveAfter(item: ContextMenuProps, after: ContextMenuProps) { - if (this.findByDescription(after.description)) { + if (after && this.findByDescription(after.description)) { const curInd = this._items.findIndex((i) => i.description === item.description); this._items.splice(curInd, 1); const afterInd = this._items.findIndex((i) => i.description === after.description); @@ -238,7 +233,7 @@ export class ContextMenu extends React.Component { <span className="icon-background"> <FontAwesomeIcon icon="search" size="lg" /> </span> - <input className="contextMenu-item contextMenu-description search" type="text" placeholder="Search Menu..." value={this._searchString} onKeyDown={this.onKeyDown} onChange={this.onChange} autoFocus /> + <input className="contextMenu-item contextMenu-description search" type="text" placeholder="Filter Menu..." value={this._searchString} onKeyDown={this.onKeyDown} onChange={this.onChange} autoFocus /> </span> {this.menuItems} </> diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx index 7e233ec04..d3429cdfb 100644 --- a/src/client/views/ContextMenuItem.tsx +++ b/src/client/views/ContextMenuItem.tsx @@ -1,12 +1,10 @@ import React = require("react"); import { observable, action } from "mobx"; import { observer } from "mobx-react"; -import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; -import { faAngleRight } from '@fortawesome/free-solid-svg-icons'; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { UndoManager } from "../util/UndoManager"; - -library.add(faAngleRight); +import { NumberLiteralType } from "typescript"; export interface OriginalMenuProps { description: string; @@ -31,11 +29,12 @@ export type ContextMenuProps = OriginalMenuProps | SubmenuProps; export class ContextMenuItem extends React.Component<ContextMenuProps & { selected?: boolean }> { @observable private _items: Array<ContextMenuProps> = []; @observable private overItem = false; + @observable private subRef = React.createRef<HTMLDivElement>(); constructor(props: ContextMenuProps | SubmenuProps) { super(props); if ("subitems" in this.props) { - this.props.subitems.forEach(i => this._items.push(i)); + this.props.subitems?.forEach(i => this._items.push(i)); } } @@ -54,6 +53,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select currentTimeout?: any; static readonly timeout = 300; _overPosY = 0; + _overPosX = 0; onPointerEnter = (e: React.MouseEvent) => { if (this.currentTimeout) { clearTimeout(this.currentTimeout); @@ -63,6 +63,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select return; } this._overPosY = e.clientY; + this._overPosX = e.clientX; this.currentTimeout = setTimeout(action(() => this.overItem = true), ContextMenuItem.timeout); } @@ -78,6 +79,9 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select } render() { + + + if ("event" in this.props) { return ( <div className={"contextMenu-item" + (this.props.selected ? " contextMenu-itemSelected" : "")} onPointerDown={this.handleEvent}> @@ -94,8 +98,13 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select } else if ("subitems" in this.props) { const where = !this.overItem ? "" : this._overPosY < window.innerHeight / 3 ? "flex-start" : this._overPosY > window.innerHeight * 2 / 3 ? "flex-end" : "center"; const marginTop = !this.overItem ? "" : this._overPosY < window.innerHeight / 3 ? "20px" : this._overPosY > window.innerHeight * 2 / 3 ? "-20px" : ""; + + // here const submenu = !this.overItem ? (null) : - <div className="contextMenu-subMenu-cont" style={{ marginLeft: "90%", left: "0px", marginTop }}> + <div className="contextMenu-subMenu-cont" + style={{ + marginLeft: window.innerHeight - this._overPosX - 50 > 0 ? "90%" : "20%", marginTop + }}> {this._items.map(prop => <ContextMenuItem {...prop} key={prop.description} closeMenu={this.props.closeMenu} />)} </div>; if (!("noexpand" in this.props)) { @@ -115,7 +124,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select <div className="contextMenu-description" onMouseEnter={this.onPointerEnter} style={{ alignItems: "center" }} > {this.props.description} - <FontAwesomeIcon icon={faAngleRight} size="lg" style={{ position: "absolute", right: "10px" }} /> + <FontAwesomeIcon icon={"angle-right"} size="lg" style={{ position: "absolute", right: "10px" }} /> </div> {submenu} </div> diff --git a/src/client/views/DictationOverlay.tsx b/src/client/views/DictationOverlay.tsx index 9ed14509f..f4f96da8a 100644 --- a/src/client/views/DictationOverlay.tsx +++ b/src/client/views/DictationOverlay.tsx @@ -4,7 +4,7 @@ import "normalize.css"; import * as React from 'react'; import { DictationManager } from '../util/DictationManager'; import "./Main.scss"; -import MainViewModal from './MainViewModal'; +import { MainViewModal } from './MainViewModal'; @observer export class DictationOverlay extends React.Component { diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index 23ce71c4f..893b74d75 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -58,7 +58,7 @@ export function ViewBoxBaseComponent<P extends ViewBoxBaseProps, T>(schemaCtor: lookupField = (field: string) => ScriptCast(this.layoutDoc.lookupField)?.script.run({ self: this.layoutDoc, data: this.rootDoc, field: field, container: this.props.ContainingCollectionDoc }).result; - active = (outsideReaction?: boolean) => !this.props.Document.isBackground && (this.props.rootSelected(outsideReaction) || this.props.isSelected(outsideReaction) || this.props.renderDepth === 0 || this.layoutDoc.forceActive);// && !Doc.SelectedTool(); // bcz: inking state shouldn't affect static tools + active = (outsideReaction?: boolean) => !this.props.Document._isBackground && (this.props.rootSelected(outsideReaction) || this.props.isSelected(outsideReaction) || this.props.renderDepth === 0 || this.layoutDoc.forceActive);// && !Doc.SelectedTool(); // bcz: inking state shouldn't affect static tools protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; } return Component; @@ -104,7 +104,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T styleFromLayoutString = (scale: number) => { const style: { [key: string]: any } = {}; - const divKeys = ["width", "height", "background", "top", "position"]; + const divKeys = ["width", "height", "fontSize", "left", "background", "top", "pointerEvents", "position"]; const replacer = (match: any, expr: string, offset: any, string: any) => { // bcz: this executes a script to convert a property expression string: { script } into a value return ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name, scale: "number" })?.script.run({ self: this.rootDoc, this: this.layoutDoc, scale }).result as string || ""; }; @@ -123,7 +123,8 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T @action.bound removeDocument(doc: Doc | Doc[]): boolean { const effectiveAcl = GetEffectiveAcl(this.dataDoc); - if (effectiveAcl === AclAdmin || effectiveAcl === AclEdit) { + const docAcl = GetEffectiveAcl(doc); + if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin || docAcl === AclAdmin) { const docs = doc instanceof Doc ? [doc] : doc; docs.map(doc => doc.isPushpin = doc.annotationOn = undefined); const targetDataDoc = this.dataDoc; @@ -131,7 +132,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T const toRemove = value.filter(v => docs.includes(v)); if (toRemove.length !== 0) { - const recent = Cast(Doc.UserDoc().myRecentlyClosed, Doc) as Doc; + const recent = Cast(Doc.UserDoc().myRecentlyClosedDocs, Doc) as Doc; toRemove.forEach(doc => { Doc.RemoveDocFromList(targetDataDoc, this.props.fieldKey + "-annotations", doc); recent && Doc.AddDocToList(recent, "data", doc, undefined, true, true); @@ -166,7 +167,8 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T added.forEach(d => { for (const [key, value] of Object.entries(this.props.Document[AclSym])) { if (d.author === key.substring(4).replace("_", ".") && !d.aliasOf) distributeAcls(key, SharingPermissions.Admin, d, true); - else distributeAcls(key, this.AclMap.get(value) as SharingPermissions, d, true); + //else if (this.props.Document[key] === SharingPermissions.Admin) distributeAcls(key, SharingPermissions.Add, d, true); + // else distributeAcls(key, this.AclMap.get(value) as SharingPermissions, d, true); } }); } @@ -178,8 +180,6 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T added.map(doc => doc.context = this.props.Document); (targetDataDoc[this.annotationKey] as List<Doc>).push(...added); targetDataDoc[this.annotationKey + "-lastModified"] = new DateField(new Date(Date.now())); - const lastModified = "lastModified"; - targetDataDoc[lastModified] = new DateField(new Date(Date.now())); } } } @@ -187,9 +187,9 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T } whenActiveChanged = action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive)); - active = (outsideReaction?: boolean) => ((Doc.GetSelectedTool() === InkTool.None && !this.props.Document.isBackground) && + active = (outsideReaction?: boolean) => ((Doc.GetSelectedTool() === InkTool.None && !this.props.Document._) && (this.props.rootSelected(outsideReaction) || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0 || BoolCast((this.layoutDoc as any).forceActive)) ? true : false) - annotationsActive = (outsideReaction?: boolean) => (Doc.GetSelectedTool() !== InkTool.None || (this.props.Document.isBackground && this.props.active()) || + annotationsActive = (outsideReaction?: boolean) => (Doc.GetSelectedTool() !== InkTool.None || (this.props.Document._isBackground && this.props.active()) || (this.props.Document.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false) } return Component; diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index 8748b1880..d8c32a919 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -1,45 +1,34 @@ -import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; -import { faArrowAltCircleDown, faArrowAltCircleRight, faArrowAltCircleUp, faCheckCircle, faCloudUploadAlt, faLink, faPhotoVideo, faShare, faStopCircle, faSyncAlt, faTag, faTimes } from '@fortawesome/free-solid-svg-icons'; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Tooltip } from '@material-ui/core'; import { action, computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCast } from "../../fields/Doc"; +import { Doc } from "../../fields/Doc"; import { RichTextField } from '../../fields/RichTextField'; -import { Cast, NumCast } from "../../fields/Types"; -import { emptyFunction, setupMoveUpEvents } from "../../Utils"; -import GoogleAuthenticationManager from '../apis/GoogleAuthenticationManager'; +import { Cast, NumCast, StrCast } from "../../fields/Types"; +import { emptyFunction, setupMoveUpEvents, simulateMouseClick } from "../../Utils"; +import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager'; import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils'; -import { Docs, DocUtils } from '../documents/Documents'; +import { Docs } from '../documents/Documents'; +import { DocumentType } from '../documents/DocumentTypes'; +import { CurrentUserUtils } from '../util/CurrentUserUtils'; import { DragManager } from '../util/DragManager'; -import { CollectionDockingView, DockedFrameRenderer } from './collections/CollectionDockingView'; -import { ParentDocSelector } from './collections/ParentDocumentSelector'; -import './collections/ParentDocumentSelector.scss'; +import { SelectionManager } from '../util/SelectionManager'; +import { SharingManager } from '../util/SharingManager'; +import { CollectionDockingView } from './collections/CollectionDockingView'; +import { TabDocView } from './collections/TabDocView'; import './DocumentButtonBar.scss'; import { MetadataEntryMenu } from './MetadataEntryMenu'; +import { DocumentLinksButton } from './nodes/DocumentLinksButton'; import { DocumentView } from './nodes/DocumentView'; import { GoogleRef } from "./nodes/formattedText/FormattedTextBox"; import { TemplateMenu } from "./TemplateMenu"; -import { Template, Templates } from "./Templates"; import React = require("react"); -import { DocumentLinksButton } from './nodes/DocumentLinksButton'; -import { Tooltip } from '@material-ui/core'; +import { PresBox } from './nodes/PresBox'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; -library.add(faLink); -library.add(faTag); -library.add(faTimes); -library.add(faArrowAltCircleDown); -library.add(faArrowAltCircleUp); -library.add(faArrowAltCircleRight); -library.add(faStopCircle); -library.add(faCheckCircle); -library.add(faCloudUploadAlt); -library.add(faSyncAlt); -library.add(faShare); -library.add(faPhotoVideo); - const cloud: IconProp = "cloud-upload-alt"; const fetch: IconProp = "sync-alt"; @@ -164,11 +153,11 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV e.preventDefault(); let googleDoc = await Cast(dataDoc.googleDoc, Doc); if (!googleDoc) { - const options = { _width: 600, _nativeWidth: 960, _nativeHeight: 800, isAnnotating: false, UseCors: false }; + const options = { _width: 600, _fitWidth: true, _nativeWidth: 960, _nativeHeight: 800, isAnnotating: false, useCors: false }; googleDoc = Docs.Create.WebDocument(googleDocUrl, options); dataDoc.googleDoc = googleDoc; } - CollectionDockingView.AddRightSplit(googleDoc); + CollectionDockingView.AddSplit(googleDoc, "right"); } else if (e.altKey) { e.preventDefault(); window.open(googleDocUrl); @@ -196,16 +185,80 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV get pinButton() { const targetDoc = this.view0?.props.Document; const isPinned = targetDoc && Doc.isDocPinned(targetDoc); - return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{Doc.isDocPinned(targetDoc) ? "Unpin from presentation" : "Pin to presentation"}</div></>}> + return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{"Pin to presentation"}</div></>}> <div className="documentButtonBar-linker" - style={{ backgroundColor: isPinned ? "white" : "", color: isPinned ? "black" : "white", border: isPinned ? "black 1px solid " : "" }} - onClick={e => DockedFrameRenderer.PinDoc(targetDoc, isPinned)}> - <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="map-pin" - /> + style={{ color: "white" }} + onClick={e => this.props.views().map(view => view && TabDocView.PinDoc(view.props.Document, false))}> + <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="map-pin" /> </div></Tooltip>; } @computed + get pinWithViewButton() { + const presPinWithViewIcon = <img src="/assets/pinWithView.png" style={{ margin: "auto", width: 17, transform: 'translate(0, 1px)' }} />; + const targetDoc = this.view0?.props.Document; + return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{"Pin with current view"}</div></>}> + <div className="documentButtonBar-linker" + onClick={e => { + if (targetDoc) { + TabDocView.PinDoc(targetDoc, false); + const activeDoc = PresBox.Instance.childDocs[PresBox.Instance.childDocs.length - 1]; + const x = targetDoc._panX; + const y = targetDoc._panY; + const scale = targetDoc._viewScale; + activeDoc.presPinView = true; + activeDoc.presPinViewX = x; + activeDoc.presPinViewY = y; + activeDoc.presPinViewScale = scale; + } + }}> + {presPinWithViewIcon} + </div> + </Tooltip>; + } + + @computed + get shareButton() { + const targetDoc = this.view0?.props.Document; + return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{"Open Sharing Manager"}</div></>}> + <div className="documentButtonBar-linker" style={{ color: "white" }} onClick={e => SharingManager.Instance.open(this.view0, targetDoc)}> + <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="users" + /> + </div></Tooltip >; + } + + @computed + get annotateButton() { + const targetDoc = this.view0?.props.Document; + const isAnnotating = targetDoc?.isAnnotating; + return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{`${isAnnotating ? "Exit" : "Enter"} annotation mode`}</div></>}> + <div className="documentButtonBar-linker" style={{ backgroundColor: isAnnotating ? "white" : "", color: isAnnotating ? "black" : "white", }} + onClick={e => targetDoc.isAnnotating = !targetDoc.isAnnotating}> + <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="edit" /> + </div></Tooltip >; + } + + @computed + get menuButton() { + const targetDoc = this.view0?.props.Document; + return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{`Open Context Menu`}</div></>}> + <div className="documentButtonBar-linker" style={{ color: "white", cursor: "pointer" }} onClick={e => this.openContextMenu(e)}> + <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="bars" /> + </div></Tooltip >; + } + + @computed + get moreButton() { + const targetDoc = this.view0?.props.Document; + return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{`${CurrentUserUtils.propertiesWidth > 0 ? "Close" : "Open"} Properties Panel`}</div></>}> + <div className="documentButtonBar-linker" style={{ color: "white", cursor: "e-resize" }} onClick={action(e => + CurrentUserUtils.propertiesWidth = CurrentUserUtils.propertiesWidth > 0 ? 0 : 250)}> + <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="ellipsis-h" + /> + </div></Tooltip >; + } + + @computed get metadataButton() { const view0 = this.view0; return !view0 ? (null) : <Tooltip title={<><div className="dash-tooltip">Show metadata panel</div></>}> @@ -218,73 +271,78 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV </Flyout> </div></Tooltip>; } - - @computed - get contextButton() { - return !this.view0 ? (null) : <ParentDocSelector Document={this.view0.props.Document} addDocTab={(doc, where) => { - where === "onRight" ? CollectionDockingView.AddRightSplit(doc) : - this.props.stack ? CollectionDockingView.Instance.AddTab(this.props.stack, doc) : - this.view0?.props.addDocTab(doc, "onRight"); - return true; - }} />; - } - @observable _aliasDown = false; - onAliasButtonDown = (e: React.PointerEvent): void => { + onAliasButtonDown = action((e: React.PointerEvent): void => { + this.props.views()[0]?.select(false); + this._tooltipOpen = false; setupMoveUpEvents(this, e, this.onAliasButtonMoved, emptyFunction, emptyFunction); - } + }); onAliasButtonMoved = () => { if (this._dragRef.current) { const dragDocView = this.view0!; const dragData = new DragManager.DocumentDragData([dragDocView.props.Document]); const [left, top] = dragDocView.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); - dragData.dropAction = "alias"; - DragManager.StartDocumentDrag([dragDocView.ContentDiv!], dragData, left, top, { - offsetX: dragData.offset[0], - offsetY: dragData.offset[1], - hideSource: false - }); + dragData.defaultDropAction = "alias"; + dragData.canEmbed = true; + DragManager.StartDocumentDrag([dragDocView.ContentDiv!], dragData, left, top, { hideSource: false }); return true; } return false; } + _ref = React.createRef<HTMLDivElement>(); + @observable _tooltipOpen: boolean = false; @computed get templateButton() { const view0 = this.view0; - const templates: Map<Template, boolean> = new Map(); + const templates: Map<string, boolean> = new Map(); const views = this.props.views(); - Array.from(Object.values(Templates.TemplateList)).map(template => - templates.set(template, views.reduce((checked, doc) => checked || doc?.props.Document["_show" + template.Name] ? true : false, false as boolean))); + Array.from(["Caption", "Title", "TitleHover"]).map(template => + templates.set(template, views.reduce((checked, doc) => checked || doc?.props.Document["_show" + template] ? true : false, false as boolean))); return !view0 ? (null) : - <Tooltip title={<><div className="dash-tooltip">Tap: Customize layout. Drag: Create alias</div></>}> - <div className="documentButtonBar-linkFlyout" ref={this._dragRef}> + <Tooltip title={<div className="dash-tooltip">Tap to Customize Layout. Drag an embeddable alias</div>} open={this._tooltipOpen} onClose={action(() => this._tooltipOpen = false)} placement="bottom"> + <div className="documentButtonBar-linkFlyout" ref={this._dragRef} + onPointerEnter={action(() => !this._ref.current?.getBoundingClientRect().width && (this._tooltipOpen = true))} > + <Flyout anchorPoint={anchorPoints.LEFT_TOP} onOpen={action(() => this._aliasDown = true)} onClose={action(() => this._aliasDown = false)} - content={!this._aliasDown ? (null) : <TemplateMenu docViews={views.filter(v => v).map(v => v as DocumentView)} templates={templates} />}> + content={!this._aliasDown ? (null) : <div ref={this._ref}> <TemplateMenu docViews={views.filter(v => v).map(v => v as DocumentView)} templates={templates} /></div>}> <div className={"documentButtonBar-linkButton-empty"} ref={this._dragRef} onPointerDown={this.onAliasButtonDown} > {<FontAwesomeIcon className="documentdecorations-icon" icon="edit" size="sm" />} </div> </Flyout> - </div></Tooltip>; + </div> + </Tooltip>; + } + + openContextMenu = (e: React.MouseEvent) => { + let child = SelectionManager.SelectedDocuments()[0].ContentDiv!.children[0]; + while (child.children.length) { + const next = Array.from(child.children).find(c => typeof (c.className) === "string"); + if (next?.className.includes("documentView-node")) break; + if (next?.className.includes("dashFieldView")) break; + if (next) child = next; + else break; + } + simulateMouseClick(child, e.clientX, e.clientY - 30, e.screenX, e.screenY - 30); } render() { if (!this.view0) return (null); - const isText = this.view0.props.Document[Doc.LayoutFieldKey(this.view0.props.Document)] instanceof RichTextField; + const isText = this.view0.props.Document[this.view0.LayoutFieldKey] instanceof RichTextField; const considerPull = isText && this.considerGoogleDocsPull; const considerPush = isText && this.considerGoogleDocsPush; return <div className="documentButtonBar"> <div className="documentButtonBar-button"> <DocumentLinksButton links={this.view0.allLinks} View={this.view0} AlwaysOn={true} InMenu={true} StartLink={true} /> </div> - {DocumentLinksButton.StartLink || !Doc.UserDoc()["documentLinksButton-hideEnd"] ? <div className="documentButtonBar-button"> + {DocumentLinksButton.StartLink || !Doc.UserDoc()["documentLinksButton-fullMenu"] ? <div className="documentButtonBar-button"> <DocumentLinksButton links={this.view0.allLinks} View={this.view0} AlwaysOn={true} InMenu={true} StartLink={false} /> </div> : (null)} - {/* <div className="documentButtonBar-button"> + {!Doc.UserDoc()["documentLinksButton-fullMenu"] ? (null) : <div className="documentButtonBar-button"> {this.templateButton} </div> - <div className="documentButtonBar-button"> + /*<div className="documentButtonBar-button"> {this.metadataButton} </div> <div className="documentButtonBar-button"> @@ -293,12 +351,27 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV <div className="documentButtonBar-button"> {this.pinButton} </div> - {/* <div className="documentButtonBar-button" style={{ display: !considerPush ? "none" : "" }}> + {/* <div className="documentButtonBar-button"> + {this.pinWithViewButton} + </div> */} + {!Doc.UserDoc()["documentLinksButton-fullMenu"] ? (null) : <div className="documentButtonBar-button"> + {this.shareButton} + </div>} + {![DocumentType.VID, DocumentType.WEB].includes(StrCast(this.view0.props.Document.type) as DocumentType) ? (null) : <div className="documentButtonBar-button"> + {this.annotateButton} + </div>} + {!Doc.UserDoc()["documentLinksButton-fullMenu"] ? (null) : <div className="documentButtonBar-button" style={{ display: !considerPush ? "none" : "" }}> {this.considerGoogleDocsPush} - </div> + </div>} <div className="documentButtonBar-button" style={{ display: !considerPull ? "none" : "" }}> {this.considerGoogleDocsPull} - </div> */} + </div> + <div className="documentButtonBar-button"> + {this.menuButton} + </div> + {/* {Doc.UserDoc().noviceMode ? (null) : <div className="documentButtonBar-button"> + {this.moreButton} + </div>} */} </div>; } -} +}
\ No newline at end of file diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss index 1e8cfdff4..f9b8c1940 100644 --- a/src/client/views/DocumentDecorations.scss +++ b/src/client/views/DocumentDecorations.scss @@ -16,22 +16,12 @@ $linkGap : 3px; grid-template-columns: 8px 16px 1fr 8px 8px; pointer-events: none; - #documentDecorations-centerCont { + .documentDecorations-centerCont { grid-column: 3; background: none; } - .documentDecorations-resizer { - pointer-events: auto; - background: $alt-accent; - opacity: 0.1; - } - - .documentDecorations-resizer:hover { - opacity: 1; - } - - .documentDecorations-selector { + .documentDecorations-levelSelector { pointer-events: auto; height: 15px; width: 15px; @@ -49,27 +39,43 @@ $linkGap : 3px; grid-row: 4; } - #documentDecorations-topLeftResizer, - #documentDecorations-leftResizer, - #documentDecorations-bottomLeftResizer { + .documentDecorations-topLeftResizer, + .documentDecorations-topRightResizer, + .documentDecorations-bottomLeftResizer, + .documentDecorations-bottomRightResizer, + .documentDecorations-leftResizer, + .documentDecorations-topResizer, + .documentDecorations-bottomResizer, + .documentDecorations-rightResizer { + pointer-events: auto; + background: $alt-accent; + opacity: 0.1; + &:hover { + opacity: 1; + } + } + + .documentDecorations-topLeftResizer, + .documentDecorations-leftResizer, + .documentDecorations-bottomLeftResizer { grid-column: 1 } - #documentDecorations-topResizer, - #documentDecorations-bottomResizer { + .documentDecorations-topResizer, + .documentDecorations-bottomResizer { grid-column-start: 2; grid-column-end: 5; } - #documentDecorations-bottomRightResizer, - #documentDecorations-topRightResizer, - #documentDecorations-rightResizer { + .documentDecorations-bottomRightResizer, + .documentDecorations-topRightResizer, + .documentDecorations-rightResizer { grid-column-start: 5; grid-column-end: 7; } - #documentDecorations-rotation, - #documentDecorations-borderRadius { + .documentDecorations-rotation, + .documentDecorations-borderRadius { grid-column: 5; grid-row: 4; border-radius: 100%; @@ -79,6 +85,7 @@ $linkGap : 3px; top: 12; position: relative; pointer-events: all; + cursor: nwse-resize; .borderRadiusTooltip { width: 10px; @@ -86,69 +93,69 @@ $linkGap : 3px; position: absolute; } } - #documentDecorations-rotation { + .documentDecorations-rotation { background: transparent; right: -15; } - #documentDecorations-topLeftResizer, - #documentDecorations-bottomRightResizer { + .documentDecorations-topLeftResizer, + .documentDecorations-bottomRightResizer { cursor: nwse-resize; background: unset; opacity: 1; } - #documentDecorations-topLeftResizer { + .documentDecorations-topLeftResizer { border-left: 2px solid; border-top: solid 2px; } - #documentDecorations-bottomRightResizer { + .documentDecorations-bottomRightResizer { border-right: 2px solid; border-bottom: solid 2px; } - #documentDecorations-topLeftResizer:hover, - #documentDecorations-bottomRightResizer:hover { + .documentDecorations-topLeftResizer:hover, + .documentDecorations-bottomRightResizer:hover { opacity: 1; } - #documentDecorations-bottomRightResizer { + .documentDecorations-bottomRightResizer { grid-row: 4; } - #documentDecorations-topRightResizer, - #documentDecorations-bottomLeftResizer { + .documentDecorations-topRightResizer, + .documentDecorations-bottomLeftResizer { cursor: nesw-resize; background: unset; opacity: 1; } - #documentDecorations-topRightResizer { + .documentDecorations-topRightResizer { border-right: 2px solid; border-top: 2px solid; } - #documentDecorations-bottomLeftResizer { + .documentDecorations-bottomLeftResizer { border-left: 2px solid; border-bottom: 2px solid; } - #documentDecorations-topRightResizer:hover, - #documentDecorations-bottomLeftResizer:hover { + .documentDecorations-topRightResizer:hover, + .documentDecorations-bottomLeftResizer:hover { cursor: nesw-resize; background: dimGray; opacity: 1; } - #documentDecorations-topResizer, - #documentDecorations-bottomResizer { + .documentDecorations-topResizer, + .documentDecorations-bottomResizer { cursor: ns-resize; } - #documentDecorations-leftResizer, - #documentDecorations-rightResizer { + .documentDecorations-leftResizer, + .documentDecorations-rightResizer { cursor: ew-resize; } @@ -164,7 +171,7 @@ $linkGap : 3px; .documentDecorations-title { opacity: 1; - grid-column-start: 3; + grid-column-start: 2; grid-column-end: 4; pointer-events: auto; overflow: hidden; diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index e546ca858..066cfd95a 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -1,56 +1,31 @@ -import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; -import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote, faTextHeight, faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faCloudUploadAlt, faLink, faShare, faStopCircle, faSyncAlt, faTag, faTimes, faAngleLeft, faAngleRight, faAngleDoubleLeft, faAngleDoubleRight, faPause, faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons'; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, observable, reaction, runInAction, get } from "mobx"; +import { Tooltip } from '@material-ui/core'; +import { action, computed, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DataSym, Field, WidthSym, HeightSym, AclEdit, AclAdmin } from "../../fields/Doc"; +import { AclAdmin, AclEdit, DataSym, Doc, Field, WidthSym, HeightSym } from "../../fields/Doc"; import { Document } from '../../fields/documentSchemas'; +import { HtmlField } from '../../fields/HtmlField'; +import { InkField } from "../../fields/InkField"; import { ScriptField } from '../../fields/ScriptField'; -import { Cast, StrCast, NumCast } from "../../fields/Types"; -import { Utils, setupMoveUpEvents, emptyFunction, returnFalse, simulateMouseClick } from "../../Utils"; -import { DocUtils } from "../documents/Documents"; +import { Cast, NumCast } from "../../fields/Types"; +import { GetEffectiveAcl } from '../../fields/util'; +import { emptyFunction, returnFalse, setupMoveUpEvents, simulateMouseClick, returnVal } from "../../Utils"; +import { DocUtils, Docs } from "../documents/Documents"; import { DocumentType } from '../documents/DocumentTypes'; -import { DragManager } from "../util/DragManager"; +import { DragManager, dropActionType } from "../util/DragManager"; import { SelectionManager } from "../util/SelectionManager"; +import { SnappingManager } from '../util/SnappingManager'; import { undoBatch, UndoManager } from "../util/UndoManager"; +import { CollectionDockingView } from './collections/CollectionDockingView'; import { DocumentButtonBar } from './DocumentButtonBar'; import './DocumentDecorations.scss'; import { DocumentView } from "./nodes/DocumentView"; import React = require("react"); import e = require('express'); -import { CollectionDockingView } from './collections/CollectionDockingView'; -import { SnappingManager } from '../util/SnappingManager'; -import { HtmlField } from '../../fields/HtmlField'; -import { InkField } from "../../fields/InkField"; -import { Tooltip } from '@material-ui/core'; -import { GetEffectiveAcl } from '../../fields/util'; -import { DocumentIcon } from './nodes/DocumentIcon'; -import { render } from 'react-dom'; -import { createLessThan } from 'typescript'; -import FormatShapePane from './collections/collectionFreeForm/FormatShapePane'; -import { PropertiesView } from './collections/collectionFreeForm/PropertiesView'; - -library.add(faCaretUp); -library.add(faObjectGroup); -library.add(faStickyNote); -library.add(faFilePdf); -library.add(faFilm, faTextHeight); -library.add(faLink); -library.add(faTag); -library.add(faTimes); -library.add(faArrowAltCircleDown); -library.add(faArrowAltCircleUp); -library.add(faStopCircle); -library.add(faCheckCircle); -library.add(faCloudUploadAlt); -library.add(faSyncAlt); -library.add(faShare); -library.add(faAngleDoubleLeft); -library.add(faAngleDoubleRight); -library.add(faAngleLeft); -library.add(faAngleRight); -library.add(faPause); -library.add(faExternalLinkAlt); +import { CurrentUserUtils } from '../util/CurrentUserUtils'; +import { InkStrokeProperties } from './InkStrokeProperties'; +import { KeyManager } from './GlobalKeyHandler'; @observer export class DocumentDecorations extends React.Component<{}, { value: string }> { @@ -121,7 +96,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> this._titleControlString = this._accumulatedTitle; } else if (this._titleControlString.startsWith("#")) { const selectionTitleFieldKey = this._titleControlString.substring(1); - selectionTitleFieldKey === "title" && (SelectionManager.SelectedDocuments()[0].props.Document.customTitle = !this._accumulatedTitle.startsWith("-")); + selectionTitleFieldKey === "title" && (SelectionManager.SelectedDocuments()[0].dataDoc["title-custom"] = !this._accumulatedTitle.startsWith("-")); UndoManager.RunInBatch(() => selectionTitleFieldKey && SelectionManager.SelectedDocuments().forEach(d => { const value = typeof d.props.Document[selectionTitleFieldKey] === "number" ? +this._accumulatedTitle : this._accumulatedTitle; Doc.SetInPlace(d.props.Document, selectionTitleFieldKey, value, true); @@ -145,7 +120,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> } } @action onTitleDown = (e: React.PointerEvent): void => { - setupMoveUpEvents(this, e, this.onBackgroundMove, (e) => { }, this.onTitleClick); + setupMoveUpEvents(this, e, e => this.onBackgroundMove(true, e), (e) => { }, this.onTitleClick); } @action onTitleClick = (e: PointerEvent): void => { !this._edtingTitle && (this._accumulatedTitle = this._titleControlString.startsWith("#") ? this.selectionTitle : this._titleControlString); @@ -153,38 +128,27 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> setTimeout(() => this._keyinput.current!.focus(), 0); } - @action onSettingsDown = (e: React.PointerEvent): void => { - setupMoveUpEvents(this, e, () => false, (e) => { }, this.onSettingsClick); - } - @action onSettingsClick = (e: PointerEvent): void => { - if (e.button === 0 && !e.altKey && !e.ctrlKey) { - let child = SelectionManager.SelectedDocuments()[0].ContentDiv!.children[0]; - while (child.children.length) { - const next = Array.from(child.children).find(c => typeof (c.className) === "string"); - if (next?.className.includes("documentView-node")) break; - if (next) child = next; - else break; - } - simulateMouseClick(child, e.clientX, e.clientY + 30, e.screenX, e.screenY + 30); - } - } - onBackgroundDown = (e: React.PointerEvent): void => { - setupMoveUpEvents(this, e, this.onBackgroundMove, (e) => { }, (e) => { }); + setupMoveUpEvents(this, e, e => this.onBackgroundMove(false, e), (e) => { }, (e) => { }); } @action - onBackgroundMove = (e: PointerEvent, down: number[]): boolean => { + onBackgroundMove = (dragTitle: boolean, e: PointerEvent): boolean => { const dragDocView = SelectionManager.SelectedDocuments()[0]; const dragData = new DragManager.DocumentDragData(SelectionManager.SelectedDocuments().map(dv => dv.props.Document)); const [left, top] = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).inverse().transformPoint(0, 0); dragData.offset = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).transformDirection(e.x - left, e.y - top); - dragData.moveDocument = SelectionManager.SelectedDocuments()[0].props.moveDocument; + dragData.moveDocument = dragDocView.props.moveDocument; dragData.isSelectionMove = true; + dragData.canEmbed = dragTitle; + dragData.dropAction = dragDocView.props.dropAction; this.Interacting = true; this._hidden = true; DragManager.StartDocumentDrag(SelectionManager.SelectedDocuments().map(dv => dv.ContentDiv!), dragData, e.x, e.y, { - dragComplete: action(e => this._hidden = this.Interacting = false), + dragComplete: action(e => { + dragData.canEmbed && SelectionManager.DeselectAll(); + this._hidden = this.Interacting = false; + }), hideSource: true }); return true; @@ -212,8 +176,21 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> if (e.button === 0) { const selectedDocs = SelectionManager.SelectedDocuments(); if (selectedDocs.length) { - //CollectionDockingView.Instance?.OpenFullScreen(selectedDocs[0], selectedDocs[0].props.LibraryPath); - CollectionDockingView.AddRightSplit(Doc.MakeAlias(selectedDocs[0].props.Document), selectedDocs[0].props.LibraryPath); + if (e.ctrlKey) { // open an alias in a new tab with Ctrl Key + const alias = Doc.MakeAlias(selectedDocs[0].props.Document); + alias.context = undefined; + //CollectionDockingView.Instance?.OpenFullScreen(selectedDocs[0]); + CollectionDockingView.AddSplit(alias, "right"); + } else if (e.shiftKey) { // open centered in a new workspace with Shift Key + const alias = Doc.MakeAlias(selectedDocs[0].props.Document); + alias.context = undefined; + alias.x = -alias[WidthSym]() / 2; + alias.y = -alias[HeightSym]() / 2; + //CollectionDockingView.Instance?.OpenFullScreen(selectedDocs[0]); + CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([alias], { title: "Tab for " + alias.title }), "right"); + } else { // open same document in new tab + CollectionDockingView.ToggleSplit(selectedDocs[0].props.Document, "right"); + } } } SelectionManager.DeselectAll(); @@ -310,7 +287,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> const newY = Math.sin(angle) * (ink.X - this._centerPoints[index].X) + Math.cos(angle) * (ink.Y - this._centerPoints[index].Y) + this._centerPoints[index].Y; newPoints.push({ X: newX, Y: newY }); } - doc.data = new InkField(newPoints); + Doc.GetProto(doc).data = new InkField(newPoints); const xs = newPoints.map(p => p.X); const ys = newPoints.map(p => p.Y); const left = Math.min(...xs); @@ -349,7 +326,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> const doc = Document(element.rootDoc); if (doc.type === DocumentType.INK && doc.x && doc.y && doc._width && doc._height) { this._inkDocs.push({ x: doc.x, y: doc.y, width: doc._width, height: doc._height }); - if (FormatShapePane.Instance._lock) { + if (InkStrokeProperties.Instance?._lock) { doc._nativeHeight = doc._height; doc._nativeWidth = doc._width; } @@ -358,13 +335,13 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, (e) => { }); if (e.button === 0) { - this._resizeHdlId = e.currentTarget.id; + this._resizeHdlId = e.currentTarget.className; const bounds = e.currentTarget.getBoundingClientRect(); this._offX = this._resizeHdlId.toLowerCase().includes("left") ? bounds.right - e.clientX : bounds.left - e.clientX; this._offY = this._resizeHdlId.toLowerCase().includes("top") ? bounds.bottom - e.clientY : bounds.top - e.clientY; - this.Interacting = true; + this.Interacting = true; // turns off pointer events on things like youtube videos and web pages so that dragging doesn't get "stuck" when cursor moves over them this._resizeUndo = UndoManager.StartBatch("DocDecs resize"); - SelectionManager.SelectedDocuments()[0].props.setupDragLines?.(); + SelectionManager.SelectedDocuments()[0].props.setupDragLines?.(e.ctrlKey || e.shiftKey); } this._snapX = e.pageX; this._snapY = e.pageY; @@ -382,7 +359,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> var fixedAspect = first.layoutDoc._nativeWidth ? NumCast(first.layoutDoc._nativeWidth) / NumCast(first.layoutDoc._nativeHeight) : 0; SelectionManager.SelectedDocuments().forEach(action((element: DocumentView) => { const doc = Document(element.rootDoc); - if (doc.type === DocumentType.INK && doc._width && doc._height && FormatShapePane.Instance._lock) { + if (doc.type === DocumentType.INK && doc._width && doc._height && InkStrokeProperties.Instance?._lock) { fixedAspect = NumCast(doc._nativeWidth) / NumCast(doc._nativeHeight); } })); @@ -462,21 +439,21 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> break; } - SelectionManager.SelectedDocuments().forEach(action((element: DocumentView) => { - if (e.ctrlKey && !element.props.Document._nativeHeight) element.toggleNativeDimensions(); + SelectionManager.SelectedDocuments().forEach(action((docView: DocumentView) => { + if (e.ctrlKey && !docView.props.Document._nativeHeight) docView.toggleNativeDimensions(); if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) { - const doc = Document(element.rootDoc); - let nwidth = doc._nativeWidth || 0; - let nheight = doc._nativeHeight || 0; + const doc = Document(docView.rootDoc); + let nwidth = returnVal(docView.NativeWidth?.(), doc._nativeWidth); + let nheight = returnVal(docView.NativeHeight?.(), doc._nativeHeight); const width = (doc._width || 0); let height = (doc._height || (nheight / nwidth * width)); height = !height || isNaN(height) ? 20 : height; - const scale = element.props.ScreenToLocalTransform().Scale * element.props.ContentScaling(); + const scale = docView.props.ScreenToLocalTransform().Scale * docView.props.ContentScaling(); if (nwidth && nheight) { if (nwidth / nheight !== width / height) { height = nheight / nwidth * width; } - if (e.ctrlKey || (!dragBottom || !element.layoutDoc._fitWidth)) { // ctrl key enables modification of the nativeWidth or nativeHeight durin the interaction + if (e.ctrlKey || (!dragBottom || !docView.layoutDoc._fitWidth)) { // ctrl key enables modification of the nativeWidth or nativeHeight durin the interaction if (Math.abs(dW) > Math.abs(dH)) dH = dW * nheight / nwidth; else dW = dH * nwidth / nheight; } @@ -486,16 +463,16 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> doc.x = (doc.x || 0) + dX * (actualdW - width); doc.y = (doc.y || 0) + dY * (actualdH - height); const fixedAspect = (nwidth && nheight); - const fieldKey = Doc.LayoutFieldKey(doc); + const fieldKey = docView.LayoutFieldKey; if (fixedAspect && (!nwidth || !nheight)) { doc[DataSym][fieldKey + "-nativeWidth"] = doc._nativeWidth = nwidth = doc._width || 0; doc[DataSym][fieldKey + "-nativeHeight"] = doc._nativeHeight = nheight = doc._height || 0; } const anno = Cast(doc.annotationOn, Doc, null); - if (e.ctrlKey && anno) { + if (e.ctrlKey && (anno || doc.type === DocumentType.IMG)) { dW !== 0 && runInAction(() => { - const dataDoc = anno[DataSym]; - const annoFieldKey = Doc.LayoutFieldKey(anno); + const dataDoc = (anno ?? doc)[DataSym]; + const annoFieldKey = Doc.LayoutFieldKey(anno ?? doc); const nw = NumCast(dataDoc[annoFieldKey + "-nativeWidth"]); const nh = NumCast(dataDoc[annoFieldKey + "-nativeHeight"]); dataDoc[annoFieldKey + "-nativeWidth"] = nw + (dW > 0 ? 10 : -10); @@ -512,7 +489,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> else if (!fixedAspect || !e.ctrlKey) doc._height = actualdH; } else { - if (!fixedAspect || (dragBottom && (e.ctrlKey || element.layoutDoc._fitWidth))) { + if (!fixedAspect || (dragBottom && (e.ctrlKey || docView.layoutDoc._fitWidth))) { doc[DataSym][fieldKey + "-nativeHeight"] = doc._nativeHeight = actualdH / (doc._height || 1) * (doc._nativeHeight || 0); } doc._height = actualdH; @@ -558,7 +535,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> const newY = ((doc.y || 0) - this._inkDocs[index].y) + (i.Y * (doc._height || 0)) / this._inkDocs[index].height; newPoints.push({ X: newX, Y: newY }); }); - doc.data = new InkField(newPoints); + Doc.GetProto(doc).data = new InkField(newPoints); } doc._nativeWidth = 0; @@ -591,62 +568,45 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> } } public static DocumentIcon(layout: string) { - const button = layout.indexOf("PDFBox") !== -1 ? faFilePdf : - layout.indexOf("ImageBox") !== -1 ? faImage : - layout.indexOf("Formatted") !== -1 ? faStickyNote : - layout.indexOf("Video") !== -1 ? faFilm : - layout.indexOf("Collection") !== -1 ? faObjectGroup : - faCaretUp; + const button = layout.indexOf("PDFBox") !== -1 ? "file-pdf" : + layout.indexOf("ImageBox") !== -1 ? "image" : + layout.indexOf("Formatted") !== -1 ? "sticky-note" : + layout.indexOf("Video") !== -1 ? "film" : + layout.indexOf("Collection") !== -1 ? "object-group" : + "caret-up"; return <FontAwesomeIcon icon={button} className="documentView-minimizedIcon" />; } + render() { - const darkScheme = Cast(Doc.UserDoc().activeWorkspace, Doc, null)?.darkScheme ? "dimgray" : undefined; + const darkScheme = CurrentUserUtils.ActiveDashboard?.darkScheme ? "dimgray" : undefined; const bounds = this.Bounds; const seldoc = SelectionManager.SelectedDocuments().length ? SelectionManager.SelectedDocuments()[0] : undefined; - if (SnappingManager.GetIsDragging() || bounds.r - bounds.x < 2 || bounds.x === Number.MAX_VALUE || !seldoc || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) { + if (SnappingManager.GetIsDragging() || bounds.r - bounds.x < 1 || bounds.x === Number.MAX_VALUE || !seldoc || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) { return (null); } const canDelete = SelectionManager.SelectedDocuments().some(docView => { - const collectionAcl = GetEffectiveAcl(docView.props.ContainingCollectionDoc?.[DataSym]); - return collectionAcl === AclAdmin || collectionAcl === AclEdit; + const collectionAcl = docView.props.ContainingCollectionView ? GetEffectiveAcl(docView.props.ContainingCollectionDoc?.[DataSym]) : AclEdit; + const docAcl = GetEffectiveAcl(docView.props.Document); + return !docView.props.Document._stayInCollection && (collectionAcl === AclAdmin || collectionAcl === AclEdit || docAcl === AclAdmin); }); - const minimal = bounds.r - bounds.x < 100 ? true : false; - const maximizeIcon = minimal ? ( - <Tooltip title={<><div className="dash-tooltip">Show context menu</div></>} placement="top"> - <div className="documentDecorations-contextMenu" onPointerDown={this.onSettingsDown}> - <FontAwesomeIcon size="lg" icon="cog" /> - </div></Tooltip>) : canDelete ? ( - <Tooltip title={<><div className="dash-tooltip">Close</div></>} placement="top"> - <div className="documentDecorations-closeButton" onClick={this.onCloseClick}> - {/* Currently, this is set to be enabled if there is no ink selected. It might be interesting to think about minimizing ink if it's useful? -syip2*/} - <FontAwesomeIcon className="documentdecorations-times" icon={faTimes} size="lg" /> - </div></Tooltip>) : (null); + const canOpen = SelectionManager.SelectedDocuments().some(docView => !docView.props.Document._stayInCollection); + const closeIcon = !canDelete ? (null) : ( + <Tooltip title={<div className="dash-tooltip">Close</div>} placement="top"> + <div className="documentDecorations-closeButton" onClick={this.onCloseClick}> + <FontAwesomeIcon className="documentdecorations-times" icon={"times"} size="lg" /> + </div></Tooltip>); + + const openIcon = !canOpen ? (null) : <Tooltip title={<div className="dash-tooltip">Open in Tab (ctrl: as alias, shift: in new collection)</div>} placement="top"><div className="documentDecorations-openInTab" onContextMenu={e => { e.preventDefault(); e.stopPropagation(); }} onPointerDown={this.onMaximizeDown}> + {SelectionManager.SelectedDocuments().length === 1 ? <FontAwesomeIcon icon="external-link-alt" className="documentView-minimizedIcon" /> : "..."} + </div> + </Tooltip>; const titleArea = this._edtingTitle ? - <> - <input ref={this._keyinput} className="documentDecorations-title" type="text" name="dynbox" autoComplete="on" value={this._accumulatedTitle} - onBlur={e => this.titleBlur(true)} onChange={action(e => this._accumulatedTitle = e.target.value)} onKeyPress={this.titleEntered} /> - {minimal ? (null) : <div className="publishBox" // title="make document referenceable by its title" - // onPointerDown={action(e => { - // if (!seldoc.props.Document.customTitle) { - // seldoc.props.Document.customTitle = true; - // StrCast(Doc.GetProto(seldoc.props.Document).title).startsWith("-") && (Doc.GetProto(seldoc.props.Document).title = StrCast(seldoc.props.Document.title).substring(1)); - // this._accumulatedTitle = StrCast(seldoc.props.Document.title); - // } - // DocUtils.Publish(seldoc.props.Document, this._accumulatedTitle, seldoc.props.addDocument, seldoc.props.removeDocument); - // })} - > - {/* <FontAwesomeIcon size="lg" color={SelectionManager.SelectedDocuments()[0].props.Document.title === SelectionManager.SelectedDocuments()[0].props.Document[Id] ? "green" : undefined} icon="sticky-note"></FontAwesomeIcon> */} - </div>} - </> : - <> - {minimal ? (null) : <Tooltip title={<><div className="dash-tooltip">Show context menu</div></>} placement="top"><div className="documentDecorations-contextMenu" key="menu" onPointerDown={this.onSettingsDown}> - <FontAwesomeIcon size="lg" icon="cog" /> - </div></Tooltip>} - <div className="documentDecorations-title" key="title" onPointerDown={this.onTitleDown} > - <span style={{ width: "100%", display: "inline-block", cursor: "move" }}>{`${this.selectionTitle}`}</span> - </div> - </>; + <input ref={this._keyinput} className="documentDecorations-title" type="text" name="dynbox" autoComplete="on" value={this._accumulatedTitle} + onBlur={e => this.titleBlur(true)} onChange={action(e => this._accumulatedTitle = e.target.value)} onKeyPress={this.titleEntered} /> : + <div className="documentDecorations-title" style={{ gridColumnEnd: 5 }} key="title" onPointerDown={this.onTitleDown} > + <span style={{ width: "100%", display: "inline-block", cursor: "move" }}>{`${this.selectionTitle}`}</span> + </div>; bounds.x = Math.max(0, bounds.x - this._resizeBorderWidth / 2) + this._resizeBorderWidth / 2; bounds.y = Math.max(0, bounds.y - this._resizeBorderWidth / 2 - this._titleHeight) + this._resizeBorderWidth / 2 + this._titleHeight; @@ -667,7 +627,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> height: (bounds.b - bounds.y + this._resizeBorderWidth) + "px", left: bounds.x - this._resizeBorderWidth / 2, top: bounds.y - this._resizeBorderWidth / 2, - pointerEvents: this.Interacting ? "none" : "all", + pointerEvents: KeyManager.Instance.ShiftPressed || this.Interacting ? "none" : "all", zIndex: SelectionManager.SelectedDocuments().length > 1 ? 900 : 0, }} onPointerDown={this.onBackgroundDown} onContextMenu={e => { e.preventDefault(); e.stopPropagation(); }} > </div> @@ -678,46 +638,36 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> left: bounds.x - this._resizeBorderWidth / 2, top: bounds.y - this._resizeBorderWidth / 2 - this._titleHeight, }}> - {maximizeIcon} - {titleArea} - {SelectionManager.SelectedDocuments().length !== 1 || seldoc.Document.type === DocumentType.INK ? (null) : - <Tooltip title={<><div className="dash-tooltip">{`${seldoc.finalLayoutKey.includes("icon") ? "De" : ""}Iconify Document`}</div></>} placement="top"> + {closeIcon} + {Object.keys(SelectionManager.SelectedDocuments()[0].props).includes("treeViewDoc") ? (null) : titleArea} + {SelectionManager.SelectedDocuments().length !== 1 || seldoc.Document.type === DocumentType.INK || Object.keys(SelectionManager.SelectedDocuments()[0].props).includes("treeViewDoc") ? (null) : + <Tooltip title={<div className="dash-tooltip">{`${seldoc.finalLayoutKey.includes("icon") ? "De" : ""}Iconify Document`}</div>} placement="top"> <div className="documentDecorations-iconifyButton" onPointerDown={this.onIconifyDown}> <FontAwesomeIcon icon={seldoc.finalLayoutKey.includes("icon") ? "window-restore" : "window-minimize"} className="documentView-minimizedIcon" /> </div></Tooltip>} - <Tooltip title={<><div className="dash-tooltip">Open In a New Pane</div></>} placement="top"><div className="documentDecorations-openInTab" onPointerDown={this.onMaximizeDown}> - {SelectionManager.SelectedDocuments().length === 1 ? <FontAwesomeIcon icon="external-link-alt" className="documentView-minimizedIcon" /> : "..."} - </div></Tooltip> - <div id="documentDecorations-topLeftResizer" className="documentDecorations-resizer" - onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> - <div id="documentDecorations-topResizer" className="documentDecorations-resizer" - onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> - <div id="documentDecorations-topRightResizer" className="documentDecorations-resizer" - onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> - <div id="documentDecorations-leftResizer" className="documentDecorations-resizer" - onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> - <div id="documentDecorations-centerCont"></div> - <div id="documentDecorations-rightResizer" className="documentDecorations-resizer" - onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> - <div id="documentDecorations-bottomLeftResizer" className="documentDecorations-resizer" - onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> - <div id="documentDecorations-bottomResizer" className="documentDecorations-resizer" - onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> - <div id="documentDecorations-bottomRightResizer" className="documentDecorations-resizer" - onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> + {openIcon} + <div className="documentDecorations-topLeftResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> + <div className="documentDecorations-topResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> + <div className="documentDecorations-topRightResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> + <div className="documentDecorations-leftResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> + <div className="documentDecorations-centerCont"></div> + <div className="documentDecorations-rightResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> + <div className="documentDecorations-bottomLeftResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> + <div className="documentDecorations-bottomResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> + <div className="documentDecorations-bottomRightResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> {seldoc.props.renderDepth <= 1 || !seldoc.props.ContainingCollectionView ? (null) : - <Tooltip title={<><div className="dash-tooltip">tap to select containing document</div></>} placement="top"> - <div id="documentDecorations-levelSelector" className="documentDecorations-selector" + <Tooltip title={<div className="dash-tooltip">tap to select containing document</div>} placement="top"> + <div className="documentDecorations-levelSelector" onPointerDown={this.onSelectorUp} onContextMenu={e => e.preventDefault()}> - <FontAwesomeIcon className="documentdecorations-times" icon={faArrowAltCircleUp} size="lg" /> + <FontAwesomeIcon className="documentdecorations-times" icon={"arrow-alt-circle-up"} size="lg" /> </div></Tooltip>} - <div id={`documentDecorations-${useRotation ? "rotation" : "borderRadius"}`} + <div className={`documentDecorations-${useRotation ? "rotation" : "borderRadius"}`} onPointerDown={useRotation ? this.onRotateDown : this.onRadiusDown} onContextMenu={(e) => e.preventDefault()}>{useRotation && "⟲"}</div> </div > - <div className="link-button-container" key="links" style={{ left: bounds.x - this._resizeBorderWidth / 2 + 10, top: bounds.b + this._resizeBorderWidth / 2 }}> + {seldoc?.Document.type === DocumentType.FONTICON ? (null) : <div className="link-button-container" key="links" style={{ left: bounds.x - this._resizeBorderWidth / 2 + 10, top: bounds.b + this._resizeBorderWidth / 2 }}> <DocumentButtonBar views={SelectionManager.SelectedDocuments} /> - </div> + </div>} </>} </div > ); diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index ec3e754fb..d35271ffd 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -4,10 +4,8 @@ import { observer } from 'mobx-react'; import * as Autosuggest from 'react-autosuggest'; import { ObjectField } from '../../fields/ObjectField'; import { SchemaHeaderField } from '../../fields/SchemaHeaderField'; -import "./EditableView.scss"; import { DragManager } from '../util/DragManager'; -import { ComputedField } from '../../fields/ScriptField'; -import { FieldValue } from '../../fields/Types'; +import "./EditableView.scss"; export interface EditableProps { /** @@ -20,11 +18,12 @@ export interface EditableProps { * @param value - The string entered by the user to set the value to * @returns `true` if setting the value was successful, `false` otherwise * */ - SetValue(value: string, shiftDown?: boolean): boolean; + SetValue(value: string, shiftDown?: boolean, enterKey?: boolean): boolean; OnFillDown?(value: string): void; OnTab?(shift?: boolean): void; + OnEmpty?(): void; /** * The contents to render when not editing @@ -54,10 +53,6 @@ export interface EditableProps { color?: string | undefined; onDrop?: any; placeholder?: string; - highlight?: boolean; - positions?: number[]; - search?: string; - bing?: () => string | undefined; } /** @@ -96,25 +91,34 @@ export class EditableView extends React.Component<EditableProps> { @action onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { - if (e.key === "Tab") { - e.stopPropagation(); - this.finalizeEdit(e.currentTarget.value, e.shiftKey, false); - this.props.OnTab && this.props.OnTab(e.shiftKey); - } else if (e.key === "Enter") { - e.stopPropagation(); - if (!e.ctrlKey) { - this.finalizeEdit(e.currentTarget.value, e.shiftKey, false); - } else if (this.props.OnFillDown) { - this.props.OnFillDown(e.currentTarget.value); + switch (e.key) { + case "Tab": + e.stopPropagation(); + this.finalizeEdit(e.currentTarget.value, e.shiftKey, false, false); + this.props.OnTab?.(e.shiftKey); + break; + case "Backspace": + e.stopPropagation(); + if (!e.currentTarget.value) this.props.OnEmpty?.(); + break; + case "Enter": + e.stopPropagation(); + if (!e.ctrlKey) { + this.finalizeEdit(e.currentTarget.value, e.shiftKey, false, true); + } else if (this.props.OnFillDown) { + this.props.OnFillDown(e.currentTarget.value); + this._editing = false; + this.props.isEditingCallback?.(false); + } + break; + case "Escape": + e.stopPropagation(); this._editing = false; this.props.isEditingCallback?.(false); - } - } else if (e.key === "Escape") { - e.stopPropagation(); - this._editing = false; - this.props.isEditingCallback?.(false); - } else if (e.key === ":") { - this.props.menuCallback?.(e.currentTarget.getBoundingClientRect().x, e.currentTarget.getBoundingClientRect().y); + break; + case ":": + this.props.menuCallback?.(e.currentTarget.getBoundingClientRect().x, e.currentTarget.getBoundingClientRect().y); + break; } } @@ -123,20 +127,18 @@ export class EditableView extends React.Component<EditableProps> { e.nativeEvent.stopPropagation(); if (this._ref.current && this.props.showMenuOnLoad) { this.props.menuCallback?.(this._ref.current.getBoundingClientRect().x, this._ref.current.getBoundingClientRect().y); - } else { - if (!this.props.onClick?.(e)) { - this._editing = true; - this.props.isEditingCallback?.(true); - } + } else if (!this.props.onClick?.(e)) { + this._editing = true; + this.props.isEditingCallback?.(true); } e.stopPropagation(); } @action - private finalizeEdit(value: string, shiftDown: boolean, lostFocus: boolean) { - if (this.props.SetValue(value, shiftDown)) { + private finalizeEdit(value: string, shiftDown: boolean, lostFocus: boolean, enterKey: boolean) { + if (this.props.SetValue(value, shiftDown, enterKey)) { this._editing = false; - this.props.isEditingCallback?.(false); + this.props.isEditingCallback?.(false,); } else { this._editing = false; this.props.isEditingCallback?.(false); @@ -154,7 +156,7 @@ export class EditableView extends React.Component<EditableProps> { @action setIsFocused = (value: boolean) => { const wasFocused = this._editing; - //this._editing = value; + this._editing = value; return wasFocused !== this._editing; } @@ -167,10 +169,11 @@ export class EditableView extends React.Component<EditableProps> { className: "editableView-input", onKeyDown: this.onKeyDown, autoFocus: true, - onBlur: e => this.finalizeEdit(e.currentTarget.value, false, true), + onBlur: e => this.finalizeEdit(e.currentTarget.value, false, true, false), onPointerDown: this.stopPropagation, onClick: this.stopPropagation, onPointerUp: this.stopPropagation, + onKeyPress: this.stopPropagation, value: this.props.autosuggestProps.value, onChange: this.props.autosuggestProps.onChange }} @@ -179,58 +182,33 @@ export class EditableView extends React.Component<EditableProps> { defaultValue={this.props.GetValue()} onKeyDown={this.onKeyDown} autoFocus={true} - onBlur={e => this.finalizeEdit(e.currentTarget.value, false, true)} + onKeyPress={e => e.stopPropagation()} + onBlur={e => this.finalizeEdit(e.currentTarget.value, false, true, false)} onPointerDown={this.stopPropagation} onClick={this.stopPropagation} onPointerUp={this.stopPropagation} style={{ display: this.props.display, fontSize: this.props.fontSize, minWidth: 20 }} placeholder={this.props.placeholder} />; } - returnHighlights() { - const results = []; - const contents = this.props.bing!(); - - if (contents !== undefined) { - if (this.props.positions !== undefined) { - const positions = this.props.positions; - const length = this.props.search!.length; - - // contents = String(this.props.contents.valueOf()); - - results.push(<span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize, color: this.props.contents ? "black" : "grey" }}>{contents ? contents.slice(0, this.props.positions[0]) : this.props.placeholder?.valueOf()}</span>); - positions.forEach((num, cur) => { - results.push(<span style={{ backgroundColor: "#FFFF00", fontStyle: this.props.fontStyle, fontSize: this.props.fontSize, color: this.props.contents ? "black" : "grey" }}>{contents ? contents.slice(num, num + length) : this.props.placeholder?.valueOf()}</span>); - let end = 0; - cur === positions.length - 1 ? end = contents.length : end = positions[cur + 1]; - results.push(<span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize, color: this.props.contents ? "black" : "grey" }}>{contents ? contents.slice(num + length, end) : this.props.placeholder?.valueOf()}</span>); - } - ); - } - return results; - } - else { - return <span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize, color: this.props.contents ? "black" : "grey" }}>{this.props.contents ? this.props.contents?.valueOf() : this.props.placeholder?.valueOf()}</span>; - } - } - render() { if (this._editing && this.props.GetValue() !== undefined) { return this.props.sizeToContent ? <div style={{ display: "grid", minWidth: 100 }}> - <div style={{ display: "inline-block", position: "relative", height: 0, width: "100%", overflow: "hidden" }}>{this.props.GetValue()}</div> + <div style={{ display: "inline-block", position: "relative", height: 0, width: "100%", overflow: "hidden" }}> + {this.props.GetValue()} + </div> {this.renderEditor()} - </div> : this.renderEditor(); - } else { - setTimeout(() => this.props.autosuggestProps?.resetValue(), 0); - return (this.props.contents instanceof ObjectField ? (null) : - <div className={`editableView-container-editing${this.props.oneLine ? "-oneLine" : ""}`} - ref={this._ref} - style={{ display: this.props.display, minHeight: "17px", whiteSpace: "nowrap", height: `${this.props.height ? this.props.height : "auto"}`, maxHeight: `${this.props.maxHeight}` }} - onClick={this.onClick} placeholder={this.props.placeholder}> - {this.props.highlight === undefined || this.props.positions === undefined || this.props.bing === undefined ? <span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize, color: this.props.contents ? "black" : "grey" }}>{this.props.contents ? this.props.contents?.valueOf() : this.props.placeholder?.valueOf()}</span> - : this.returnHighlights()} - </div> - ); + </div> : + this.renderEditor(); } + setTimeout(() => this.props.autosuggestProps?.resetValue(), 0); + return this.props.contents instanceof ObjectField ? (null) : + <div className={`editableView-container-editing${this.props.oneLine ? "-oneLine" : ""}`} ref={this._ref} + style={{ display: this.props.display, minHeight: "17px", whiteSpace: "nowrap", height: this.props.height || "auto", maxHeight: this.props.maxHeight }} + onClick={this.onClick} placeholder={this.props.placeholder}> + <span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize }}>{ + this.props.contents ? this.props.contents?.valueOf() : this.props.placeholder?.valueOf()} + </span> + </div>; } }
\ No newline at end of file diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index 76e786257..18743e850 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -7,7 +7,7 @@ import { Cast, FieldValue, NumCast } from "../../fields/Types"; import MobileInkOverlay from "../../mobile/MobileInkOverlay"; import { GestureUtils } from "../../pen-gestures/GestureUtils"; import { MobileInkOverlayContent } from "../../server/Message"; -import { emptyFunction, emptyPath, returnEmptyString, returnFalse, returnOne, returnTrue, returnZero, returnEmptyFilter, setupMoveUpEvents } from "../../Utils"; +import { emptyFunction, emptyPath, returnEmptyString, returnFalse, returnOne, returnTrue, returnZero, returnEmptyFilter, setupMoveUpEvents, returnEmptyDoclist } from "../../Utils"; import { CognitiveServices } from "../cognitive_services/CognitiveServices"; import { DocUtils } from "../documents/Documents"; import { CurrentUserUtils } from "../util/CurrentUserUtils"; @@ -26,7 +26,7 @@ import * as fitCurve from 'fit-curve'; import { CollectionFreeFormViewChrome } from "./collections/CollectionMenu"; @observer -export default class GestureOverlay extends Touchable { +export class GestureOverlay extends Touchable { static Instance: GestureOverlay; @observable public InkShape: string = ""; @@ -613,10 +613,10 @@ export default class GestureOverlay extends Touchable { } } // if we're not drawing in a toolglass try to recognize as gesture - else { + else { // need to decide when to turn gestures back on const result = points.length > 2 && GestureUtils.GestureRecognizer.Recognize(new Array(points)); let actionPerformed = false; - if (result && result.Score > 0.7) { + if (Doc.UserDoc().recognizeGestures && result && result.Score > 0.7) { switch (result.Name) { case GestureUtils.Gestures.Box: actionPerformed = this.dispatchGesture(GestureUtils.Gestures.Box); break; case GestureUtils.Gestures.StartBracket: actionPerformed = this.dispatchGesture(GestureUtils.Gestures.StartBracket); break; @@ -695,7 +695,7 @@ export default class GestureOverlay extends Touchable { left = this._points[0].X; bottom = this._points[this._points.length - 2].Y; top = this._points[0].Y; - if (shape !== "arrow" && shape !== "line") { + if (shape !== "arrow" && shape !== "line" && shape !== "circle") { if (left > right) { const temp = right; right = left; @@ -754,21 +754,38 @@ export default class GestureOverlay extends Touchable { break; case "circle": - const centerX = (right + left) / 2; - const centerY = (bottom + top) / 2; - const radius = bottom - centerY; - for (var y = top; y < bottom; y++) { - const x = Math.sqrt(Math.pow(radius, 2) - (Math.pow((y - centerY), 2))) + centerX; - this._points.push({ X: x, Y: y }); - } - for (var y = bottom; y > top; y--) { - const x = Math.sqrt(Math.pow(radius, 2) - (Math.pow((y - centerY), 2))) + centerX; - const newX = centerX - (x - centerX); - this._points.push({ X: newX, Y: y }); - } - this._points.push({ X: Math.sqrt(Math.pow(radius, 2) - (Math.pow((top - centerY), 2))) + centerX, Y: top }); - this._points.push({ X: Math.sqrt(Math.pow(radius, 2) - (Math.pow((top - centerY), 2))) + centerX, Y: top - 1 }); + const centerX = (Math.max(left, right) + Math.min(left, right)) / 2; + const centerY = (Math.max(top, bottom) + Math.min(top, bottom)) / 2; + const radius = Math.max(centerX - Math.min(left, right), centerY - Math.min(top, bottom)); + if (centerX - Math.min(left, right) < centerY - Math.min(top, bottom)) { + for (var y = Math.min(top, bottom); y < Math.max(top, bottom); y++) { + const x = Math.sqrt(Math.pow(radius, 2) - (Math.pow((y - centerY), 2))) + centerX; + this._points.push({ X: x, Y: y }); + } + for (var y = Math.max(top, bottom); y > Math.min(top, bottom); y--) { + const x = Math.sqrt(Math.pow(radius, 2) - (Math.pow((y - centerY), 2))) + centerX; + const newX = centerX - (x - centerX); + this._points.push({ X: newX, Y: y }); + } + this._points.push({ X: Math.sqrt(Math.pow(radius, 2) - (Math.pow((Math.min(top, bottom) - centerY), 2))) + centerX, Y: Math.min(top, bottom) }); + this._points.push({ X: Math.sqrt(Math.pow(radius, 2) - (Math.pow((Math.min(top, bottom) - centerY), 2))) + centerX, Y: Math.min(top, bottom) - 1 }); + + + } else { + for (var x = Math.min(left, right); x < Math.max(left, right); x++) { + const y = Math.sqrt(Math.pow(radius, 2) - (Math.pow((x - centerX), 2))) + centerY; + this._points.push({ X: x, Y: y }); + } + for (var x = Math.max(left, right); x > Math.min(left, right); x--) { + const y = Math.sqrt(Math.pow(radius, 2) - (Math.pow((x - centerX), 2))) + centerY; + const newY = centerY - (y - centerY); + this._points.push({ X: x, Y: newY }); + } + this._points.push({ X: Math.min(left, right), Y: Math.sqrt(Math.pow(radius, 2) - (Math.pow((Math.min(left, right) - centerX), 2))) + centerY }); + this._points.push({ X: Math.min(left, right), Y: Math.sqrt(Math.pow(radius, 2) - (Math.pow((Math.min(left, right) - centerX), 2))) + centerY - 1 }); + + } break; case "line": if (Math.abs(firstx - lastx) < 20) { @@ -882,15 +899,15 @@ export default class GestureOverlay extends Touchable { ContentScaling={returnOne} PanelWidth={this.return300} PanelHeight={this.return300} - NativeHeight={returnZero} - NativeWidth={returnZero} renderDepth={0} backgroundColor={returnEmptyString} focus={emptyFunction} parentActive={returnTrue} whenActiveChanged={emptyFunction} bringToFront={emptyFunction} + docRangeFilters={returnEmptyFilter} docFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} />; diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index be6aa6be2..1b2395423 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -1,4 +1,4 @@ -import { action } from "mobx"; +import { action, observable } from "mobx"; import { DateField } from "../../fields/DateField"; import { Doc, DocListCast } from "../../fields/Doc"; import { Id } from "../../fields/FieldSymbols"; @@ -6,23 +6,26 @@ import { InkTool } from "../../fields/InkField"; import { List } from "../../fields/List"; import { ScriptField } from "../../fields/ScriptField"; import { Cast, PromiseValue } from "../../fields/Types"; -import GoogleAuthenticationManager from "../apis/GoogleAuthenticationManager"; +import { GoogleAuthenticationManager } from "../apis/GoogleAuthenticationManager"; import { DocServer } from "../DocServer"; import { DocumentType } from "../documents/DocumentTypes"; import { DictationManager } from "../util/DictationManager"; import { DragManager } from "../util/DragManager"; +import { GroupManager } from "../util/GroupManager"; import { SelectionManager } from "../util/SelectionManager"; -import SharingManager from "../util/SharingManager"; +import { SharingManager } from "../util/SharingManager"; import { undoBatch, UndoManager } from "../util/UndoManager"; import { CollectionDockingView } from "./collections/CollectionDockingView"; +import { CollectionFreeFormViewChrome } from "./collections/CollectionMenu"; +import { ContextMenu } from "./ContextMenu"; import { DocumentDecorations } from "./DocumentDecorations"; +import { InkStrokeProperties } from "./InkStrokeProperties"; import { MainView } from "./MainView"; -import { DocumentView } from "./nodes/DocumentView"; import { DocumentLinksButton } from "./nodes/DocumentLinksButton"; -import PDFMenu from "./pdf/PDFMenu"; -import { ContextMenu } from "./ContextMenu"; -import GroupManager from "../util/GroupManager"; -import { CollectionFreeFormViewChrome } from "./collections/CollectionMenu"; +import { DocumentView } from "./nodes/DocumentView"; +import { PDFMenu } from "./pdf/PDFMenu"; +import { SnappingManager } from "../util/SnappingManager"; +import { SearchBox } from "./search/SearchBox"; const modifiers = ["control", "meta", "shift", "alt"]; type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo | Promise<KeyControlInfo>; @@ -31,9 +34,10 @@ type KeyControlInfo = { stopPropagation: boolean }; -export default class KeyManager { +export class KeyManager { public static Instance: KeyManager = new KeyManager(); private router = new Map<string, KeyHandler>(); + @observable ShiftPressed = false; constructor() { const isMac = navigator.platform.toLowerCase().indexOf("mac") >= 0; @@ -46,7 +50,12 @@ export default class KeyManager { this.router.set("1000", this.shift); } - public handle = async (e: KeyboardEvent) => { + public unhandle = action((e: KeyboardEvent) => { + if (e.key.toLowerCase() === "shift") KeyManager.Instance.ShiftPressed = false; + }); + + public handle = action(async (e: KeyboardEvent) => { + if (e.key.toLowerCase() === "shift") KeyManager.Instance.ShiftPressed = true; const keyname = e.key && e.key.toLowerCase(); this.handleGreedy(keyname); @@ -66,7 +75,7 @@ export default class KeyManager { control.stopPropagation && e.stopPropagation(); control.preventDefault && e.preventDefault(); - } + }); private handleGreedy = action((keyname: string) => { switch (keyname) { @@ -81,26 +90,18 @@ export default class KeyManager { // MarqueeView.DragMarquee = !MarqueeView.DragMarquee; // bcz: this needs a better disclosure UI break; case "escape": - // if (DocumentLinksButton.StartLink) { - // if (DocumentLinksButton.StartLink.Document) { - // action((e: React.PointerEvent<HTMLDivElement>) => { - // Doc.UnBrushDoc(DocumentLinksButton.StartLink?.Document as Doc); - // }); - // } - // } DocumentLinksButton.StartLink = undefined; + DocumentLinksButton.StartLinkView = undefined; + InkStrokeProperties.Instance && (InkStrokeProperties.Instance._controlBtn = false); - const main = MainView.Instance; Doc.SetSelectedTool(InkTool.None); var doDeselect = true; - if (main.isPointerDown) { + if (SnappingManager.GetIsDragging()) { DragManager.AbortDrag(); + } else if (CollectionDockingView.Instance.HasFullScreen) { + CollectionDockingView.Instance.CloseFullScreen(); } else { - if (CollectionDockingView.Instance.HasFullScreen()) { - CollectionDockingView.Instance.CloseFullScreen(); - } else { - doDeselect = !ContextMenu.Instance.closeMenu(); - } + doDeselect = !ContextMenu.Instance.closeMenu(); } doDeselect && SelectionManager.DeselectAll(); DictationManager.Controls.stop(); @@ -113,30 +114,18 @@ export default class KeyManager { break; case "delete": case "backspace": - if (document.activeElement) { - if (document.activeElement.tagName === "INPUT" || document.activeElement.tagName === "TEXTAREA") { - return { stopPropagation: false, preventDefault: false }; - } + if (document.activeElement?.tagName === "INPUT" || document.activeElement?.tagName === "TEXTAREA") { + return { stopPropagation: false, preventDefault: false }; } const selected = SelectionManager.SelectedDocuments().slice(); - UndoManager.RunInBatch(() => { - selected.map(dv => dv.props.removeDocument?.(dv.props.Document)); - }, "delete"); + UndoManager.RunInBatch(() => selected.map(dv => !dv.props.Document._stayInCollection && dv.props.removeDocument?.(dv.props.Document)), "delete"); SelectionManager.DeselectAll(); break; - case "arrowleft": - UndoManager.RunInBatch(() => SelectionManager.SelectedDocuments().map(dv => dv.props.nudge?.(-1, 0)), "nudge left"); - break; - case "arrowright": - UndoManager.RunInBatch(() => SelectionManager.SelectedDocuments().map(dv => dv.props.nudge?.(1, 0)), "nudge right"); - break; - case "arrowup": - UndoManager.RunInBatch(() => SelectionManager.SelectedDocuments().map(dv => dv.props.nudge?.(0, -1)), "nudge up"); - break; - case "arrowdown": - UndoManager.RunInBatch(() => SelectionManager.SelectedDocuments().map(dv => dv.props.nudge?.(0, 1)), "nudge down"); - break; + case "arrowleft": UndoManager.RunInBatch(() => SelectionManager.SelectedDocuments().map(dv => dv.props.nudge?.(-1, 0)), "nudge left"); break; + case "arrowright": UndoManager.RunInBatch(() => SelectionManager.SelectedDocuments().map(dv => dv.props.nudge?.(1, 0)), "nudge right"); break; + case "arrowup": UndoManager.RunInBatch(() => SelectionManager.SelectedDocuments().map(dv => dv.props.nudge?.(0, -1)), "nudge up"); break; + case "arrowdown": UndoManager.RunInBatch(() => SelectionManager.SelectedDocuments().map(dv => dv.props.nudge?.(0, 1)), "nudge down"); break; } return { @@ -145,34 +134,22 @@ export default class KeyManager { }; }); - private shift = async (keyname: string) => { + private shift = action(async (keyname: string) => { const stopPropagation = false; const preventDefault = false; switch (keyname) { - // case "~": - // DictationManager.Controls.listen({ useOverlay: true, tryExecute: true }); - // stopPropagation = true; - // preventDefault = true; - case "arrowleft": - UndoManager.RunInBatch(() => SelectionManager.SelectedDocuments().map(dv => dv.props.nudge?.(-10, 0)), "nudge left"); - break; - case "arrowright": - UndoManager.RunInBatch(() => SelectionManager.SelectedDocuments().map(dv => dv.props.nudge?.(10, 0)), "nudge right"); - break; - case "arrowup": - UndoManager.RunInBatch(() => SelectionManager.SelectedDocuments().map(dv => dv.props.nudge?.(0, -10)), "nudge up"); - break; - case "arrowdown": - UndoManager.RunInBatch(() => SelectionManager.SelectedDocuments().map(dv => dv.props.nudge?.(0, 10)), "nudge down"); - break; + case "arrowleft": UndoManager.RunInBatch(() => SelectionManager.SelectedDocuments().map(dv => dv.props.nudge?.(-10, 0)), "nudge left"); break; + case "arrowright": UndoManager.RunInBatch(() => SelectionManager.SelectedDocuments().map(dv => dv.props.nudge?.(10, 0)), "nudge right"); break; + case "arrowup": UndoManager.RunInBatch(() => SelectionManager.SelectedDocuments().map(dv => dv.props.nudge?.(0, -10)), "nudge up"); break; + case "arrowdown": UndoManager.RunInBatch(() => SelectionManager.SelectedDocuments().map(dv => dv.props.nudge?.(0, 10)), "nudge down"); break; } return { stopPropagation: stopPropagation, preventDefault: preventDefault }; - } + }); private alt = action((keyname: string) => { const stopPropagation = true; @@ -204,64 +181,43 @@ export default class KeyManager { switch (keyname) { case "arrowright": - if (document.activeElement) { - if (document.activeElement.tagName === "INPUT" || document.activeElement.tagName === "TEXTAREA") { - return { stopPropagation: false, preventDefault: false }; - } + if (document.activeElement?.tagName === "INPUT" || document.activeElement?.tagName === "TEXTAREA") { + return { stopPropagation: false, preventDefault: false }; } - MainView.Instance.mainFreeform && CollectionDockingView.AddRightSplit(MainView.Instance.mainFreeform); + MainView.Instance.mainFreeform && CollectionDockingView.AddSplit(MainView.Instance.mainFreeform, "right"); break; case "arrowleft": - if (document.activeElement) { - if (document.activeElement.tagName === "INPUT" || document.activeElement.tagName === "TEXTAREA") { - return { stopPropagation: false, preventDefault: false }; - } + if (document.activeElement?.tagName === "INPUT" || document.activeElement?.tagName === "TEXTAREA") { + return { stopPropagation: false, preventDefault: false }; } - MainView.Instance.mainFreeform && CollectionDockingView.CloseRightSplit(MainView.Instance.mainFreeform); + MainView.Instance.mainFreeform && CollectionDockingView.CloseSplit(MainView.Instance.mainFreeform); break; case "backspace": - if (document.activeElement) { - if (document.activeElement.tagName === "INPUT" || document.activeElement.tagName === "TEXTAREA") { - return { stopPropagation: false, preventDefault: false }; - } + if (document.activeElement?.tagName === "INPUT" || document.activeElement?.tagName === "TEXTAREA") { + return { stopPropagation: false, preventDefault: false }; } break; case "t": PromiseValue(Cast(Doc.UserDoc()["tabs-button-tools"], Doc)).then(pv => pv && (pv.onClick as ScriptField).script.run({ this: pv })); - if (MainView.Instance.flyoutWidth === 240) { - MainView.Instance.flyoutWidth = 0; - } else { - MainView.Instance.flyoutWidth = 240; - } - break; - case "l": - PromiseValue(Cast(Doc.UserDoc()["tabs-button-library"], Doc)).then(pv => pv && (pv.onClick as ScriptField).script.run({ this: pv })); - if (MainView.Instance.flyoutWidth === 250) { - MainView.Instance.flyoutWidth = 0; - } else { - MainView.Instance.flyoutWidth = 250; - } break; case "f": - PromiseValue(Cast(Doc.UserDoc()["tabs-button-search"], Doc)).then(pv => pv && (pv.onClick as ScriptField).script.run({ this: pv })); - if (MainView.Instance.flyoutWidth === 400) { - MainView.Instance.flyoutWidth = 0; - } else { - MainView.Instance.flyoutWidth = 400; - } + SearchBox.Instance._searchFullDB = "My Stuff"; + SearchBox.Instance.enter(undefined); break; case "o": const target = SelectionManager.SelectedDocuments()[0]; - target && CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(target); + target && CollectionDockingView.OpenFullScreen(target.props.Document); break; case "r": preventDefault = false; break; case "y": + SelectionManager.DeselectAll(); UndoManager.Redo(); stopPropagation = false; break; case "z": + SelectionManager.DeselectAll(); UndoManager.Undo(); stopPropagation = false; break; @@ -309,7 +265,7 @@ export default class KeyManager { let count = 1; const list: Doc[] = []; const targetDataDoc = Doc.GetProto(first.props.Document); - const fieldKey = Doc.LayoutFieldKey(first.props.Document); + const fieldKey = first.LayoutFieldKey; const docList = DocListCast(targetDataDoc[fieldKey]); docids.map((did, i) => i && DocServer.GetRefField(did).then(async doc => { count++; @@ -323,8 +279,6 @@ export default class KeyManager { undoBatch(() => { targetDataDoc[fieldKey] = new List<Doc>([...docList, ...added]); targetDataDoc[fieldKey + "-lastModified"] = new DateField(new Date(Date.now())); - const lastModified = "lastModified"; - targetDataDoc[lastModified] = new DateField(new Date(Date.now())); })(); } } diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts new file mode 100644 index 000000000..ad5c70fb1 --- /dev/null +++ b/src/client/views/InkStrokeProperties.ts @@ -0,0 +1,268 @@ +import { action, computed, observable } from "mobx"; +import { ColorState } from 'react-color'; +import { Doc, Field, Opt } from "../../fields/Doc"; +import { Document } from "../../fields/documentSchemas"; +import { InkField } from "../../fields/InkField"; +import { Cast, NumCast } from "../../fields/Types"; +import { DocumentType } from "../documents/DocumentTypes"; +import { SelectionManager } from "../util/SelectionManager"; +import { undoBatch } from "../util/UndoManager"; + +export class InkStrokeProperties { + static Instance: InkStrokeProperties | undefined; + + private _lastFill = "#D0021B"; + private _lastLine = "#D0021B"; + private _lastDash = "2"; + + @observable _lock = false; + @observable _controlBtn = false; + @observable _currPoint = -1; + + getField(key: string) { + return this.selectedInk?.reduce((p, i) => + (p === undefined || (p && p === i.rootDoc[key])) && i.rootDoc[key] !== "0" ? Field.toString(i.rootDoc[key] as Field) : "", undefined as Opt<string>); + } + + @computed get selectedInk() { + const inks = SelectionManager.SelectedDocuments().filter(i => Document(i.rootDoc).type === DocumentType.INK); + return inks.length ? inks : undefined; + } + @computed get unFilled() { return this.selectedInk?.reduce((p, i) => p && !i.rootDoc.fillColor ? true : false, true) || false; } + @computed get unStrokd() { return this.selectedInk?.reduce((p, i) => p && !i.rootDoc.color ? true : false, true) || false; } + @computed get solidFil() { return this.selectedInk?.reduce((p, i) => p && i.rootDoc.fillColor ? true : false, true) || false; } + @computed get solidStk() { return this.selectedInk?.reduce((p, i) => p && i.rootDoc.color && (!i.rootDoc.strokeDash || i.rootDoc.strokeDash === "0") ? true : false, true) || false; } + @computed get dashdStk() { return !this.unStrokd && this.getField("strokeDash") || ""; } + @computed get colorFil() { const ccol = this.getField("fillColor") || ""; ccol && (this._lastFill = ccol); return ccol; } + @computed get colorStk() { const ccol = this.getField("color") || ""; ccol && (this._lastLine = ccol); return ccol; } + @computed get widthStk() { return this.getField("strokeWidth") || "1"; } + @computed get markHead() { return this.getField("strokeStartMarker") || ""; } + @computed get markTail() { return this.getField("strokeEndMarker") || ""; } + @computed get shapeHgt() { return this.getField("_height"); } + @computed get shapeWid() { return this.getField("_width"); } + @computed get shapeXps() { return this.getField("x"); } + @computed get shapeYps() { return this.getField("y"); } + @computed get shapeRot() { return this.getField("rotation"); } + set unFilled(value) { this.colorFil = value ? "" : this._lastFill; } + set solidFil(value) { this.unFilled = !value; } + set colorFil(value) { value && (this._lastFill = value); this.selectedInk?.forEach(i => i.rootDoc.fillColor = value ? value : undefined); } + set colorStk(value) { value && (this._lastLine = value); this.selectedInk?.forEach(i => i.rootDoc.color = value ? value : undefined); } + set markHead(value) { this.selectedInk?.forEach(i => i.rootDoc.strokeStartMarker = value); } + set markTail(value) { this.selectedInk?.forEach(i => i.rootDoc.strokeEndMarker = value); } + set unStrokd(value) { this.colorStk = value ? "" : this._lastLine; } + set solidStk(value) { this.dashdStk = ""; this.unStrokd = !value; } + set dashdStk(value) { + value && (this._lastDash = value) && (this.unStrokd = false); + this.selectedInk?.forEach(i => i.rootDoc.strokeDash = value ? this._lastDash : undefined); + } + set shapeXps(value) { this.selectedInk?.forEach(i => i.rootDoc.x = Number(value)); } + set shapeYps(value) { this.selectedInk?.forEach(i => i.rootDoc.y = Number(value)); } + set shapeRot(value) { this.selectedInk?.forEach(i => i.rootDoc.rotation = Number(value)); } + set widthStk(value) { this.selectedInk?.forEach(i => i.rootDoc.strokeWidth = Number(value)); } + set shapeWid(value) { + this.selectedInk?.filter(i => i.rootDoc._width && i.rootDoc._height).forEach(i => { + const oldWidth = NumCast(i.rootDoc._width); + i.rootDoc._width = Number(value); + this._lock && (i.rootDoc._height = (i.rootDoc._width * NumCast(i.rootDoc._height)) / oldWidth); + }); + } + set shapeHgt(value) { + this.selectedInk?.filter(i => i.rootDoc._width && i.rootDoc._height).forEach(i => { + const oldHeight = NumCast(i.rootDoc._height); + i.rootDoc._height = Number(value); + this._lock && (i.rootDoc._width = (i.rootDoc._height * NumCast(i.rootDoc._width)) / oldHeight); + }); + } + + constructor() { + InkStrokeProperties.Instance = this; + } + + @undoBatch + @action + addPoints = (x: number, y: number, pts: { X: number, Y: number }[], index: number, control: { X: number, Y: number }[]) => { + this.selectedInk?.forEach(action(inkView => { + if (this.selectedInk?.length === 1) { + const doc = Document(inkView.rootDoc); + if (doc.type === DocumentType.INK) { + const ink = Cast(doc.data, InkField)?.inkData; + if (ink) { + const newPoints: { X: number, Y: number }[] = []; + var counter = 0; + for (var k = 0; k < index; k++) { + control.forEach(pt => (pts[k].X === pt.X && pts[k].Y === pt.Y) && counter++); + } + //decide where to put the new coordinate + const spNum = Math.floor(counter / 2) * 4 + 2; + + for (var i = 0; i < spNum; i++) { + ink[i] && newPoints.push({ X: ink[i].X, Y: ink[i].Y }); + } + for (var j = 0; j < 4; j++) { + newPoints.push({ X: x, Y: y }); + + } + for (var i = spNum; i < ink.length; i++) { + newPoints.push({ X: ink[i].X, Y: ink[i].Y }); + } + this._currPoint = -1; + Doc.GetProto(doc).data = new InkField(newPoints); + } + } + } + })); + } + + @undoBatch + @action + deletePoints = () => { + this.selectedInk?.forEach(action(inkView => { + if (this.selectedInk?.length === 1 && this._currPoint !== -1) { + const doc = Document(inkView.rootDoc); + if (doc.type === DocumentType.INK) { + const ink = Cast(doc.data, InkField)?.inkData; + if (ink && ink.length > 4) { + const newPoints: { X: number, Y: number }[] = []; + const toRemove = Math.floor(((this._currPoint + 2) / 4)); + for (var i = 0; i < ink.length; i++) { + if (Math.floor((i + 2) / 4) !== toRemove) { + newPoints.push({ X: ink[i].X, Y: ink[i].Y }); + } + } + this._currPoint = -1; + Doc.GetProto(doc).data = new InkField(newPoints); + if (newPoints.length === 4) { + const newerPoints: { X: number, Y: number }[] = []; + newerPoints.push({ X: newPoints[0].X, Y: newPoints[0].Y }); + newerPoints.push({ X: newPoints[0].X, Y: newPoints[0].Y }); + newerPoints.push({ X: newPoints[3].X, Y: newPoints[3].Y }); + newerPoints.push({ X: newPoints[3].X, Y: newPoints[3].Y }); + Doc.GetProto(doc).data = new InkField(newerPoints); + + } + } + } + } + })); + } + + @undoBatch + @action + rotate = (angle: number) => { + const _centerPoints: { X: number, Y: number }[] = []; + SelectionManager.SelectedDocuments().forEach(action(inkView => { + const doc = Document(inkView.rootDoc); + if (doc.type === DocumentType.INK && doc.x && doc.y && doc._width && doc._height && doc.data) { + const ink = Cast(doc.data, InkField)?.inkData; + if (ink) { + const xs = ink.map(p => p.X); + const ys = ink.map(p => p.Y); + const left = Math.min(...xs); + const top = Math.min(...ys); + const right = Math.max(...xs); + const bottom = Math.max(...ys); + _centerPoints.push({ X: left, Y: top }); + } + } + })); + + var index = 0; + SelectionManager.SelectedDocuments().forEach(action(inkView => { + const doc = Document(inkView.rootDoc); + if (doc.type === DocumentType.INK && doc.x && doc.y && doc._width && doc._height && doc.data) { + doc.rotation = Number(doc.rotation) + Number(angle); + const ink = Cast(doc.data, InkField)?.inkData; + if (ink) { + + const newPoints: { X: number, Y: number }[] = []; + ink.forEach(i => { + const newX = Math.cos(angle) * (i.X - _centerPoints[index].X) - Math.sin(angle) * (i.Y - _centerPoints[index].Y) + _centerPoints[index].X; + const newY = Math.sin(angle) * (i.X - _centerPoints[index].X) + Math.cos(angle) * (i.Y - _centerPoints[index].Y) + _centerPoints[index].Y; + newPoints.push({ X: newX, Y: newY }); + }); + Doc.GetProto(doc).data = new InkField(newPoints); + const xs = newPoints.map(p => p.X); + const ys = newPoints.map(p => p.Y); + const left = Math.min(...xs); + const top = Math.min(...ys); + const right = Math.max(...xs); + const bottom = Math.max(...ys); + + doc._height = (bottom - top); + doc._width = (right - left); + } + index++; + } + })); + } + + @undoBatch + @action + control = (xDiff: number, yDiff: number, controlNum: number) => { + this.selectedInk?.forEach(action(inkView => { + if (this.selectedInk?.length === 1) { + const doc = Document(inkView.rootDoc); + if (doc.type === DocumentType.INK && doc.x && doc.y && doc._width && doc._height && doc.data) { + const ink = Cast(doc.data, InkField)?.inkData; + if (ink) { + + const newPoints: { X: number, Y: number }[] = []; + const order = controlNum % 4; + for (var i = 0; i < ink.length; i++) { + if (controlNum === i || + (order === 0 && i === controlNum + 1) || + (order === 0 && controlNum !== 0 && i === controlNum - 2) || + (order === 0 && controlNum !== 0 && i === controlNum - 1) || + (order === 3 && i === controlNum - 1) || + (order === 3 && controlNum !== ink.length - 1 && i === controlNum + 1) || + (order === 3 && controlNum !== ink.length - 1 && i === controlNum + 2) + || ((ink[0].X === ink[ink.length - 1].X) && (ink[0].Y === ink[ink.length - 1].Y) && (i === 0 || i === ink.length - 1) && (controlNum === 0 || controlNum === ink.length - 1)) + ) { + newPoints.push({ X: ink[i].X - (xDiff * inkView.props.ScreenToLocalTransform().Scale), Y: ink[i].Y - (yDiff * inkView.props.ScreenToLocalTransform().Scale) }); + } + else { + newPoints.push({ X: ink[i].X, Y: ink[i].Y }); + } + } + const oldx = doc.x; + const oldy = doc.y; + const xs = ink.map(p => p.X); + const ys = ink.map(p => p.Y); + const left = Math.min(...xs); + const top = Math.min(...ys); + Doc.GetProto(doc).data = new InkField(newPoints); + const xs2 = newPoints.map(p => p.X); + const ys2 = newPoints.map(p => p.Y); + const left2 = Math.min(...xs2); + const top2 = Math.min(...ys2); + const right2 = Math.max(...xs2); + const bottom2 = Math.max(...ys2); + doc._height = (bottom2 - top2); + doc._width = (right2 - left2); + //if points move out of bounds + + doc.x = oldx - (left - left2); + doc.y = oldy - (top - top2); + + } + } + } + })); + } + + @undoBatch + @action + switchStk = (color: ColorState) => { + const val = String(color.hex); + this.colorStk = val; + return true; + } + + @undoBatch + @action + switchFil = (color: ColorState) => { + const val = String(color.hex); + this.colorFil = val; + return true; + } +}
\ No newline at end of file diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 41311ed86..186406424 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -1,27 +1,22 @@ -import { library } from "@fortawesome/fontawesome-svg-core"; -import { faPaintBrush } from "@fortawesome/free-solid-svg-icons"; +import { action } from "mobx"; import { observer } from "mobx-react"; +import { Doc } from "../../fields/Doc"; import { documentSchema } from "../../fields/documentSchemas"; import { InkData, InkField, InkTool } from "../../fields/InkField"; import { makeInterface } from "../../fields/Schema"; import { Cast, StrCast } from "../../fields/Types"; import { TraceMobx } from "../../fields/util"; +import { setupMoveUpEvents } from "../../Utils"; import { CognitiveServices } from "../cognitive_services/CognitiveServices"; import { InteractionUtils } from "../util/InteractionUtils"; +import { Scripting } from "../util/Scripting"; +import { UndoManager } from "../util/UndoManager"; import { ContextMenu } from "./ContextMenu"; import { ViewBoxBaseComponent } from "./DocComponent"; import "./InkingStroke.scss"; import { FieldView, FieldViewProps } from "./nodes/FieldView"; import React = require("react"); -import { Scripting } from "../util/Scripting"; -import { Doc } from "../../fields/Doc"; -import FormatShapePane from "./collections/collectionFreeForm/FormatShapePane"; -import { action } from "mobx"; -import { setupMoveUpEvents } from "../../Utils"; -import { undoBatch, UndoManager } from "../util/UndoManager"; - - -library.add(faPaintBrush); +import { InkStrokeProperties } from "./InkStrokeProperties"; type InkDocument = makeInterface<[typeof documentSchema]>; const InkDocument = makeInterface(documentSchema); @@ -47,16 +42,12 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume this.props.Document.isInkMask = true; } - @action - private formatShape = () => { - FormatShapePane.Instance.Pinned = true; - } - public _prevX = 0; public _prevY = 0; private _controlNum = 0; @action onControlDown = (e: React.PointerEvent, i: number): void => { + //TODO:renew points before controlling setupMoveUpEvents(this, e, this.onControlMove, this.onControlup, (e) => { }); this._controlUndo = UndoManager.StartBatch("DocDecs set radius"); this._prevX = e.clientX; @@ -66,15 +57,17 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume @action changeCurrPoint = (i: number) => { - FormatShapePane.Instance._currPoint = i; + if (!InkStrokeProperties.Instance) return; + InkStrokeProperties.Instance._currPoint = i; document.addEventListener("keydown", this.delPts, true); } @action onControlMove = (e: PointerEvent, down: number[]): boolean => { + if (!InkStrokeProperties.Instance) return false; const xDiff = this._prevX - e.clientX; const yDiff = this._prevY - e.clientY; - FormatShapePane.Instance.control(xDiff, yDiff, this._controlNum); + InkStrokeProperties.Instance.control(xDiff, yDiff, this._controlNum); this._prevX = e.clientX; this._prevY = e.clientY; return false; @@ -89,8 +82,8 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume } @action delPts = (e: KeyboardEvent | React.PointerEvent | undefined) => { - if (e instanceof KeyboardEvent ? e.key === "-" : true) { - FormatShapePane.Instance.deletePoints(); + if (InkStrokeProperties.Instance && (e instanceof KeyboardEvent ? e.key === "-" : true)) { + InkStrokeProperties.Instance.deletePoints(); } } @@ -98,34 +91,40 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume public static MaskDim = 50000; render() { TraceMobx(); + const formatInstance = InkStrokeProperties.Instance; + if (!formatInstance) return (null); const data: InkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? []; // const strokeWidth = Number(StrCast(this.layoutDoc.strokeWidth, ActiveInkWidth())); const strokeWidth = Number(this.layoutDoc.strokeWidth); const xs = data.map(p => p.X); const ys = data.map(p => p.Y); - const left = Math.min(...xs) - strokeWidth / 2; - const top = Math.min(...ys) - strokeWidth / 2; - const right = Math.max(...xs) + strokeWidth / 2; - const bottom = Math.max(...ys) + strokeWidth / 2; - const width = Math.max(right - left); + const lineTop = Math.min(...ys); + const lineBot = Math.max(...ys); + const lineLft = Math.min(...xs); + const lineRgt = Math.max(...xs); + const left = lineLft - strokeWidth / 2; + const top = lineTop - strokeWidth / 2; + const right = lineRgt + strokeWidth / 2; + const bottom = lineBot + strokeWidth / 2; + const width = Math.max(1, right - left); const height = Math.max(1, bottom - top); const scaleX = width === strokeWidth ? 1 : (this.props.PanelWidth() - strokeWidth) / (width - strokeWidth); const scaleY = height === strokeWidth ? 1 : (this.props.PanelHeight() - strokeWidth) / (height - strokeWidth); const strokeColor = StrCast(this.layoutDoc.color, ""); const points = InteractionUtils.CreatePolyline(data, left, top, strokeColor, strokeWidth, strokeWidth, - StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "transparent"), + StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"), StrCast(this.layoutDoc.strokeStartMarker), StrCast(this.layoutDoc.strokeEndMarker), - StrCast(this.layoutDoc.strokeDash), scaleX, scaleY, "", "none", this.props.isSelected() && strokeWidth <= 5, false); + StrCast(this.layoutDoc.strokeDash), scaleX, scaleY, "", "none", this.props.isSelected() && strokeWidth <= 5 && lineBot - lineTop > 1 && lineRgt - lineLft > 1, false); const hpoints = InteractionUtils.CreatePolyline(data, left, top, this.props.isSelected() && strokeWidth > 5 ? strokeColor : "transparent", strokeWidth, (strokeWidth + 15), - StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "transparent"), - "none", "none", "0", scaleX, scaleY, "", this.props.active() ? "visiblepainted" : "none", false, true); + StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"), + "none", "none", undefined, scaleX, scaleY, "", "visiblepainted", false, true); //points for adding const apoints = InteractionUtils.CreatePoints(data, left, top, strokeColor, strokeWidth, strokeWidth, - StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "transparent"), + StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"), StrCast(this.layoutDoc.strokeStartMarker), StrCast(this.layoutDoc.strokeEndMarker), StrCast(this.layoutDoc.strokeDash), scaleX, scaleY, "", "none", this.props.isSelected() && strokeWidth <= 5, false); @@ -161,43 +160,38 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume const dotsize = String(Math.max(width * scaleX, height * scaleY) / 40); const addpoints = apoints.map((pts, i) => - - <svg height="10" width="10"> - <circle cx={(pts.X - left - strokeWidth / 2) * scaleX + strokeWidth / 2} cy={(pts.Y - top - strokeWidth / 2) * scaleY + strokeWidth / 2} r={dotsize} stroke="invisible" stroke-width={String(Number(dotsize) / 2)} fill="invisible" - onPointerDown={(e) => { FormatShapePane.Instance.addPoints(pts.X, pts.Y, apoints, i, controlPoints); }} pointerEvents="all" cursor="all-scroll" + <svg height="10" width="10" key={`add${i}`}> + <circle cx={(pts.X - left - strokeWidth / 2) * scaleX + strokeWidth / 2} cy={(pts.Y - top - strokeWidth / 2) * scaleY + strokeWidth / 2} r={strokeWidth / 2} stroke="invisible" strokeWidth={String(Number(dotsize) / 2)} fill="invisible" + onPointerDown={(e) => { formatInstance.addPoints(pts.X, pts.Y, apoints, i, controlPoints); }} pointerEvents="all" cursor="all-scroll" /> </svg>); const controls = controlPoints.map((pts, i) => - - <svg height="10" width="10"> - <circle cx={(pts.X - left - strokeWidth / 2) * scaleX + strokeWidth / 2} cy={(pts.Y - top - strokeWidth / 2) * scaleY + strokeWidth / 2} r={dotsize} stroke="black" stroke-width={String(Number(dotsize) / 2)} fill="red" + <svg height="10" width="10" key={`ctrl${i}`}> + <circle cx={(pts.X - left - strokeWidth / 2) * scaleX + strokeWidth / 2} cy={(pts.Y - top - strokeWidth / 2) * scaleY + strokeWidth / 2} r={strokeWidth / 2} stroke="black" strokeWidth={String(Number(dotsize) / 2)} fill="red" onPointerDown={(e) => { this.changeCurrPoint(pts.I); this.onControlDown(e, pts.I); }} pointerEvents="all" cursor="all-scroll" /> </svg>); const handles = handlePoints.map((pts, i) => - - <svg height="10" width="10"> - <circle cx={(pts.X - left - strokeWidth / 2) * scaleX + strokeWidth / 2} cy={(pts.Y - top - strokeWidth / 2) * scaleY + strokeWidth / 2} r={dotsize} stroke="black" stroke-width={String(Number(dotsize) / 2)} fill="green" - onPointerDown={(e) => this.onControlDown(e, pts.I)} pointerEvents="all" cursor="all-scroll" display={(pts.dot1 === FormatShapePane.Instance._currPoint || pts.dot2 === FormatShapePane.Instance._currPoint) ? "inherit" : "none"} /> + <svg height="10" width="10" key={`hdl${i}`}> + <circle cx={(pts.X - left - strokeWidth / 2) * scaleX + strokeWidth / 2} cy={(pts.Y - top - strokeWidth / 2) * scaleY + strokeWidth / 2} r={strokeWidth / 2} stroke="black" strokeWidth={String(Number(dotsize) / 2)} fill="green" + onPointerDown={(e) => this.onControlDown(e, pts.I)} pointerEvents="all" cursor="all-scroll" display={(pts.dot1 === formatInstance._currPoint || pts.dot2 === formatInstance._currPoint) ? "inherit" : "none"} /> </svg>); const handleLines = handleLine.map((pts, i) => - - <svg height="100" width="100"> + <svg height="100" width="100" key={`line${i}`}> <line x1={(pts.X1 - left - strokeWidth / 2) * scaleX + strokeWidth / 2} y1={(pts.Y1 - top - strokeWidth / 2) * scaleY + strokeWidth / 2} - x2={(pts.X2 - left - strokeWidth / 2) * scaleX + strokeWidth / 2} y2={(pts.Y2 - top - strokeWidth / 2) * scaleY + strokeWidth / 2} stroke="green" stroke-width={String(Number(dotsize) / 2)} - display={(pts.dot1 === FormatShapePane.Instance._currPoint || pts.dot2 === FormatShapePane.Instance._currPoint) ? "inherit" : "none"} /> + x2={(pts.X2 - left - strokeWidth / 2) * scaleX + strokeWidth / 2} y2={(pts.Y2 - top - strokeWidth / 2) * scaleY + strokeWidth / 2} stroke="green" strokeWidth={String(Number(dotsize) / 2)} + display={(pts.dot1 === formatInstance._currPoint || pts.dot2 === formatInstance._currPoint) ? "inherit" : "none"} /> <line x1={(pts.X2 - left - strokeWidth / 2) * scaleX + strokeWidth / 2} y1={(pts.Y2 - top - strokeWidth / 2) * scaleY + strokeWidth / 2} - x2={(pts.X3 - left - strokeWidth / 2) * scaleX + strokeWidth / 2} y2={(pts.Y3 - top - strokeWidth / 2) * scaleY + strokeWidth / 2} stroke="green" stroke-width={String(Number(dotsize) / 2)} - display={(pts.dot1 === FormatShapePane.Instance._currPoint || pts.dot2 === FormatShapePane.Instance._currPoint) ? "inherit" : "none"} /> - + x2={(pts.X3 - left - strokeWidth / 2) * scaleX + strokeWidth / 2} y2={(pts.Y3 - top - strokeWidth / 2) * scaleY + strokeWidth / 2} stroke="green" strokeWidth={String(Number(dotsize) / 2)} + display={(pts.dot1 === formatInstance._currPoint || pts.dot2 === formatInstance._currPoint) ? "inherit" : "none"} /> </svg>); return ( <svg className="inkingStroke" - width={width} - height={height} + width={Math.max(width, height)} + height={Math.max(width, height)} style={{ pointerEvents: this.props.Document.isInkMask ? "all" : "none", transform: this.props.Document.isInkMask ? `translate(${InkingStroke.MaskDim / 2}px, ${InkingStroke.MaskDim / 2}px)` : undefined, @@ -209,6 +203,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume if (cm) { !Doc.UserDoc().noviceMode && cm.addItem({ description: "Recognize Writing", event: this.analyzeStrokes, icon: "paint-brush" }); cm.addItem({ description: "Make Mask", event: this.makeMask, icon: "paint-brush" }); + cm.addItem({ description: "Edit Points", event: action(() => formatInstance._controlBtn = !formatInstance._controlBtn), icon: "paint-brush" }); //cm.addItem({ description: "Format Shape...", event: this.formatShape, icon: "paint-brush" }); } }} @@ -216,10 +211,10 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume </defs> {hpoints} {points} - {FormatShapePane.Instance._controlBtn && this.props.isSelected() ? addpoints : ""} - {FormatShapePane.Instance._controlBtn && this.props.isSelected() ? controls : ""} - {FormatShapePane.Instance._controlBtn && this.props.isSelected() ? handles : ""} - {FormatShapePane.Instance._controlBtn && this.props.isSelected() ? handleLines : ""} + {formatInstance._controlBtn && this.props.isSelected() ? addpoints : ""} + {formatInstance._controlBtn && this.props.isSelected() ? controls : ""} + {formatInstance._controlBtn && this.props.isSelected() ? handles : ""} + {formatInstance._controlBtn && this.props.isSelected() ? handleLines : ""} </svg> ); diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss index 97ed0a901..b1ad4868c 100644 --- a/src/client/views/Main.scss +++ b/src/client/views/Main.scss @@ -1,6 +1,10 @@ @import "globalCssVariables"; @import "nodeModuleOverrides"; +:root { + --flyoutHandleWidth: 28px; + --menuPanelWidth: 60px; +} html, body { width: 100%; diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss index a05a2b858..f64449616 100644 --- a/src/client/views/MainView.scss +++ b/src/client/views/MainView.scss @@ -1,6 +1,7 @@ @import "globalCssVariables"; @import "nodeModuleOverrides"; + .dash-tooltip { font-size: 11px; padding: 2px; @@ -14,18 +15,24 @@ margin-top: 10px; } +.mainContent-div-flyout, .mainContent-div { position: relative; width: 100%; height: 100%; } +.mainContent-div-flyout { + left: calc(-1 * var(--flyoutHandleWidth)); +} + // add nodes menu. Note that the + button is actually an input label, not an actual button. .mainView-docButtons { position: absolute; - bottom: 35px; + bottom: 10px; left: calc(100% + 5px); z-index: 1; + pointer-events: none; } .mainView-snapLines { @@ -110,13 +117,16 @@ height: 55px; width: 17px; position: absolute; - top: 55%; + top: 50%; border: 1px black solid; border-radius: 0; border-top-left-radius: 10px; border-bottom-left-radius: 10px; border-right: unset; - z-index: 2; + z-index: 41; // lm_maximised has a z-index of 40 and this needs to be above that + display: flex; + align-items: center; + padding: 4px; .mainView-propertiesDragger-icon { width: 10px; @@ -143,26 +153,41 @@ cursor: auto; } -.mainView-flyoutContainer { - display: flex; - flex-direction: column; +.mainView-innerContent { + display: contents; + flex-direction: row; position: relative; - height: 100%; - background: dimgray; - .documentView-node-topmost { - background: lightgrey; + .mainView-flyoutContainer { + display: flex; + flex-direction: column; + position: relative; + height: 100%; + background: dimgray; + + .documentView-node-topmost { + background: lightgrey; + } + } + + .propertiesView { + right: 0; + position: absolute; + z-index: 2; } } .mainView-menuPanel { - - width: 60px; + min-width: var(--menuPanelWidth); background-color: #121721; - height: calc(100% - $searchpanel-height); - //overflow-y: scroll; - //overflow-x: hidden; + .collectionStackingView { + scrollbar-width: none; + } + + ::-webkit-scrollbar { + width: 0; + } .mainView-menuPanel-button { padding: 7px; @@ -265,13 +290,21 @@ transform: none !important; } +.mainView-libraryFlyout-out, .mainView-libraryFlyout { height: 100%; width: 100%; - position: absolute; + position: relative; display: flex; flex-direction: column; - z-index: 2; + z-index: 42; + + .mainView-contentArea { + position: relative; + height: 100%; + width: 100%; + overflow: visible; + } .mainView-libraryFlyout-close { right: 6; @@ -283,25 +316,27 @@ } } -.mainView-expandFlyoutButton { - position: absolute; - top: 120px; - right: 55px; - cursor: pointer; +.mainView-libraryFlyout-out { + transition: width .25s; + box-shadow: rgb(156, 147, 150) 0.2vw 0.2vw 0.2vw; } + .mainView-libraryHandle { - width: 28px; - left: calc(100% - 10px); + width: var(--flyoutHandleWidth); height: 55px; top: 50%; + left: -10px; border: 1px solid black; border-radius: 8px; - position: absolute; - z-index: 2; + position: relative; + z-index: 41; // lm_maximised has a z-index of 40 and this needs to be above that touch-action: none; background-color: lightgrey; cursor: grab; + display: flex; + align-items: center; + padding: 12px; .mainView-libraryHandle-icon { width: 10px; @@ -313,7 +348,7 @@ } -.mainView-workspace { +.mainView-dashboard { height: 200px; position: relative; display: flex; @@ -325,8 +360,53 @@ display: flex; } -.mainView-recentlyClosed { +.mainView-inactiveDocs { height: 25%; position: relative; display: flex; +} + +.mainVew-webRef { + position: absolute; + left: -1000; + top: -1000; + display: block; + width: 200px; + height: 800px; +} + +.mainVew-invisibleWebRef { + position: absolute; + left: 50; + top: 50; + display: block; + width: 500px; + height: 1000px; +} + +.lm_drag_tab { + padding: 0; + width: 15px !important; + height: 15px !important; + position: relative !important; + display: inline-flex !important; + align-items: center; + top: 0 !important; + right: unset !important; + left: 0 !important; +} +.lm_close_tab { + padding: 0; + width: 15px !important; + height: 15px !important; + position: relative !important; + display: inline-flex !important; + align-items: center; + top: 0 !important; + right: unset !important; + left: 0 !important; +} +.lm_tab, .lm_tab_active { + display: flex !important; + padding-right: 0 !important; }
\ No newline at end of file diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index ec43e6e1d..5193c3c02 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -1,48 +1,45 @@ import { library } from '@fortawesome/fontawesome-svg-core'; -import { faHireAHelper, faBuffer } from '@fortawesome/free-brands-svg-icons'; +import { faBuffer, faHireAHelper } from '@fortawesome/free-brands-svg-icons'; import * as fa from '@fortawesome/free-solid-svg-icons'; +import * as far from '@fortawesome/free-regular-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, configure, observable, reaction, runInAction } from 'mobx'; +import { action, computed, configure, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import "normalize.css"; import * as React from 'react'; -import Measure from 'react-measure'; -import { Doc, DocListCast, Field, Opt } from '../../fields/Doc'; -import { Id } from '../../fields/FieldSymbols'; +import * as ReactDOM from 'react-dom'; +import { Doc, DocListCast, Opt } from '../../fields/Doc'; import { List } from '../../fields/List'; -import { listSpec } from '../../fields/Schema'; -import { ScriptField } from '../../fields/ScriptField'; -import { BoolCast, Cast, FieldValue, StrCast } from '../../fields/Types'; -import { TraceMobx } from '../../fields/util'; -import { emptyFunction, emptyPath, returnEmptyFilter, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents, Utils, simulateMouseClick } from '../../Utils'; -import GoogleAuthenticationManager from '../apis/GoogleAuthenticationManager'; +import { PrefetchProxy } from '../../fields/Proxy'; +import { BoolCast, PromiseValue, StrCast } from '../../fields/Types'; +import { emptyFunction, emptyPath, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents, simulateMouseClick, Utils } from '../../Utils'; +import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager'; import { DocServer } from '../DocServer'; -import { Docs, DocumentOptions } from '../documents/Documents'; +import { Docs } from '../documents/Documents'; import { DocumentType } from '../documents/DocumentTypes'; import { CurrentUserUtils } from '../util/CurrentUserUtils'; import { DocumentManager } from '../util/DocumentManager'; -import GroupManager from '../util/GroupManager'; +import { GroupManager } from '../util/GroupManager'; import { HistoryUtil } from '../util/History'; +import { Hypothesis } from '../util/HypothesisUtils'; import { Scripting } from '../util/Scripting'; -import { SelectionManager } from '../util/SelectionManager'; -import SettingsManager from '../util/SettingsManager'; -import SharingManager from '../util/SharingManager'; +import { SettingsManager } from '../util/SettingsManager'; +import { SharingManager } from '../util/SharingManager'; import { SnappingManager } from '../util/SnappingManager'; import { Transform } from '../util/Transform'; import { TimelineMenu } from './animationtimeline/TimelineMenu'; import { CollectionDockingView } from './collections/CollectionDockingView'; -import FormatShapePane from "./collections/collectionFreeForm/FormatShapePane"; -import MarqueeOptionsMenu from './collections/collectionFreeForm/MarqueeOptionsMenu'; -import { PropertiesView } from './collections/collectionFreeForm/PropertiesView'; +import { MarqueeOptionsMenu } from './collections/collectionFreeForm/MarqueeOptionsMenu'; import { CollectionLinearView } from './collections/CollectionLinearView'; -import CollectionMenu from './collections/CollectionMenu'; -import { CollectionView, CollectionViewType } from './collections/CollectionView'; +import { CollectionMenu } from './collections/CollectionMenu'; +import { CollectionViewType } from './collections/CollectionView'; import { ContextMenu } from './ContextMenu'; import { DictationOverlay } from './DictationOverlay'; import { DocumentDecorations } from './DocumentDecorations'; -import GestureOverlay from './GestureOverlay'; -import { ANTIMODEMENU_HEIGHT, SEARCH_PANEL_HEIGHT } from './globalCssVariables.scss'; -import KeyManager from './GlobalKeyHandler'; +import { InkStrokeProperties } from './InkStrokeProperties'; +import { GestureOverlay } from './GestureOverlay'; +import { MENU_PANEL_WIDTH, SEARCH_PANEL_HEIGHT } from './globalCssVariables.scss'; +import { KeyManager } from './GlobalKeyHandler'; import { LinkMenu } from './linking/LinkMenu'; import "./MainView.scss"; import { AudioBox } from './nodes/AudioBox'; @@ -53,91 +50,67 @@ import { LinkDescriptionPopup } from './nodes/LinkDescriptionPopup'; import { LinkDocPreview } from './nodes/LinkDocPreview'; import { RadialMenu } from './nodes/RadialMenu'; import { TaskCompletionBox } from './nodes/TaskCompletedBox'; +import { WebBox } from './nodes/WebBox'; import { OverlayView } from './OverlayView'; -import PDFMenu from './pdf/PDFMenu'; +import { PDFMenu } from './pdf/PDFMenu'; import { PreviewCursor } from './PreviewCursor'; -import { Hypothesis } from '../util/HypothesisUtils'; -import { WebBox } from './nodes/WebBox'; -import * as ReactDOM from 'react-dom'; +import { PropertiesView } from './PropertiesView'; import { SearchBox } from './search/SearchBox'; -import { SearchUtil } from '../util/SearchUtil'; -import { Networking } from '../Network'; -import * as rp from 'request-promise'; -import { LinkManager } from '../util/LinkManager'; -import RichTextMenu from './nodes/formattedText/RichTextMenu'; +import { TraceMobx } from '../../fields/util'; +import { SelectionManager } from '../util/SelectionManager'; +const _global = (window /* browser */ || global /* node */) as any; @observer export class MainView extends React.Component { public static Instance: MainView; - private _buttonBarHeight = 36; - private _flyoutSizeOnDown = 0; - private _urlState: HistoryUtil.DocUrl; private _docBtnRef = React.createRef<HTMLDivElement>(); private _mainViewRef = React.createRef<HTMLDivElement>(); + private _lastButton: Doc | undefined; @observable private _panelWidth: number = 0; @observable private _panelHeight: number = 0; - @observable private _flyoutTranslate: boolean = false; - @observable public flyoutWidth: number = 0; - private get darkScheme() { return BoolCast(Cast(this.userDoc?.activeWorkspace, Doc, null)?.darkScheme); } + @observable private _panelContent: string = "none"; + @observable private _sidebarContent: any = this.userDoc?.sidebar; + @observable private _flyoutWidth: number = 0; + @computed private get topOffset() { return (CollectionMenu.Instance?.Pinned ? 35 : 0) + Number(SEARCH_PANEL_HEIGHT.replace("px", "")); } @computed private get userDoc() { return Doc.UserDoc(); } - @computed private get mainContainer() { return this.userDoc ? FieldValue(Cast(this.userDoc.activeWorkspace, Doc)) : CurrentUserUtils.GuestWorkspace; } + @computed private get darkScheme() { return BoolCast(CurrentUserUtils.ActiveDashboard?.darkScheme); } + @computed private get mainContainer() { return this.userDoc ? CurrentUserUtils.ActiveDashboard : CurrentUserUtils.GuestDashboard; } @computed public get mainFreeform(): Opt<Doc> { return (docs => (docs && docs.length > 1) ? docs[1] : undefined)(DocListCast(this.mainContainer!.data)); } - @computed public get searchDoc() { return Cast(this.userDoc["search-panel"], Doc) as Doc; } - - @observable public sidebarContent: any = this.userDoc?.sidebar; - @observable public panelContent: string = "none"; - @observable public showProperties: boolean = false; - public isPointerDown = false; - @computed get selectedDocumentView() { - if (SelectionManager.SelectedDocuments().length) { - return SelectionManager.SelectedDocuments()[0]; - } else { return undefined; } - } - - propertiesWidth = () => Math.max(0, Math.min(this._panelWidth - 50, CurrentUserUtils.propertiesWidth)); - @computed get propertiesIcon() { - if (this.propertiesWidth() < 10) { - return "chevron-left"; - } else { - return "chevron-right"; - } - } - @observable propertiesDownX: number | undefined; + menuPanelWidth = () => Number(MENU_PANEL_WIDTH.replace("px", "")); + propertiesWidth = () => Math.max(0, Math.min(this._panelWidth - 50, CurrentUserUtils.propertiesWidth || 0)); componentDidMount() { - DocServer.setPlaygroundFields(["dataTransition", "_viewTransition", "_panX", "_panY", "_viewScale", "_viewType", "_chromeStatus"]); // can play with these fields on someone else's - - const tag = document.createElement('script'); + document.getElementById("root")?.addEventListener("scroll", e => ((ele) => ele.scrollLeft = ele.scrollTop = 0)(document.getElementById("root")!)); + new InkStrokeProperties(); + this._sidebarContent.proto = undefined; + DocServer.setPlaygroundFields(["dataTransition", "_delayAutoHeight", "_autoHeight", "_showSidebar", "_sidebarWidthPercent", "_width", "_height", "_viewTransition", "_panX", "_panY", "_viewScale", "_scrollY", "_scrollTop", "hidden", "_curPage", "_viewType", "_chromeStatus"]); // can play with these fields on someone else's - const proto = DocServer.GetRefField("rtfProto").then(proto => { - (proto instanceof Doc) && reaction(() => StrCast(proto.BROADCAST_MESSAGE), - msg => msg && alert(msg)); - }); + DocServer.GetRefField("rtfProto").then(proto => (proto instanceof Doc) && reaction(() => StrCast(proto.BROADCAST_MESSAGE), msg => msg && alert(msg))); + const tag = document.createElement('script'); tag.src = "https://www.youtube.com/iframe_api"; const firstScriptTag = document.getElementsByTagName('script')[0]; firstScriptTag.parentNode!.insertBefore(tag, firstScriptTag); window.removeEventListener("keydown", KeyManager.Instance.handle); window.addEventListener("keydown", KeyManager.Instance.handle); + window.removeEventListener("keyup", KeyManager.Instance.unhandle); + window.addEventListener("keyup", KeyManager.Instance.unhandle); window.addEventListener("paste", KeyManager.Instance.paste as any); document.addEventListener("dash", (e: any) => { // event used by chrome plugin to tell Dash which document to focus on const id = FormattedTextBox.GetDocFromUrl(e.detail); - DocServer.GetRefField(id).then(doc => { - if (doc instanceof Doc) { - DocumentManager.Instance.jumpToDocument(doc, false, undefined); - } - }); + DocServer.GetRefField(id).then(doc => (doc instanceof Doc) ? DocumentManager.Instance.jumpToDocument(doc, false, undefined) : (null)); }); document.addEventListener("linkAnnotationToDash", Hypothesis.linkListener); + this.initEventListeners(); } componentWillUnMount() { + window.removeEventListener("keyup", KeyManager.Instance.unhandle); window.removeEventListener("keydown", KeyManager.Instance.handle); window.removeEventListener("pointerdown", this.globalPointerDown); - window.removeEventListener("pointerup", this.globalPointerUp); window.removeEventListener("paste", KeyManager.Instance.paste as any); document.removeEventListener("linkAnnotationToDash", Hypothesis.linkListener); } @@ -145,217 +118,124 @@ export class MainView extends React.Component { constructor(props: Readonly<{}>) { super(props); MainView.Instance = this; - this.sidebarContent.proto = undefined; - this._urlState = HistoryUtil.parseUrl(window.location) || {} as any; - // causes errors to be generated when modifying an observable outside of an action - - CurrentUserUtils.propertiesWidth = 0; + CurrentUserUtils._urlState = HistoryUtil.parseUrl(window.location) || {} as any; + // causes errors to be generated when modifying an observable outside of an action configure({ enforceActions: "observed" }); + if (window.location.pathname !== "/home") { const pathname = window.location.pathname.substr(1).split("/"); - if (pathname.length > 1) { - const type = pathname[0]; - if (type === "doc") { - CurrentUserUtils.MainDocId = pathname[1]; - if (!this.userDoc) { - runInAction(() => this.closeFlyout()); - DocServer.GetRefField(CurrentUserUtils.MainDocId).then(action((field: Opt<Field>) => - field instanceof Doc && (CurrentUserUtils.GuestTarget = field))); - } - } + if (pathname.length > 1 && pathname[0] === "doc") { + CurrentUserUtils.MainDocId = pathname[1]; + !this.userDoc && DocServer.GetRefField(pathname[1]).then(action(field => field instanceof Doc && (CurrentUserUtils.GuestTarget = field))); } } library.add(fa.faEdit, fa.faTrash, fa.faTrashAlt, fa.faShare, fa.faDownload, fa.faExpandArrowsAlt, fa.faLayerGroup, fa.faExternalLinkAlt, fa.faCalendar, - fa.faSquare, fa.faConciergeBell, fa.faWindowRestore, fa.faFolder, fa.faMapPin, fa.faFingerprint, fa.faCrosshairs, fa.faDesktop, fa.faUnlock, - fa.faLock, fa.faLaptopCode, fa.faMale, fa.faCopy, fa.faHandPointRight, fa.faCompass, fa.faSnowflake, fa.faMicrophone, fa.faKeyboard, - fa.faQuestion, fa.faTasks, fa.faPalette, fa.faAngleRight, fa.faBell, fa.faCamera, fa.faExpand, fa.faCaretDown, fa.faCaretLeft, fa.faCaretRight, + fa.faSquare, fa.faConciergeBell, fa.faWindowRestore, fa.faFolder, fa.faMapPin, fa.faMapMarker, fa.faFingerprint, fa.faCrosshairs, fa.faDesktop, fa.faUnlock, + fa.faLock, fa.faLaptopCode, fa.faMale, fa.faCopy, fa.faHandPointLeft, fa.faHandPointRight, fa.faCompass, fa.faSnowflake, fa.faMicrophone, fa.faKeyboard, + fa.faQuestion, fa.faTasks, fa.faPalette, fa.faAngleLeft, fa.faAngleRight, fa.faBell, fa.faCamera, fa.faExpand, fa.faCaretDown, fa.faCaretLeft, fa.faCaretRight, fa.faCaretSquareDown, fa.faCaretSquareRight, fa.faArrowsAltH, fa.faPlus, fa.faMinus, fa.faTerminal, fa.faToggleOn, fa.faFile, fa.faLocationArrow, - fa.faSearch, fa.faFileDownload, fa.faStop, fa.faCalculator, fa.faWindowMaximize, fa.faAddressCard, fa.faQuestionCircle, fa.faArrowLeft, - fa.faArrowRight, fa.faArrowDown, fa.faArrowUp, fa.faBolt, fa.faBullseye, fa.faCaretUp, fa.faCat, fa.faCheck, fa.faChevronRight, fa.faClipboard, + fa.faSearch, fa.faFileDownload, fa.faFileUpload, fa.faStop, fa.faCalculator, fa.faWindowMaximize, fa.faAddressCard, fa.faQuestionCircle, fa.faArrowLeft, + fa.faArrowRight, fa.faArrowDown, fa.faArrowUp, fa.faBolt, fa.faBullseye, fa.faCaretUp, fa.faCat, fa.faCheck, fa.faChevronRight, fa.faChevronLeft, fa.faChevronDown, fa.faChevronUp, fa.faClone, fa.faCloudUploadAlt, fa.faCommentAlt, fa.faCompressArrowsAlt, fa.faCut, fa.faEllipsisV, fa.faEraser, fa.faExclamation, fa.faFileAlt, - fa.faFileAudio, fa.faFilePdf, fa.faFilm, fa.faFilter, fa.faFont, fa.faGlobeAsia, fa.faHighlighter, fa.faLongArrowAltRight, fa.faMousePointer, + fa.faFileAudio, fa.faFilePdf, fa.faFilm, fa.faFilter, fa.faFont, fa.faGlobeAmericas, fa.faGlobeAsia, fa.faHighlighter, fa.faLongArrowAltRight, fa.faMousePointer, fa.faMusic, fa.faObjectGroup, fa.faPause, fa.faPen, fa.faPenNib, fa.faPhone, fa.faPlay, fa.faPortrait, fa.faRedoAlt, fa.faStamp, fa.faStickyNote, fa.faTimesCircle, fa.faThumbtack, fa.faTree, fa.faTv, fa.faUndoAlt, fa.faVideo, fa.faAsterisk, fa.faBrain, fa.faImage, fa.faPaintBrush, fa.faTimes, fa.faEye, fa.faArrowsAlt, fa.faQuoteLeft, fa.faSortAmountDown, fa.faAlignLeft, fa.faAlignCenter, fa.faAlignRight, fa.faHeading, fa.faRulerCombined, - fa.faFillDrip, fa.faLink, fa.faUnlink, fa.faBold, fa.faItalic, fa.faChevronLeft, fa.faUnderline, fa.faStrikethrough, fa.faSuperscript, fa.faSubscript, + fa.faFillDrip, fa.faLink, fa.faUnlink, fa.faBold, fa.faItalic, fa.faClipboard, fa.faUnderline, fa.faStrikethrough, fa.faSuperscript, fa.faSubscript, fa.faIndent, fa.faEyeDropper, fa.faPaintRoller, fa.faBars, fa.faBrush, fa.faShapes, fa.faEllipsisH, fa.faHandPaper, fa.faMap, fa.faUser, faHireAHelper, - fa.faDesktop, fa.faTrashRestore, fa.faUsers, fa.faWrench, fa.faCog, fa.faMap, fa.faBellSlash, fa.faExpandAlt, fa.faArchive, fa.faBezierCurve, fa.faCircle, + fa.faTrashRestore, fa.faUsers, fa.faWrench, fa.faCog, fa.faMap, fa.faBellSlash, fa.faExpandAlt, fa.faArchive, fa.faBezierCurve, fa.faCircle, far.faCircle, fa.faLongArrowAltRight, fa.faPenFancy, fa.faAngleDoubleRight, faBuffer, fa.faExpand, fa.faUndo, fa.faSlidersH, fa.faAngleDoubleLeft, fa.faAngleUp, fa.faAngleDown, fa.faPlayCircle, fa.faClock, fa.faRocket, fa.faExchangeAlt, faBuffer, fa.faHashtag, fa.faAlignJustify, fa.faCheckSquare, fa.faListUl, - fa.faWindowMinimize, fa.faWindowRestore); - this.initEventListeners(); + fa.faWindowMinimize, fa.faWindowRestore, fa.faTextWidth, fa.faTextHeight, fa.faClosedCaptioning, fa.faInfoCircle, fa.faTag, fa.faSyncAlt, fa.faPhotoVideo, + fa.faArrowAltCircleDown, fa.faArrowAltCircleUp, fa.faArrowAltCircleLeft, fa.faArrowAltCircleRight, fa.faStopCircle, fa.faCheckCircle, fa.faGripVertical, + fa.faSortUp, fa.faSortDown, fa.faTable, fa.faTh, fa.faThList, fa.faProjectDiagram, fa.faSignature, fa.faColumns, fa.faChevronCircleUp, fa.faUpload, + fa.faBraille, fa.faChalkboard, fa.faPencilAlt, fa.faEyeSlash, fa.faSmile, fa.faIndent, fa.faOutdent, fa.faChartBar, fa.faBan, fa.faPhoneSlash, fa.faGripLines); this.initAuthenticationRouters(); } globalPointerDown = action((e: PointerEvent) => { - this.isPointerDown = true; AudioBox.Enabled = true; const targets = document.elementsFromPoint(e.x, e.y); - if (targets && targets.length && targets[0].className.toString().indexOf("contextMenu") === -1) { - ContextMenu.Instance.closeMenu(); - } - if (targets && (targets.length && targets[0].className.toString() !== "timeline-menu-desc" && targets[0].className.toString() !== "timeline-menu-item" && targets[0].className.toString() !== "timeline-menu-input")) { - TimelineMenu.Instance.closeMenu(); - } - if (targets && targets.length && SearchBox.Instance._searchbarOpen) { - let check = false; - const icon = "icon"; - targets.forEach((thing) => { - if (thing.className.toString() === "collectionSchemaView-searchContainer" || (thing as any)?.dataset[icon] === "filter" || thing.className.toString() === "collectionSchema-header-menuOptions" || thing.className.toString() === "altcollectionTimeView-treeView") { - check = true; - } - }); - if (check === false) { - SearchBox.Instance.closeSearch(); + if (targets.length) { + const targClass = targets[0].className.toString(); + if (SearchBox.Instance._searchbarOpen || SearchBox.Instance.open) { + const check = targets.some((thing) => + (thing.className === "collectionSchemaView-searchContainer" || (thing as any)?.dataset.icon === "filter" || + thing.className === "collectionSchema-header-menuOptions")); + !check && SearchBox.Instance.resetSearch(true); } + !targClass.includes("contextMenu") && ContextMenu.Instance.closeMenu(); + !["timeline-menu-desc", "timeline-menu-item", "timeline-menu-input"].includes(targClass) && TimelineMenu.Instance.closeMenu(); } - - }); - globalPointerUp = () => this.isPointerDown = false; - initEventListeners = () => { - window.addEventListener("drop", (e) => { e.preventDefault(); }, false); // drop event handler - window.addEventListener("dragover", (e) => { e.preventDefault(); }, false); // drag event handler - // click interactions for the context menu + window.addEventListener("drop", e => e.preventDefault(), false); // prevent default behavior of navigating to a new web page + window.addEventListener("dragover", e => e.preventDefault(), false); document.addEventListener("pointerdown", this.globalPointerDown); - document.addEventListener("pointerup", this.globalPointerUp); + document.addEventListener("click", (e: MouseEvent) => { + if (!e.cancelBubble) { + const pathstr = (e as any)?.path.map((p: any) => p.classList?.toString()).join(); + if (pathstr.includes("libraryFlyout")) { + SelectionManager.DeselectAll(); + } + } + }, false); } initAuthenticationRouters = async () => { - // Load the user's active workspace, or create a new one if initial session after signup + // Load the user's active dashboard, or create a new one if initial session after signup const received = CurrentUserUtils.MainDocId; if (received && !this.userDoc) { - reaction( - () => CurrentUserUtils.GuestTarget, - target => target && this.createNewWorkspace(), - { fireImmediately: true } - ); + reaction(() => CurrentUserUtils.GuestTarget, target => target && CurrentUserUtils.createNewDashboard(Doc.UserDoc()), { fireImmediately: true }); } else { - if (received && this._urlState.sharing) { + if (received && CurrentUserUtils._urlState.sharing) { reaction(() => CollectionDockingView.Instance && CollectionDockingView.Instance.initialized, initialized => initialized && received && DocServer.GetRefField(received).then(docField => { if (docField instanceof Doc && docField._viewType !== CollectionViewType.Docking) { - CollectionDockingView.AddRightSplit(docField); + CollectionDockingView.AddSplit(docField, "right"); } }), ); } - const doc = this.userDoc && await Cast(this.userDoc.activeWorkspace, Doc); - if (doc) { - this.openWorkspace(doc); - } else { - this.createNewWorkspace(); - } - } - } - - @action - createNewWorkspace = async (id?: string) => { - const myCatalog = Doc.UserDoc().myCatalog as Doc; - const presentation = Doc.MakeCopy(Doc.UserDoc().emptyPresentation as Doc, true); - const workspaces = Cast(this.userDoc.myWorkspaces, Doc) as Doc; - const workspaceCount = DocListCast(workspaces.data).length + 1; - const freeformOptions: DocumentOptions = { - x: 0, - y: 400, - _width: this._panelWidth * .7 - this.propertiesWidth() * 0.7, - _height: this._panelHeight, - title: "Untitled Collection", - }; - const freeformDoc = CurrentUserUtils.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions); - const workspaceDoc = Docs.Create.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600, path: [myCatalog] }], { title: `Workspace ${workspaceCount}` }, id, "row"); - Doc.AddDocToList(myCatalog, "data", freeformDoc); - Doc.AddDocToList(myCatalog, "data", presentation); - Doc.UserDoc().activePresentation = presentation; - const toggleTheme = ScriptField.MakeScript(`self.darkScheme = !self.darkScheme`); - const toggleComic = ScriptField.MakeScript(`toggleComicMode()`); - const copyWorkspace = ScriptField.MakeScript(`copyWorkspace()`); - workspaceDoc.contextMenuScripts = new List<ScriptField>([toggleTheme!, toggleComic!, copyWorkspace!]); - workspaceDoc.contextMenuLabels = new List<string>(["Toggle Theme Colors", "Toggle Comic Mode", "Snapshot Workspace"]); - - Doc.AddDocToList(workspaces, "data", workspaceDoc); - // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container) - setTimeout(() => this.openWorkspace(workspaceDoc), 0); - } - - @action - openWorkspace = (doc: Doc, fromHistory = false) => { - CurrentUserUtils.MainDocId = doc[Id]; - - if (doc) { // this has the side-effect of setting the main container since we're assigning the active/guest workspace - !("presentationView" in doc) && (doc.presentationView = new List<Doc>([Docs.Create.TreeDocument([], { title: "Presentation" })])); - this.userDoc ? (this.userDoc.activeWorkspace = doc) : (CurrentUserUtils.GuestWorkspace = doc); - } - const state = this._urlState; - if (state.sharing === true && !this.userDoc) { - DocServer.Control.makeReadOnly(); - } else { - fromHistory || HistoryUtil.pushState({ - type: "doc", - docId: doc[Id], - readonly: state.readonly, - nro: state.nro, - sharing: false, + const activeDash = PromiseValue(this.userDoc.activeDashboard); + activeDash.then(dash => { + if (dash instanceof Doc) CurrentUserUtils.openDashboard(this.userDoc, dash); + else CurrentUserUtils.createNewDashboard(this.userDoc); }); - if (state.readonly === true || state.readonly === null) { - DocServer.Control.makeReadOnly(); - } else if (state.safe) { - if (!state.nro) { - DocServer.Control.makeReadOnly(); - } - CollectionView.SetSafeMode(true); - } else if (state.nro || state.nro === null || state.readonly === false) { - } else if (doc.readOnly) { - DocServer.Control.makeReadOnly(); - } else { - DocServer.Control.makeEditable(); - } } - - return true; - } - - onDrop = (e: React.DragEvent<HTMLDivElement>) => { - e.preventDefault(); - e.stopPropagation(); } @action - onResize = (r: any) => { - this._panelWidth = r.offset.width;// - this.propertiesWidth(); - this._panelHeight = r.offset.height; + createNewPresentation = async () => { + if (!await this.userDoc.myPresentations) { + this.userDoc.myPresentations = new PrefetchProxy(Docs.Create.TreeDocument([], { + title: "PRESENTATION TRAILS", _height: 100, forceActive: true, boxShadow: "0 0", lockedPosition: true, treeViewOpen: true, system: true + })); + } + const pres = Docs.Create.PresDocument(new List<Doc>(), + { title: "Untitled Presentation", _viewType: CollectionViewType.Stacking, _width: 400, _height: 500, targetDropAction: "alias", _chromeStatus: "replaced", boxShadow: "0 0" }); + CollectionDockingView.AddSplit(pres, "right"); + this.userDoc.activePresentation = pres; + Doc.AddDocToList(this.userDoc.myPresentations as Doc, "data", pres); } - @action - getPWidth = () => this._panelWidth - this.propertiesWidth() - + getPWidth = () => this._panelWidth - this.propertiesWidth(); getPHeight = () => this._panelHeight; - getContentsHeight = () => this._panelHeight - this._buttonBarHeight; + getContentsHeight = () => this._panelHeight - Number(SEARCH_PANEL_HEIGHT.replace("px", "")); defaultBackgroundColors = (doc: Opt<Doc>, renderDepth: number) => { - if (this.panelContent === doc?.title) return "lightgrey"; - if (doc?.type === DocumentType.COL) { - if (doc.title === "Basic Item Creators" || doc.title === "sidebar-tools" - || doc.title === "sidebar-recentlyClosed" || doc.title === "sidebar-catalog" - || doc.title === "Mobile Uploads" || doc.title === "COLLECTION_PROTO" - || doc.title === "Advanced Item Prototypes" || doc.title === "all Creators") { - return "lightgrey"; - } - return StrCast(renderDepth > 0 ? Doc.UserDoc().activeCollectionNestedBackground : Doc.UserDoc().activeCollectionBackground); + return Doc.IsSystem(doc) ? "lightgrey" : StrCast(renderDepth > 0 ? Doc.UserDoc().activeCollectionNestedBackground : Doc.UserDoc().activeCollectionBackground); } if (this.darkScheme) { switch (doc?.type) { case DocumentType.FONTICON: return "white"; case DocumentType.RTF || DocumentType.LABEL || DocumentType.BUTTON: return "#2d2d2d"; case DocumentType.LINK: - case DocumentType.COL: { - if (doc._viewType !== CollectionViewType.Freeform && doc._viewType !== CollectionViewType.Time) return "rgb(62,62,62)"; - } + case DocumentType.COL: if (doc._viewType !== CollectionViewType.Freeform && doc._viewType !== CollectionViewType.Time) return "rgb(62,62,62)"; default: return "black"; } } else { @@ -365,9 +245,7 @@ export class MainView extends React.Component { case DocumentType.BUTTON: case DocumentType.LABEL: return "lightgray"; case DocumentType.LINK: - case DocumentType.COL: { - if (doc._viewType !== CollectionViewType.Freeform && doc._viewType !== CollectionViewType.Time) return "lightgray"; - } + case DocumentType.COL: if (doc._viewType !== CollectionViewType.Freeform && doc._viewType !== CollectionViewType.Time) return "lightgray"; default: return "white"; } } @@ -387,8 +265,6 @@ export class MainView extends React.Component { removeDocument={undefined} ScreenToLocalTransform={Transform.Identity} ContentScaling={returnOne} - NativeHeight={returnZero} - NativeWidth={returnZero} PanelWidth={this.getPWidth} PanelHeight={this.getPHeight} focus={emptyFunction} @@ -396,6 +272,8 @@ export class MainView extends React.Component { whenActiveChanged={emptyFunction} bringToFront={emptyFunction} docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} renderDepth={-1} @@ -403,67 +281,46 @@ export class MainView extends React.Component { } @computed get dockingContent() { - TraceMobx(); - const mainContainer = this.mainContainer; - const width = this.flyoutWidth + this.propertiesWidth(); - return <div className="mainContent-div" onDrop={this.onDrop} style={{ width: `calc(100% - ${width}px)`, height: `calc(100% - ${SEARCH_PANEL_HEIGHT})` }}> - {!mainContainer ? (null) : this.mainDocView} + return <div className={`mainContent-div${this._flyoutWidth ? "-flyout" : ""}`} onDrop={e => { e.stopPropagation(); e.preventDefault(); }} + style={{ minWidth: `calc(100% - ${this._flyoutWidth + this.menuPanelWidth() + this.propertiesWidth()}px)`, width: `calc(100% - ${this._flyoutWidth + this.menuPanelWidth() + this.propertiesWidth()}px)` }}> + {!this.mainContainer ? (null) : this.mainDocView} </div>; } @action onPropertiesPointerDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => { - CurrentUserUtils.propertiesWidth = this._panelWidth - e.clientX; - return false; - }), returnFalse, action(() => CurrentUserUtils.propertiesWidth = this.propertiesWidth() < 15 ? Math.min(this._panelWidth - 50, 250) : 0), false); + setupMoveUpEvents(this, e, + action(e => (CurrentUserUtils.propertiesWidth = Math.max(0, this._panelWidth - e.clientX)) ? false : false), + action(() => CurrentUserUtils.propertiesWidth < 5 && (CurrentUserUtils.propertiesWidth = 0)), + action(() => CurrentUserUtils.propertiesWidth = this.propertiesWidth() < 15 ? Math.min(this._panelWidth - 50, 250) : 0), false); } @action onFlyoutPointerDown = (e: React.PointerEvent) => { - if (this._flyoutTranslate) { - setupMoveUpEvents(this, e, action((e: PointerEvent) => { - this.flyoutWidth = Math.max(e.clientX, 0); - if (this.flyoutWidth < 5) { - this.panelContent = "none"; - this._lastButton && (this._lastButton.color = "white"); - this._lastButton && (this._lastButton._backgroundColor = ""); - } - return false; - }), emptyFunction, action(() => { - if (this.flyoutWidth < 15) MainView.expandFlyout(); - else this.closeFlyout(); - })); - } + setupMoveUpEvents(this, e, + action(e => (this._flyoutWidth = Math.max(e.clientX - 58, 0)) ? false : false), + () => this._flyoutWidth < 5 && this.closeFlyout(), + this.closeFlyout); } - flyoutWidthFunc = () => this.flyoutWidth; - addDocTabFunc = (doc: Doc, where: string, libraryPath?: Doc[]): boolean => { - return where === "close" ? CollectionDockingView.CloseRightSplit(doc) : - doc.dockingConfig ? this.openWorkspace(doc) : - CollectionDockingView.AddRightSplit(doc, libraryPath); - } + flyoutWidthFunc = () => this._flyoutWidth; sidebarScreenToLocal = () => new Transform(0, (CollectionMenu.Instance.Pinned ? -35 : 0) - Number(SEARCH_PANEL_HEIGHT.replace("px", "")), 1); mainContainerXf = () => this.sidebarScreenToLocal().translate(-58, 0); + addDocTabFunc = (doc: Doc, where: string, libraryPath?: Doc[]): boolean => { + return where === "close" ? CollectionDockingView.CloseSplit(doc) : + doc.dockingConfig ? CurrentUserUtils.openDashboard(Doc.UserDoc(), doc) : CollectionDockingView.AddSplit(doc, "right"); + } @computed get flyout() { - if (!this.sidebarContent) return null; - return <div className="mainView-libraryFlyout"> - <div className="mainView-contentArea" style={{ position: "relative", height: `calc(100% - ${SEARCH_PANEL_HEIGHT})`, width: "100%", overflow: "visible" }}> - {/* {this.flyoutWidth > 0 ? <div className="mainView-libraryFlyout-close" - onPointerDown={this.closeFlyout}> - <FontAwesomeIcon icon="times" color="black" size="lg" /> - </div> : null} */} - + return <div className={`mainView-libraryFlyout${this._flyoutWidth ? "" : "-out"}`} style={{ minWidth: this._flyoutWidth, width: this._flyoutWidth }} > + <div className="mainView-contentArea" > <DocumentView - Document={this.sidebarContent} + Document={this._sidebarContent} DataDoc={undefined} LibraryPath={emptyPath} addDocument={undefined} addDocTab={this.addDocTabFunc} pinToPres={emptyFunction} - NativeHeight={returnZero} - NativeWidth={returnZero} rootSelected={returnTrue} removeDocument={returnFalse} onClick={undefined} @@ -478,13 +335,16 @@ export class MainView extends React.Component { whenActiveChanged={emptyFunction} bringToFront={emptyFunction} docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} relative={true} forcedBackgroundColor={() => "lightgrey"} /> </div> - {this.docButtons}</div>; + {this.docButtons} + </div>; } @computed get menuPanel() { @@ -496,14 +356,12 @@ export class MainView extends React.Component { addDocument={undefined} addDocTab={this.addDocTabFunc} pinToPres={emptyFunction} - NativeHeight={returnZero} - NativeWidth={returnZero} rootSelected={returnTrue} removeDocument={returnFalse} onClick={undefined} ScreenToLocalTransform={this.sidebarScreenToLocal} ContentScaling={returnOne} - PanelWidth={() => 60} + PanelWidth={this.menuPanelWidth} PanelHeight={this.getContentsHeight} renderDepth={0} focus={emptyFunction} @@ -512,6 +370,8 @@ export class MainView extends React.Component { whenActiveChanged={emptyFunction} bringToFront={emptyFunction} docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} relative={true} @@ -520,163 +380,90 @@ export class MainView extends React.Component { </div>; } - - @action - closeFlyout = () => { - this._lastButton && (this._lastButton.color = "white"); - this._lastButton && (this._lastButton._backgroundColor = ""); - this.panelContent = "none"; - this.flyoutWidth = 0; - } - - get groupManager() { return GroupManager.Instance; } - - _lastButton: Doc | undefined; @action selectMenu = (button: Doc) => { const title = StrCast(Doc.GetProto(button).title); - this._lastButton && (this._lastButton.color = "white"); - this._lastButton && (this._lastButton._backgroundColor = ""); - if (this.panelContent === title && this.flyoutWidth !== 0) { - this.panelContent = "none"; - this.flyoutWidth = 0; - } else { - let panelDoc: Doc | undefined; - switch (this.panelContent = title) { - case "Tools": panelDoc = Doc.UserDoc()["sidebar-tools"] as Doc ?? undefined; break; - case "Workspace": panelDoc = Doc.UserDoc()["sidebar-workspaces"] as Doc ?? undefined; break; - case "Catalog": panelDoc = Doc.UserDoc()["sidebar-catalog"] as Doc ?? undefined; break; - case "Archive": panelDoc = Doc.UserDoc()["sidebar-recentlyClosed"] as Doc ?? undefined; break; - case "Settings": SettingsManager.Instance.open(); break; - case "Import": panelDoc = Doc.UserDoc()["sidebar-import"] as Doc ?? undefined; break; - case "Sharing": panelDoc = Doc.UserDoc()["sidebar-sharing"] as Doc ?? undefined; break; - case "User Doc": panelDoc = Doc.UserDoc()["sidebar-userDoc"] as Doc ?? undefined; break; + const willOpen = !this._flyoutWidth || this._panelContent !== title; + this.closeFlyout(); + if (willOpen) { + switch (this._panelContent = title) { + case "Settings": + SettingsManager.Instance.open(); + break; + case "Catalog": + SearchBox.Instance._searchFullDB = "My Stuff"; + SearchBox.Instance.enter(undefined); + break; + default: + this.expandFlyout(button); } - this.sidebarContent.proto = panelDoc; - if (panelDoc) { - MainView.expandFlyout(); - button._backgroundColor = "lightgrey"; - button.color = "black"; - this._lastButton = button; - } else this.flyoutWidth = 0; } return true; } - @action - closeProperties = () => { - CurrentUserUtils.propertiesWidth = 0; - } - - @computed get propertiesView() { - TraceMobx(); - return <div className="mainView-propertiesView" style={{ - overflow: this.propertiesWidth() < 15 ? "hidden" : undefined - }}> - <PropertiesView - width={this.propertiesWidth()} - height={this._panelHeight} - renderDepth={1} - ScreenToLocalTransform={Transform.Identity} - onDown={this.closeProperties} - /> - </div>; - } - @computed get mainInnerContent() { - const rightFlyout = this.propertiesWidth() - 1; return <> {this.menuPanel} - <div style={{ display: "contents", flexDirection: "row", position: "relative" }}> - <div className="mainView-flyoutContainer" style={{ width: this.flyoutWidth }}> - {this.flyoutWidth !== 0 ? <div className="mainView-libraryHandle" - onPointerDown={this.onFlyoutPointerDown} - //style={{ backgroundColor: '#8c8b8b' }} - > - <span title="library View Dragger" style={{ - width: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "100%" : "3vw", - //height: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "100%" : "100vh", - position: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "absolute" : "fixed", - top: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "" : "0" - }} /> - <div className="mainview-libraryHandle-icon"> - <FontAwesomeIcon icon="chevron-left" color="black" size="sm" /> - </div> - </div> : null} - <div className="mainView-libraryFlyout" style={{ - //transformOrigin: this._flyoutTranslate ? "" : "left center", - transition: this._flyoutTranslate ? "" : "width .5s", - //transform: `scale(${this._flyoutTranslate ? 1 : 0.8})`, - boxShadow: this._flyoutTranslate ? "" : "rgb(156, 147, 150) 0.2vw 0.2vw 0.2vw" - }}> - {this.flyout} - {this.expandButton} - </div> + <div className="mainView-innerContent" > + {this.flyout} + <div className="mainView-libraryHandle" style={{ display: !this._flyoutWidth ? "none" : undefined }} onPointerDown={this.onFlyoutPointerDown} > + <FontAwesomeIcon icon="chevron-left" color="black" size="sm" /> </div> + {this.dockingContent} - {this.showProperties ? (null) : - <div className="mainView-propertiesDragger" title="Properties View Dragger" onPointerDown={this.onPropertiesPointerDown} - style={{ right: rightFlyout, top: "50%" }}> - <div className="mainView-propertiesDragger-icon"> - <FontAwesomeIcon icon={this.propertiesIcon} color="black" size="sm" /> </div> - </div> - } - {this.propertiesWidth() < 10 ? (null) : - <div style={{ width: this.propertiesWidth(), height: "calc(100% - 35px)" }}> {this.propertiesView} </div>} + + <div className="mainView-propertiesDragger" onPointerDown={this.onPropertiesPointerDown} style={{ right: this.propertiesWidth() - 1 }}> + <FontAwesomeIcon icon={this.propertiesWidth() < 10 ? "chevron-left" : "chevron-right"} color="black" size="sm" /> + </div> + {this.propertiesWidth() < 10 ? (null) : <PropertiesView width={this.propertiesWidth()} height={this.getContentsHeight()} />} </div> </>; } @computed get mainContent() { - const n = (CollectionMenu.Instance?.Pinned ? 1 : 0); - const height = `calc(100% - ${n * Number(ANTIMODEMENU_HEIGHT.replace("px", ""))}px)`; - const pinned = FormatShapePane.Instance?.Pinned; - const innerContent = this.mainInnerContent; - return !this.userDoc ? (null) : ( - <Measure offset onResize={this.onResize}> - {({ measureRef }) => - <div className="mainView-mainContent" ref={measureRef} style={{ - color: this.darkScheme ? "rgb(205,205,205)" : "black", - //change to times 2 for both pinned - height, - width: pinned ? `calc(100% - 200px)` : "100%" - }} > - {innerContent} - </div> - } - </Measure>); + return !this.userDoc ? (null) : + <div className="mainView-mainContent" ref={r => { + r && new _global.ResizeObserver(action(() => { this._panelWidth = r.getBoundingClientRect().width; this._panelHeight = r.getBoundingClientRect().height; })).observe(r); + }} style={{ + color: this.darkScheme ? "rgb(205,205,205)" : "black", + height: `calc(100% - ${this.topOffset}px)`, + width: "100%", + }} > + {this.mainInnerContent} + </div>; } - public static expandFlyout = action(() => { - MainView.Instance._flyoutTranslate = true; - MainView.Instance.flyoutWidth = (MainView.Instance.flyoutWidth || 250); - + expandFlyout = action((button: Doc) => { + this._flyoutWidth = (this._flyoutWidth || 250); + this._sidebarContent.proto = button.target as any; + button._backgroundColor = "lightgrey"; + button.color = "black"; + this._lastButton = button; }); - @computed get expandButton() { - return !this._flyoutTranslate ? (<div className="mainView-expandFlyoutButton" title="Re-attach sidebar" onPointerDown={MainView.expandFlyout}></div>) : (null); - } + closeFlyout = action(() => { + this._lastButton && (this._lastButton.color = "white"); + this._lastButton && (this._lastButton._backgroundColor = ""); + this._panelContent = "none"; + this._sidebarContent.proto = undefined; + this._flyoutWidth = 0; + }); - addButtonDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg: boolean, doc) => { - const ret = flg && Doc.AddDocToList(Doc.UserDoc().dockedBtns as Doc, "data", doc); - ret && (doc._stayInCollection = undefined); - return ret; - }, true) remButtonDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg: boolean, doc) => flg && Doc.RemoveDocFromList(Doc.UserDoc().dockedBtns as Doc, "data", doc), true); moveButtonDoc = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => this.remButtonDoc(doc) && addDocument(doc); + addButtonDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg: boolean, doc) => flg && Doc.AddDocToList(Doc.UserDoc().dockedBtns as Doc, "data", doc), true); buttonBarXf = () => { if (!this._docBtnRef.current) return Transform.Identity(); const { scale, translateX, translateY } = Utils.GetScreenTransform(this._docBtnRef.current); return new Transform(-translateX, -translateY, 1 / scale); } + @computed get docButtons() { - const dockedBtns = Doc.UserDoc()?.dockedBtns; - if (dockedBtns instanceof Doc) { - return <div className="mainView-docButtons" ref={this._docBtnRef} - style={{ height: !dockedBtns.linearViewIsExpanded ? "42px" : undefined }} > + return !(this.userDoc.dockedBtns instanceof Doc) ? (null) : + <div className="mainView-docButtons" ref={this._docBtnRef} style={{ height: !this.userDoc.dockedBtns.linearViewIsExpanded ? "42px" : undefined }} > <CollectionLinearView - Document={dockedBtns} + Document={this.userDoc.dockedBtns} DataDoc={undefined} LibraryPath={emptyPath} fieldKey={"data"} @@ -697,31 +484,20 @@ export class MainView extends React.Component { onClick={undefined} ScreenToLocalTransform={this.buttonBarXf} ContentScaling={returnOne} - NativeHeight={returnZero} - NativeWidth={returnZero} PanelWidth={this.flyoutWidthFunc} PanelHeight={this.getContentsHeight} renderDepth={0} focus={emptyFunction} whenActiveChanged={emptyFunction} docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} /> </div>; - } - return (null); - } - - get mainViewElement() { - return document.getElementById("mainView-container"); - } - - get mainViewRef() { - return this._mainViewRef; } - @computed get snapLines() { - return !Doc.UserDoc().showSnapLines ? (null) : <div className="mainView-snapLines"> + return !this.userDoc.showSnapLines ? (null) : <div className="mainView-snapLines"> <svg style={{ width: "100%", height: "100%" }}> {SnappingManager.horizSnapLines().map(l => <line x1="0" y1={l} x2="2000" y2={l} stroke="black" opacity={0.3} strokeWidth={0.5} strokeDasharray={"1 1"} />)} {SnappingManager.vertSnapLines().map(l => <line y1="0" x1={l} y2="2000" x2={l} stroke="black" opacity={0.3} strokeWidth={0.5} strokeDasharray={"1 1"} />)} @@ -751,12 +527,18 @@ export class MainView extends React.Component { </defs> </svg>; } + select = (ctrlPressed: boolean) => { }; @computed get search() { + TraceMobx(); return <div className="mainView-searchPanel"> - {/* <div style={{ float: "left", marginLeft: "10px" }}>{Doc.CurrentUserEmail}</div> */} - <div><DocumentView Document={this.searchDoc} - DataDoc={undefined} + <SearchBox Document={CurrentUserUtils.MySearchPanelDoc} + DataDoc={CurrentUserUtils.MySearchPanelDoc} + fieldKey="data" + dropAction="move" + isSelected={returnTrue} + active={returnTrue} + select={this.select} LibraryPath={emptyPath} addDocument={undefined} addDocTab={this.addDocTabFunc} @@ -767,25 +549,23 @@ export class MainView extends React.Component { removeDocument={undefined} ScreenToLocalTransform={Transform.Identity} ContentScaling={returnOne} - NativeHeight={returnZero} - NativeWidth={returnZero} PanelWidth={this.getPWidth} PanelHeight={this.getPHeight} renderDepth={0} focus={emptyFunction} - parentActive={returnTrue} whenActiveChanged={emptyFunction} bringToFront={emptyFunction} docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} - /></div> + ContainingCollectionDoc={undefined} /> </div>; } @computed get invisibleWebBox() { // see note under the makeLink method in HypothesisUtils.ts return !DocumentLinksButton.invisibleWebDoc ? null : - <div style={{ position: 'absolute', left: 50, top: 50, display: 'block', width: '500px', height: '1000px' }} ref={DocumentLinksButton.invisibleWebRef}> + <div className="mainView-invisibleWebRef" ref={DocumentLinksButton.invisibleWebRef}> <WebBox fieldKey={"data"} ContainingCollectionView={undefined} @@ -806,17 +586,16 @@ export class MainView extends React.Component { focus={returnFalse} PanelWidth={() => 500} PanelHeight={() => 800} - NativeHeight={() => 500} - NativeWidth={() => 800} ContentScaling={returnOne} docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} /> </div>; } render() { - return (<div className={"mainView-container" + (this.darkScheme ? "-dark" : "")} ref={this._mainViewRef}> - + return (<div className={"mainView-container" + (this.darkScheme ? "-dark" : "")} onScroll={() => ((ele) => ele.scrollTop = ele.scrollLeft = 0)(document.getElementById("root")!)} ref={this._mainViewRef}> {this.inkResources} <DictationOverlay /> <SharingManager /> @@ -826,7 +605,6 @@ export class MainView extends React.Component { <DocumentDecorations /> {this.search} <CollectionMenu /> - <FormatShapePane /> {LinkDescriptionPopup.descriptionPopup ? <LinkDescriptionPopup /> : null} {DocumentLinksButton.EditLink ? <LinkMenu docView={DocumentLinksButton.EditLink} addDocTab={DocumentLinksButton.EditLink.props.addDocTab} changeFlyout={emptyFunction} /> : (null)} {LinkDocPreview.LinkInfo ? <LinkDocPreview location={LinkDocPreview.LinkInfo.Location} backgroundColor={this.defaultBackgroundColors} @@ -838,15 +616,13 @@ export class MainView extends React.Component { <PreviewCursor /> <TaskCompletionBox /> <ContextMenu /> - <FormatShapePane /> <RadialMenu /> <PDFMenu /> <MarqueeOptionsMenu /> - <OverlayView /> <TimelineMenu /> {this.snapLines} - <div ref={this.makeWebRef} style={{ position: 'absolute', left: -1000, top: -1000, display: 'block', width: '200px', height: '800px' }} /> + <div className="mainView-webRef" ref={this.makeWebRef} /> </div >); } @@ -855,7 +631,7 @@ export class MainView extends React.Component { invisibleDoc => { ReactDOM.unmountComponentAtNode(ele); invisibleDoc && ReactDOM.render(<span title="Drag as document" className="invisible-webbox" > - <div style={{ position: 'absolute', left: -1000, top: -1000, display: 'block', width: '200px', height: '800px' }} ref={DocumentLinksButton.invisibleWebRef}> + <div className="mainView-webRef" ref={DocumentLinksButton.invisibleWebRef}> <WebBox fieldKey={"data"} ContainingCollectionView={undefined} @@ -876,116 +652,29 @@ export class MainView extends React.Component { focus={returnFalse} PanelWidth={() => 500} PanelHeight={() => 800} - NativeHeight={() => 500} - NativeWidth={() => 800} ContentScaling={returnOne} docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} /> </div>; </span>, ele); - var success = false; + let success = false; const onSuccess = () => { success = true; clearTimeout(interval); document.removeEventListener("editSuccess", onSuccess); }; - // For some reason, Hypothes.is annotations don't load until a click is registered on the page, + // For some reason, Hypothes.is annotations don't load until a click is registered on the page, // so we keep simulating clicks until annotations have loaded and editing is successful - const interval = setInterval(() => { - !success && simulateMouseClick(ele, 50, 50, 50, 50); - }, 500); - + const interval = setInterval(() => !success && simulateMouseClick(ele, 50, 50, 50, 50), 500); setTimeout(() => !success && clearInterval(interval), 10000); // give up if no success after 10s document.addEventListener("editSuccess", onSuccess); }); } - - importDocument = () => { - const sidebar = Cast(Doc.UserDoc()["sidebar-import-documents"], Doc, null); - const sidebarDocView = DocumentManager.Instance.getDocumentView(sidebar); - const input = document.createElement("input"); - input.type = "file"; - input.multiple = true; - input.accept = ".zip, application/pdf, video/*, image/*, audio/*"; - input.onchange = async _e => { - const upload = Utils.prepend("/uploadDoc"); - const formData = new FormData(); - const file = input.files && input.files[0]; - if (file && file.type === 'application/zip') { - formData.append('file', file); - formData.append('remap', "true"); - const response = await fetch(upload, { method: "POST", body: formData }); - const json = await response.json(); - if (json !== "error") { - const doc = await DocServer.GetRefField(json); - if (doc instanceof Doc && sidebarDocView) { - sidebarDocView.props.addDocument?.(doc); - setTimeout(() => { - SearchUtil.Search(`{!join from=id to=proto_i}id:link*`, true, {}).then(docs => { - docs.docs.forEach(d => LinkManager.Instance.addLink(d)); - }); - }, 2000); // need to give solr some time to update so that this query will find any link docs we've added. - - } - } - } else if (input.files && input.files.length !== 0) { - const files = input.files || []; - Array.from(files).forEach(async file => { - const res = await Networking.UploadFilesToServer(file); - res.map(async ({ result }) => { - const name = file.name; - if (result instanceof Error) { - return; - } - const path = Utils.prepend(result.accessPaths.agnostic.client); - let doc: Doc; - // Case 1: File is a video - if (file.type.includes("video")) { - doc = Docs.Create.VideoDocument(path, { _height: 100, title: name }); - // Case 2: File is a PDF document - } else if (file.type === "application/pdf") { - doc = Docs.Create.PdfDocument(path, { _height: 100, _fitWidth: true, title: name }); - // Case 3: File is an image - } else if (file.type.includes("image")) { - doc = Docs.Create.ImageDocument(path, { _height: 100, title: name }); - // Case 4: File is an audio document - } else { - doc = Docs.Create.AudioDocument(path, { title: name }); - } - const res = await rp.get(Utils.prepend("/getUserDocumentId")); - if (!res) { - throw new Error("No user id returned"); - } - const field = await DocServer.GetRefField(res); - let pending: Opt<Doc>; - if (field instanceof Doc) { - pending = sidebar; - } - if (pending) { - const data = await Cast(pending.data, listSpec(Doc)); - if (data) data.push(doc); - else pending.data = new List([doc]); - } - }); - }); - } else { - console.log("No file selected"); - } - }; - input.click(); - } } + Scripting.addGlobal(function selectMainMenu(doc: Doc, title: string) { MainView.Instance.selectMenu(doc); }); -Scripting.addGlobal(function freezeSidebar() { MainView.expandFlyout(); }); -Scripting.addGlobal(function toggleComicMode() { Doc.UserDoc().fontFamily = "Comic Sans MS"; Doc.UserDoc().renderStyle = Doc.UserDoc().renderStyle === "comic" ? undefined : "comic"; }); -Scripting.addGlobal(function copyWorkspace() { - const copiedWorkspace = Doc.MakeCopy(Cast(Doc.UserDoc().activeWorkspace, Doc, null), true); - const workspaces = Cast(Doc.UserDoc().myWorkspaces, Doc, null); - Doc.AddDocToList(workspaces, "data", copiedWorkspace); - // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container) - setTimeout(() => MainView.Instance.openWorkspace(copiedWorkspace), 0); -}); -Scripting.addGlobal(function importDocument() { return MainView.Instance.importDocument(); }, - "imports files from device directly into the import sidebar"); +Scripting.addGlobal(function toggleComicMode() { Doc.UserDoc().fontFamily = "Comic Sans MS"; Doc.UserDoc().renderStyle = Doc.UserDoc().renderStyle === "comic" ? undefined : "comic"; });
\ No newline at end of file diff --git a/src/client/views/MainViewModal.tsx b/src/client/views/MainViewModal.tsx index 19387f619..34e0ff126 100644 --- a/src/client/views/MainViewModal.tsx +++ b/src/client/views/MainViewModal.tsx @@ -14,7 +14,7 @@ export interface MainViewOverlayProps { } @observer -export default class MainViewModal extends React.Component<MainViewOverlayProps> { +export class MainViewModal extends React.Component<MainViewOverlayProps> { render() { const p = this.props; diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx index 49580cde4..b40c9edfb 100644 --- a/src/client/views/OverlayView.tsx +++ b/src/client/views/OverlayView.tsx @@ -1,10 +1,11 @@ import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; import * as React from "react"; +import ReactLoading from 'react-loading'; import { Doc, DocListCast, Opt } from "../../fields/Doc"; import { Id } from "../../fields/FieldSymbols"; import { NumCast, Cast } from "../../fields/Types"; -import { emptyFunction, emptyPath, returnEmptyString, returnFalse, returnOne, returnTrue, returnZero, Utils, setupMoveUpEvents, returnEmptyFilter } from "../../Utils"; +import { emptyFunction, emptyPath, returnEmptyString, returnFalse, returnOne, returnTrue, returnZero, Utils, setupMoveUpEvents, returnEmptyFilter, returnEmptyDoclist } from "../../Utils"; import { Transform } from "../util/Transform"; import { CollectionFreeFormLinksView } from "./collections/collectionFreeForm/CollectionFreeFormLinksView"; import { DocumentView } from "./nodes/DocumentView"; @@ -145,7 +146,7 @@ export class OverlayView extends React.Component { @computed get overlayDocs() { - const userDocOverlays = Doc.UserDoc().myOverlayDocuments; + const userDocOverlays = Doc.UserDoc().myOverlayDocs; if (!userDocOverlays) { return null; } @@ -165,7 +166,7 @@ export class OverlayView extends React.Component { dragData.dropAction = "move"; dragData.removeDocument = (doc: Doc | Doc[]) => { const docs = (doc instanceof Doc) ? [doc] : doc; - docs.forEach(d => Doc.RemoveDocFromList(Cast(Doc.UserDoc().myOverlayDocuments, Doc, null), "data", d)); + docs.forEach(d => Doc.RemoveDocFromList(Cast(Doc.UserDoc().myOverlayDocs, Doc, null), "data", d)); return true; }; dragData.moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => { @@ -192,8 +193,6 @@ export class OverlayView extends React.Component { addDocument={undefined} removeDocument={undefined} ContentScaling={returnOne} - NativeHeight={returnZero} - NativeWidth={returnZero} PanelWidth={returnOne} PanelHeight={returnOne} ScreenToLocalTransform={Transform.Identity} @@ -205,12 +204,19 @@ export class OverlayView extends React.Component { addDocTab={returnFalse} pinToPres={emptyFunction} docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} /> </div>; }); } + public static ShowSpinner() { + return OverlayView.Instance.addElement(<ReactLoading type="spinningBubbles" color="green" height={250} width={250} />, { x: 300, y: 200 }); + } + + render() { return ( <div className="overlayView" id="overlayView"> diff --git a/src/client/views/Palette.tsx b/src/client/views/Palette.tsx index 0a4334302..62e0fb379 100644 --- a/src/client/views/Palette.tsx +++ b/src/client/views/Palette.tsx @@ -3,7 +3,7 @@ import { observer } from "mobx-react"; import * as React from "react"; import { Doc } from "../../fields/Doc"; import { NumCast } from "../../fields/Types"; -import { emptyFunction, emptyPath, returnEmptyString, returnZero, returnFalse, returnOne, returnTrue, returnEmptyFilter } from "../../Utils"; +import { emptyFunction, emptyPath, returnEmptyString, returnZero, returnFalse, returnOne, returnTrue, returnEmptyFilter, returnEmptyDoclist } from "../../Utils"; import { Transform } from "../util/Transform"; import { DocumentView } from "./nodes/DocumentView"; import "./Palette.scss"; @@ -49,8 +49,6 @@ export default class Palette extends React.Component<PaletteProps> { onClick={undefined} ScreenToLocalTransform={Transform.Identity} ContentScaling={returnOne} - NativeHeight={returnZero} - NativeWidth={returnZero} PanelWidth={() => window.screen.width} PanelHeight={() => window.screen.height} renderDepth={0} @@ -60,6 +58,8 @@ export default class Palette extends React.Component<PaletteProps> { whenActiveChanged={emptyFunction} bringToFront={emptyFunction} docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} /> <div className="palette-cover" style={{ transform: `translate(${Math.max(0, this._selectedIndex) * 50.75 + 23}px, 0px)` }}></div> diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx index d7034fcfb..f5f115e01 100644 --- a/src/client/views/PreviewCursor.tsx +++ b/src/client/views/PreviewCursor.tsx @@ -8,13 +8,14 @@ import { Doc } from '../../fields/Doc'; import { Transform } from "../util/Transform"; import { DocServer } from '../DocServer'; import { undoBatch, UndoManager } from '../util/UndoManager'; -import { NumCast } from '../../fields/Types'; +import { NumCast, Cast } from '../../fields/Types'; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; import * as rp from 'request-promise'; import { Utils } from '../../Utils'; import { Networking } from '../Network'; import { Upload } from '../../server/SharedMediaTypes'; import { basename } from 'path'; +import { CurrentUserUtils } from '../util/CurrentUserUtils'; @observer export class PreviewCursor extends React.Component<{}> { @@ -22,7 +23,7 @@ export class PreviewCursor extends React.Component<{}> { static _getTransform: () => Transform; static _addDocument: (doc: Doc | Doc[]) => void; static _addLiveTextDoc: (doc: Doc) => void; - static _nudge: (x: number, y: number) => boolean; + static _nudge?: undefined | ((x: number, y: number) => boolean); @observable static _clickPoint = [0, 0]; @observable public static Visible = false; constructor(props: any) { @@ -44,8 +45,7 @@ export class PreviewCursor extends React.Component<{}> { if (plain.indexOf("www.youtube.com/watch") !== -1) { const url = plain.replace("youtube.com/watch?v=", "youtube.com/embed/"); undoBatch(() => PreviewCursor._addDocument(Docs.Create.VideoDocument(url, { - title: url, _width: 400, _height: 315, - _nativeWidth: 600, _nativeHeight: 472.5, + title: url, _width: 400, _height: 315, _nativeWidth: 600, _nativeHeight: 472.5, x: newPoint[0], y: newPoint[1] })))(); } @@ -53,49 +53,37 @@ export class PreviewCursor extends React.Component<{}> { else if (re.test(plain)) { const url = plain; undoBatch(() => PreviewCursor._addDocument(Docs.Create.WebDocument(url, { - title: url, _width: 500, _height: 300, UseCors: true, - // nativeWidth: 300, nativeHeight: 472.5, - x: newPoint[0], y: newPoint[1] + title: url, _fitWidth: true, _width: 500, _height: 300, useCors: true, x: newPoint[0], y: newPoint[1] })))(); } - else if (plain.startsWith("__DashDocId(") || plain.startsWith("__DashCloneId(")) { const clone = plain.startsWith("__DashCloneId("); const docids = plain.split(":"); const strs = docids[0].split(","); const ptx = Number(strs[0].substring((clone ? "__DashCloneId(" : "__DashDocId(").length)); const pty = Number(strs[1].substring(0, strs[1].length - 1)); - let count = 1; - const list: Doc[] = []; - let first: Doc | undefined; - docids.map((did, i) => i && DocServer.GetRefField(did).then(async doc => { - count++; - if (doc instanceof Doc) { - i === 1 && (first = doc); - const alias = clone ? (await Doc.MakeClone(doc)).clone : doc; - const deltaX = NumCast(doc.x) - NumCast(first!.x) - ptx; - const deltaY = NumCast(doc.y) - NumCast(first!.y) - pty; - alias.x = newPoint[0] + deltaX; - alias.y = newPoint[1] + deltaY; - list.push(alias); - } - if (count === docids.length) { - undoBatch(() => PreviewCursor._addDocument(list))(); - } - })); + const batch = UndoManager.StartBatch("cloning"); + { + const docs = await Promise.all(docids.filter((did, i) => i).map(async (did) => { + const doc = Cast(await DocServer.GetRefField(did), Doc, null); + return clone ? (await Doc.MakeClone(doc)).clone : doc; + })); + const firstx = docs.length ? NumCast(docs[0].x) + ptx - newPoint[0] : 0; + const firsty = docs.length ? NumCast(docs[0].y) + pty - newPoint[1] : 0; + docs.map(doc => { + doc.x = NumCast(doc.x) - firstx; + doc.y = NumCast(doc.y) - firsty; + }); + PreviewCursor._addDocument(docs); + } + batch.end(); e.stopPropagation(); - } else { + } + else { // creates text document FormattedTextBox.PasteOnLoad = e; - undoBatch(() => PreviewCursor._addLiveTextDoc(Docs.Create.TextDocument("", { - _width: 500, - limitHeight: 400, - _autoHeight: true, - x: newPoint[0], - y: newPoint[1], - title: "-pasted text-" - })))(); + UndoManager.RunInBatch(() => PreviewCursor._addLiveTextDoc(CurrentUserUtils.GetNewTextDoc("-pasted text-", newPoint[0], newPoint[1], 500)), "paste"); } } else //pasting in images @@ -132,6 +120,7 @@ export class PreviewCursor extends React.Component<{}> { e.key !== "Insert" && e.key !== "Home" && e.key !== "End" && e.key !== "PageUp" && e.key !== "PageDown" && e.key !== "NumLock" && e.key !== " " && (e.keyCode < 112 || e.keyCode > 123) && // F1 thru F12 keys + (e.keyCode < 173 || e.keyCode > 183 || e.key === "-") && // mute, volume up/down etc, - is there specifically because its keycode is 173 in Firefox so shouldn't be avoided !e.key.startsWith("Arrow") && !e.defaultPrevented) { if ((!e.metaKey && !e.ctrlKey) || (e.keyCode >= 48 && e.keyCode <= 57) || (e.keyCode >= 65 && e.keyCode <= 90)) {// /^[a-zA-Z0-9$*^%#@+-=_|}{[]"':;?/><.,}]$/.test(e.key)) { @@ -162,7 +151,7 @@ export class PreviewCursor extends React.Component<{}> { addLiveText: (doc: Doc) => void, getTransform: () => Transform, addDocument: (doc: Doc | Doc[]) => boolean, - nudge: (nudgeX: number, nudgeY: number) => boolean) { + nudge: undefined | ((nudgeX: number, nudgeY: number) => boolean)) { this._clickPoint = [x, y]; this._onKeyPress = onKeyPress; this._addLiveTextDoc = addLiveText; diff --git a/src/client/views/PropertiesButtons.scss b/src/client/views/PropertiesButtons.scss index 8d9d56c9e..d63eb874c 100644 --- a/src/client/views/PropertiesButtons.scss +++ b/src/client/views/PropertiesButtons.scss @@ -43,6 +43,13 @@ $linkGap : 3px; cursor: pointer; } } +.propertiesButtons-linkButton-empty.toggle-on { + background-color: white; + color: black; +} +.propertiesButtons-linkButton-empty.toggle-off { + color: white; +} .propertiesButtons { margin-top: 3px; diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx index dfedc9ccc..9de4f9c67 100644 --- a/src/client/views/PropertiesButtons.tsx +++ b/src/client/views/PropertiesButtons.tsx @@ -1,52 +1,29 @@ -import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; -import { faArrowAltCircleDown, faArrowAltCircleRight, faArrowAltCircleUp, faCheckCircle, faCloudUploadAlt, faLink, faPhotoVideo, faShare, faStopCircle, faSyncAlt, faTag, faTimes } from '@fortawesome/free-solid-svg-icons'; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Tooltip } from '@material-ui/core'; import { action, computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DataSym, AclEdit, AclAdmin } from "../../fields/Doc"; +import { Doc } from "../../fields/Doc"; +import { InkField } from '../../fields/InkField'; import { RichTextField } from '../../fields/RichTextField'; -import { Cast, NumCast, BoolCast } from "../../fields/Types"; -import { emptyFunction, setupMoveUpEvents, Utils } from "../../Utils"; -import GoogleAuthenticationManager from '../apis/GoogleAuthenticationManager'; +import { Cast, NumCast } from "../../fields/Types"; +import { ImageField } from '../../fields/URLField'; +import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager'; import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils'; +import { GooglePhotos } from '../apis/google_docs/GooglePhotosClientUtils'; import { Docs, DocUtils } from '../documents/Documents'; -import { DragManager } from '../util/DragManager'; -import { CollectionDockingView, DockedFrameRenderer } from './collections/CollectionDockingView'; -import { ParentDocSelector } from './collections/ParentDocumentSelector'; -import './collections/ParentDocumentSelector.scss'; -import './PropertiesButtons.scss'; -import { MetadataEntryMenu } from './MetadataEntryMenu'; -import { DocumentView } from './nodes/DocumentView'; +import { DocumentType } from '../documents/DocumentTypes'; +import { SelectionManager } from '../util/SelectionManager'; +import { undoBatch } from '../util/UndoManager'; +import { CollectionDockingView } from './collections/CollectionDockingView'; import { GoogleRef } from "./nodes/formattedText/FormattedTextBox"; -import { TemplateMenu } from "./TemplateMenu"; -import { Template, Templates } from "./Templates"; +import './PropertiesButtons.scss'; import React = require("react"); -import { Tooltip } from '@material-ui/core'; -import { SelectionManager } from '../util/SelectionManager'; -import SharingManager from '../util/SharingManager'; -import { GooglePhotos } from '../apis/google_docs/GooglePhotosClientUtils'; -import { ImageField } from '../../fields/URLField'; -import { undoBatch, UndoManager } from '../util/UndoManager'; -import { DocumentType } from '../documents/DocumentTypes'; -import { InkField } from '../../fields/InkField'; -import { PresBox } from './nodes/PresBox'; -import { GetEffectiveAcl } from '../../fields/util'; +import { CollectionViewType } from './collections/CollectionView'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; -library.add(faLink); -library.add(faTag); -library.add(faTimes); -library.add(faArrowAltCircleDown); -library.add(faArrowAltCircleUp); -library.add(faArrowAltCircleRight); -library.add(faStopCircle); -library.add(faCheckCircle); -library.add(faCloudUploadAlt); -library.add(faSyncAlt); -library.add(faShare); -library.add(faPhotoVideo); const cloud: IconProp = "cloud-upload-alt"; const fetch: IconProp = "sync-alt"; @@ -59,7 +36,6 @@ enum UtilityButtonState { @observer export class PropertiesButtons extends React.Component<{}, {}> { - private _dragRef = React.createRef<HTMLDivElement>(); private _pullAnimating = false; private _pushAnimating = false; private _pullColorAnimating = false; @@ -77,13 +53,12 @@ export class PropertiesButtons extends React.Component<{}, {}> { public static hasPulledHack = false; + @computed get selectedDoc() { return SelectionManager.SelectedSchemaDoc() || this.selectedDocumentView?.rootDoc; } @computed get selectedDocumentView() { if (SelectionManager.SelectedDocuments().length) { return SelectionManager.SelectedDocuments()[0]; - } else { return undefined; } + } else return undefined; } - @computed get selectedDoc() { return this.selectedDocumentView?.rootDoc; } - @computed get dataDoc() { return this.selectedDocumentView?.dataDoc; } @computed get onClick() { return this.selectedDoc?.onClickBehavior ? this.selectedDoc?.onClickBehavior : "nothing"; } @@ -168,11 +143,8 @@ export class PropertiesButtons extends React.Component<{}, {}> { <div className="propertiesButtons-linker" style={{ backgroundColor: this.pullColor }} onPointerEnter={action(e => { - if (e.altKey) { - this.openHover = UtilityButtonState.OpenExternally; - } else if (e.shiftKey) { - this.openHover = UtilityButtonState.OpenRight; - } + e.altKey && (this.openHover = UtilityButtonState.OpenExternally); + e.shiftKey && (this.openHover = UtilityButtonState.OpenRight); })} onPointerLeave={action(() => this.openHover = UtilityButtonState.Default)} onClick={async e => { @@ -181,11 +153,11 @@ export class PropertiesButtons extends React.Component<{}, {}> { e.preventDefault(); let googleDoc = await Cast(dataDoc.googleDoc, Doc); if (!googleDoc) { - const options = { _width: 600, _nativeWidth: 960, _nativeHeight: 800, isAnnotating: false, UseCors: false }; + const options = { _width: 600, _fitWidth: true, _nativeWidth: 960, _nativeHeight: 800, isAnnotating: false, useCors: false }; googleDoc = Docs.Create.WebDocument(googleDocUrl, options); dataDoc.googleDoc = googleDoc; } - CollectionDockingView.AddRightSplit(googleDoc); + CollectionDockingView.AddSplit(googleDoc, "right"); } else if (e.altKey) { e.preventDefault(); window.open(googleDocUrl); @@ -196,8 +168,7 @@ export class PropertiesButtons extends React.Component<{}, {}> { dataDoc.unchanged && runInAction(() => this.isAnimatingFetch = true); } }}> - <FontAwesomeIcon className="documentdecorations-icon" size="lg" - color="black" + <FontAwesomeIcon className="documentdecorations-icon" size="lg" color="black" style={{ WebkitAnimation: animation, MozAnimation: animation }} icon={(() => { switch (this.openHover) { @@ -213,248 +184,116 @@ export class PropertiesButtons extends React.Component<{}, {}> { </div> </Tooltip>; } + + @action @undoBatch + onLock = () => { + SelectionManager.SelectedDocuments().forEach(dv => dv.toggleLockPosition()); + } + @computed - get pinButton() { + get lockButton() { const targetDoc = this.selectedDoc; - const isPinned = targetDoc && Doc.isDocPinned(targetDoc); - return !targetDoc ? (null) : <Tooltip title={<div className="dash-tooltip">{Doc.isDocPinned(targetDoc) ? "Unpin from presentation" : "Pin to presentation"}</div>} placement="top"> + return !targetDoc ? (null) : <Tooltip + title={<div className="dash-tooltip">{`${this.selectedDoc?.lockedPosition ? "Unlock" : "Lock"} Position`}</div>} placement="top"> <div> - <div className="propertiesButtons-linker" - style={{ backgroundColor: isPinned ? "white" : "", color: isPinned ? "black" : "white" }} - onClick={e => DockedFrameRenderer.PinDoc(targetDoc, isPinned)}> - <FontAwesomeIcon className="documentdecorations-icon" size="lg" icon="map-pin" /> + <div className={`propertiesButtons-linkButton-empty toggle-${targetDoc.lockedPosition ? "on" : "off"}`} onPointerDown={this.onLock} > + <FontAwesomeIcon className="documentdecorations-icon" size="lg" + color={this.selectedDoc?.lockedPosition ? "black" : "white"} + icon={this.selectedDoc?.lockedPosition ? "unlock" : "lock"} /> </div> - <div className="propertiesButtons-title" - // style={{ - // backgroundColor: Doc.isDocPinned(targetDoc) ? "white" : "black", - // color: Doc.isDocPinned(targetDoc) ? "black" : "white" - // }} - >{Doc.isDocPinned(targetDoc) ? "Unpin" : "Pin"}</div> + >Position </div> </div> </Tooltip>; } @computed - get pinWithViewButton() { + get downloadButton() { const targetDoc = this.selectedDoc; - if (targetDoc) { - const x = targetDoc._panX; - const y = targetDoc._panY; - const scale = targetDoc._viewScale; - } - return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{"Pin with this view"}</div></>} placement="top"> + return !targetDoc ? (null) : <Tooltip + title={<div className="dash-tooltip">{"Download Document"}</div>} placement="top"> <div> - <div className="propertiesButtons-linker" - onClick={e => { - if (targetDoc) { - DockedFrameRenderer.PinDoc(targetDoc, false); - const activeDoc = PresBox.Instance.childDocs[PresBox.Instance.childDocs.length - 1]; - const x = targetDoc._panX; - const y = targetDoc._panY; - const scale = targetDoc._viewScale; - activeDoc.presPinView = true; - activeDoc.presPinViewX = x; - activeDoc.presPinViewY = y; - activeDoc.presPinViewScale = scale; - } - }}> - <FontAwesomeIcon className="documentdecorations-icon" size="lg" icon="map-pin" /> - <div style={{ position: 'relative', fontSize: 25, fontWeight: 700, transform: 'translate(0, -28px)', color: 'rgba(250,250,250,0.55)' }}>V</div> + <div className={"propertiesButtons-linkButton-empty"} onPointerDown={() => this.selectedDoc && Doc.Zip(this.selectedDoc)}> + <FontAwesomeIcon className="propertiesButtons-icon" icon="download" size="lg" /> </div> - - <div className="propertiesButtons-title">{"View"}</div> + <div className="propertiesButtons-title"> downld </div> </div> </Tooltip>; } - - @computed - get metadataButton() { - //const view0 = this.view0; - if (this.selectedDoc) { - return <Tooltip title={<><div className="dash-tooltip">Show metadata panel</div></>} placement="top"> - <div className="propertiesButtons-linkFlyout"> - <Flyout anchorPoint={anchorPoints.LEFT_TOP} - content={<MetadataEntryMenu docs={[this.selectedDoc]} suggestWithFunction /> /* tfs: @bcz This might need to be the data document? */}> - <div> - <div className={"propertiesButtons-linkButton-" + "empty"} onPointerDown={e => e.stopPropagation()} > - {<FontAwesomeIcon className="documentdecorations-icon" icon="tag" size="lg" />} - </div> - <div className="propertiesButtons-title">Metadata</div> - </div> - </Flyout> - </div></Tooltip>; - } else { - return null; - } - - } - - @observable _aliasDown = false; - onAliasButtonDown = (e: React.PointerEvent): void => { - setupMoveUpEvents(this, e, this.onAliasButtonMoved, emptyFunction, emptyFunction); - } @undoBatch - onAliasButtonMoved = () => { - if (this._dragRef.current) { - const dragDocView = this.selectedDocumentView!; - const dragData = new DragManager.DocumentDragData([dragDocView.props.Document]); - const [left, top] = dragDocView.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); - dragData.dropAction = "alias"; - DragManager.StartDocumentDrag([dragDocView.ContentDiv!], dragData, left, top, { - offsetX: dragData.offset[0], - offsetY: dragData.offset[1], - hideSource: false - }); - return true; - } - return false; - } - - @computed - get templateButton() { - const docView = this.selectedDocumentView; - const templates: Map<Template, boolean> = new Map(); - const views = [this.selectedDocumentView]; - Array.from(Object.values(Templates.TemplateList)).map(template => - templates.set(template, views.reduce((checked, doc) => checked || doc?.props.Document["_show" + template.Name] ? true : false, false as boolean))); - return !docView ? (null) : - <Tooltip title={<><div className="dash-tooltip">Customize layout</div></>} placement="top"> - <div className="propertiesButtons-linkFlyout"> - <Flyout anchorPoint={anchorPoints.LEFT_TOP} //onOpen={action(() => this._aliasDown = true)} onClose={action(() => this._aliasDown = false)} - content={<TemplateMenu docViews={views.filter(v => v).map(v => v as DocumentView)} templates={templates} />}> - <div> - <div className={"propertiesButtons-linkButton-empty"} > - {<FontAwesomeIcon className="documentdecorations-icon" icon="edit" size="lg" />} - </div> - <div className="propertiesButtons-title">Layout</div> - </div> - </Flyout> - </div></Tooltip>; - } - - @undoBatch - onCopy = () => { - if (this.selectedDoc && this.selectedDocumentView) { - // const copy = Doc.MakeCopy(this.selectedDocumentView.props.Document, true); - // copy.x = NumCast(this.selectedDoc.x) + NumCast(this.selectedDoc._width); - // copy.y = NumCast(this.selectedDoc.y) + 30; - // this.selectedDocumentView.props.addDocument?.(copy); - const alias = Doc.MakeAlias(this.selectedDoc); - alias.x = NumCast(this.selectedDoc.x) + NumCast(this.selectedDoc._width); - alias.y = NumCast(this.selectedDoc.y) + 30; - this.selectedDocumentView.props.addDocument?.(alias); - } + @action + setDictation = () => { + SelectionManager.SelectedDocuments().forEach(dv => dv.rootDoc._showAudio = dv.rootDoc._showAudio === !dv.rootDoc._showAudio); } @computed - get copyButton() { + get dictationButton() { const targetDoc = this.selectedDoc; - return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{"Tap or Drag to create an alias"}</div></>} placement="top"> + return !targetDoc ? (null) : <Tooltip title={<div className="dash-tooltip">{"Show Dictation Controls"}</div>} placement="top"> <div> - <div className={"propertiesButtons-linkButton-empty"} - ref={this._dragRef} - onPointerDown={this.onAliasButtonDown} - onClick={this.onCopy}> - {<FontAwesomeIcon className="documentdecorations-icon" icon="copy" size="lg" />} + <div className={`propertiesButtons-linkButton-empty toggle-${targetDoc._showAudio ? "on" : "off"}`} onPointerDown={this.setDictation}> + <FontAwesomeIcon className="propertiesButtons-icon" icon="microphone" size="lg" /> </div> - <div className="propertiesButtons-title">Alias</div> + <div className="propertiesButtons-title"> Dictate </div> </div> </Tooltip>; } - @action @undoBatch - onLock = () => { - this.selectedDocumentView?.toggleLockPosition(); + + @undoBatch + @action + setTitle = () => { + SelectionManager.SelectedDocuments().forEach(dv => dv.rootDoc._showTitle = dv.rootDoc._showTitle === undefined ? "title" : undefined); } @computed - get lockButton() { + get titleButton() { const targetDoc = this.selectedDoc; - return !targetDoc ? (null) : <Tooltip - title={<><div className="dash-tooltip">{this.selectedDoc?.lockedPosition ? - "Unlock Position" : "Lock Position"}</div></>} placement="top"> + return !targetDoc ? (null) : <Tooltip title={<div className="dash-tooltip">{"Show Title Header"}</div>} placement="top"> <div> - <div className={"propertiesButtons-linkButton-empty"} - style={{ backgroundColor: BoolCast(this.selectedDoc?.lockedPosition) ? "white" : "" }} - onPointerDown={this.onLock} > - {<FontAwesomeIcon className="documentdecorations-icon" - color={BoolCast(this.selectedDoc?.lockedPosition) ? "black" : "white"} - icon={BoolCast(this.selectedDoc?.lockedPosition) ? "unlock" : "lock"} size="lg" />} + <div className={`propertiesButtons-linkButton-empty toggle-${targetDoc._showTitle ? "on" : "off"}`} onPointerDown={this.setTitle}> + <FontAwesomeIcon className="propertiesButtons-icon" icon="text-width" size="lg" /> </div> - <div className="propertiesButtons-title" - // style={{ - // backgroundColor: BoolCast(this.selectedDoc?.lockedPosition) ? "white" : "black", - // color: BoolCast(this.selectedDoc?.lockedPosition) ? "black" : "white" - // }} - >Position </div> + <div className="propertiesButtons-title"> Title </div> </div> </Tooltip>; } - @computed - get downloadButton() { - const targetDoc = this.selectedDoc; - return !targetDoc ? (null) : <Tooltip - title={<><div className="dash-tooltip">{"Download Document"}</div></>} placement="top"> - <div> - <div className={"propertiesButtons-linkButton-empty"} - onPointerDown={async () => { - if (this.selectedDocumentView?.props.Document) { - Doc.Zip(this.selectedDocumentView?.props.Document); - } - }}> - {<FontAwesomeIcon className="propertiesButtons-icon" - icon="download" size="lg" />} - </div> - <div className="propertiesButtons-title"> downld </div> - </div> - </Tooltip>; + @undoBatch + @action + setCaption = () => { + SelectionManager.SelectedDocuments().forEach(dv => dv.rootDoc._showCaption = dv.rootDoc._showCaption === undefined ? "caption" : undefined); } @computed - get deleteButton() { + get captionButton() { const targetDoc = this.selectedDoc; - return !targetDoc ? (null) : <Tooltip - title={<><div className="dash-tooltip">Close Document</div></>} placement="top"> + return !targetDoc ? (null) : <Tooltip title={<div className="dash-tooltip">{"Show Caption Footer"}</div>} placement="top"> <div> - <div className={"propertiesButtons-linkButton-empty"} - onPointerDown={this.deleteDocument}> - {<FontAwesomeIcon className="propertiesButtons-icon" - icon="times" size="lg" />} + <div className={`propertiesButtons-linkButton-empty toggle-${targetDoc._showCaption ? "on" : "off"}`} onPointerDown={this.setCaption}> + <FontAwesomeIcon className="propertiesButtons-icon" icon="closed-captioning" size="lg" /> </div> - <div className="propertiesButtons-title"> close </div> + <div className="propertiesButtons-title"> Caption </div> </div> </Tooltip>; } @undoBatch @action - deleteDocument = () => { - const selected = SelectionManager.SelectedDocuments().slice(); - selected.map(dv => dv.props.removeDocument?.(dv.props.Document)); - this.selectedDoc && (this.selectedDoc.deleted = true); - this.selectedDocumentView?.props.ContainingCollectionView?.removeDocument(this.selectedDocumentView?.props.Document); - SelectionManager.DeselectAll(); + setChrome = () => { + SelectionManager.SelectedDocuments().forEach(dv => dv.rootDoc._chromeStatus = dv.rootDoc._chromeStatus === "disabled" ? "enabled" : "disabled"); } @computed - get sharingButton() { + get chromeButton() { const targetDoc = this.selectedDoc; - return !targetDoc ? (null) : <Tooltip - title={<><div className="dash-tooltip">{"Share Document"}</div></>} placement="top"> + return !targetDoc ? (null) : <Tooltip title={<div className="dash-tooltip">{"Show Editing UI"}</div>} placement="top"> <div> - <div className={"propertiesButtons-linkButton-empty"} - onPointerDown={() => { - if (this.selectedDocumentView) { - SharingManager.Instance.open(this.selectedDocumentView); - } - }}> - {<FontAwesomeIcon className="propertiesButtons-icon" - icon="users" size="lg" />} + <div className={`propertiesButtons-linkButton-empty toggle-${targetDoc._chromeStatus === "enabled" ? "on" : "off"}`} onPointerDown={this.setChrome}> + <FontAwesomeIcon className="propertiesButtons-icon" icon="edit" size="lg" /> </div> - <div className="propertiesButtons-title"> share </div> + <div className="propertiesButtons-title"> Controls </div> </div> </Tooltip>; } @@ -485,27 +324,31 @@ export class PropertiesButtons extends React.Component<{}, {}> { handleOptionChange = (e: any) => { const value = e.target.value; this.selectedDoc && (this.selectedDoc.onClickBehavior = e.target.value); - if (value === "nothing") { - this.selectedDocumentView?.noOnClick(); - } else if (value === "enterPortal") { - this.selectedDocumentView?.noOnClick(); - this.selectedDocumentView?.makeIntoPortal(); - } else if (value === "toggleDetail") { - this.selectedDocumentView?.noOnClick(); - this.selectedDocumentView?.toggleDetail(); - } else if (value === "linkInPlace") { - this.selectedDocumentView?.noOnClick(); - this.selectedDocumentView?.toggleFollowLink("inPlace", true, false); - } else if (value === "linkOnRight") { - this.selectedDocumentView?.noOnClick(); - this.selectedDocumentView?.toggleFollowLink("onRight", false, false); - } + + SelectionManager.SelectedDocuments().forEach(dv => { + if (value === "nothing") { + dv.noOnClick(); + } else if (value === "enterPortal") { + dv.noOnClick(); + dv.makeIntoPortal(); + } else if (value === "toggleDetail") { + dv.noOnClick(); + dv.toggleDetail(); + } else if (value === "linkInPlace") { + dv.noOnClick(); + dv.toggleFollowLink("inPlace", true, false); + } else if (value === "linkOnRight") { + dv.noOnClick(); + dv.toggleFollowLink("add:right", false, false); + } + }); } @undoBatch @action editOnClickScript = () => { if (this.selectedDoc) { - DocUtils.makeCustomViewClicked(this.selectedDoc, undefined, "onClick"); + if (SelectionManager.SelectedDocuments().length) SelectionManager.SelectedDocuments().forEach(dv => DocUtils.makeCustomViewClicked(dv.rootDoc, undefined, "onClick")); + else DocUtils.makeCustomViewClicked(this.selectedDoc, undefined, "onClick"); } } @@ -553,7 +396,7 @@ export class PropertiesButtons extends React.Component<{}, {}> { </label> </div> </form> - <div onPointerDown={this.editOnClickScript} className="onClickFlyout-editScript"> Edit onClick Script</div> + {Doc.UserDoc().noviceMode ? (null) : <div onPointerDown={this.editOnClickScript} className="onClickFlyout-editScript"> Edit onClick Script</div>} </div>; } @@ -564,13 +407,8 @@ export class PropertiesButtons extends React.Component<{}, {}> { title={<><div className="dash-tooltip">{"Export to Google Photos"}</div></>} placement="top"> <div> <div className={"propertiesButtons-linkButton-empty"} - onPointerDown={() => { - if (this.selectedDocumentView) { - GooglePhotos.Export.CollectionToAlbum({ collection: this.selectedDocumentView.Document }).then(console.log); - } - }}> - {<FontAwesomeIcon className="documentdecorations-icon" - icon="cloud-upload-alt" size="lg" />} + onPointerDown={() => this.selectedDoc && GooglePhotos.Export.CollectionToAlbum({ collection: this.selectedDoc }).then(console.log)}> + {<FontAwesomeIcon className="documentdecorations-icon" icon="cloud-upload-alt" size="lg" />} </div> <div className="propertiesButtons-title"> google </div> </div> @@ -581,33 +419,30 @@ export class PropertiesButtons extends React.Component<{}, {}> { get clustersButton() { const targetDoc = this.selectedDoc; return !targetDoc ? (null) : <Tooltip - title={<><div className="dash-tooltip">{this.selectedDoc?.useClusters ? "Stop Showing Clusters" : "Show Clusters"}</div></>} placement="top"> + title={<><div className="dash-tooltip">{this.selectedDoc?._useClusters ? "Stop Showing Clusters" : "Show Clusters"}</div></>} placement="top"> <div> - <div className={"propertiesButtons-linkButton-empty"} - style={{ backgroundColor: this.selectedDoc?.useClusters ? "white" : "" }} - onPointerDown={this.changeClusters}> - {<FontAwesomeIcon className="documentdecorations-icon" - color={this.selectedDoc?.useClusters ? "black" : "white"} - icon="braille" size="lg" />} + <div className={`propertiesButtons-linkButton-empty toggle-${targetDoc._useClusters ? "on" : "off"}`} onPointerDown={this.changeClusters}> + <FontAwesomeIcon className="documentdecorations-icon" icon="braille" size="lg" /> </div> - <div className="propertiesButtons-title" - // style={{ - // backgroundColor: this.selectedDoc?.useClusters ? "white" : "black", - // color: this.selectedDoc?.useClusters ? "black" : "white" - // }} - > clusters </div> + <div className="propertiesButtons-title" > clusters </div> </div> </Tooltip>; } @action @undoBatch changeFitToBox = () => { - this.selectedDoc && (this.selectedDoc._fitToBox = !this.selectedDoc._fitToBox); + if (this.selectedDoc) { + if (SelectionManager.SelectedDocuments().length) SelectionManager.SelectedDocuments().forEach(dv => dv.rootDoc._fitToBox = !dv.rootDoc._fitToBox); + else this.selectedDoc._fitToBox = !this.selectedDoc._fitToBox; + } } @action @undoBatch changeClusters = () => { - this.selectedDoc && (this.selectedDoc.useClusters = !this.selectedDoc.useClusters); + if (this.selectedDoc) { + if (SelectionManager.SelectedDocuments().length) SelectionManager.SelectedDocuments().forEach(dv => dv.rootDoc._useClusters = !dv.rootDoc._useClusters); + else this.selectedDoc._useClusters = !this.selectedDoc._useClusters; + } } @computed @@ -616,19 +451,10 @@ export class PropertiesButtons extends React.Component<{}, {}> { return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{this.selectedDoc?._fitToBox ? "Stop Fitting Content" : "Fit Content"}</div></>} placement="top"> <div> - <div className={"propertiesButtons-linkButton-empty"} - style={{ backgroundColor: this.selectedDoc?._fitToBox ? "white" : "" }} - onPointerDown={this.changeFitToBox}> - {<FontAwesomeIcon className="documentdecorations-icon" - color={this.selectedDoc?._fitToBox ? "black" : "white"} - icon="expand" size="lg" />} + <div className={`propertiesButtons-linkButton-empty toggle-${targetDoc._fitToBox ? "on" : "off"}`} onPointerDown={this.changeFitToBox}> + <FontAwesomeIcon className="documentdecorations-icon" icon="expand" size="lg" /> </div> - <div className="propertiesButtons-title" - // style={{ - // backgroundColor: this.selectedDoc?._fitToBox ? "white" : "black", - // color: this.selectedDoc?._fitToBox ? "black" : "white" - // }} - > {this.selectedDoc?._fitToBox ? "unfit" : "fit"} </div> + <div className="propertiesButtons-title"> {this.selectedDoc?._fitToBox ? "unfit" : "fit"} </div> </div> </Tooltip>; } @@ -651,104 +477,45 @@ export class PropertiesButtons extends React.Component<{}, {}> { return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">Make Mask</div></>} placement="top"> <div> - <div className={"propertiesButtons-linkButton-empty"} - onPointerDown={this.makeMask}> - {<FontAwesomeIcon className="documentdecorations-icon" - color="white" icon="paint-brush" size="lg" />} + <div className={"propertiesButtons-linkButton-empty"} onPointerDown={this.makeMask}> + <FontAwesomeIcon className="documentdecorations-icon" color="white" icon="paint-brush" size="lg" /> </div> <div className="propertiesButtons-title"> mask </div> </div> </Tooltip>; } - @computed - get contextButton() { - if (this.selectedDoc) { - return <Tooltip title={<><div className="dash-tooltip">Show Context</div></>} placement="top"> - <div> - <div className={"propertiesButtons-linkButton-empty"}> - <ParentDocSelector Document={this.selectedDoc} addDocTab={(doc, where) => { - where === "onRight" ? CollectionDockingView.AddRightSplit(doc) : - this.selectedDocumentView?.props.addDocTab(doc, "onRight"); - return true; - }} /> - </div> - <div className="propertiesButtons-title"> context </div> - </div> - </Tooltip>; - } else { - return false; - } - - } - - // @computed - // get importButton() { - // const targetDoc = this.selectedDoc; - // return !targetDoc ? (null) : <Tooltip - // title={<><div className="dash-tooltip">{"Import a Document"}</div></>}> - // <div className={"propertiesButtons-linkButton-empty"} - // onPointerDown={() => { - // if (this.selectedDocumentView) { - // CollectionFreeFormView.importDocument(100, 100); - // } - // }}> - // {<FontAwesomeIcon className="documentdecorations-icon" - // icon="upload" size="sm" />} - // </div> - // </Tooltip>; - // } - - render() { if (!this.selectedDoc) return (null); - const isText = this.selectedDoc[Doc.LayoutFieldKey(this.selectedDoc)] instanceof RichTextField; + const layoutField = this.selectedDoc[Doc.LayoutFieldKey(this.selectedDoc)]; + const isText = layoutField instanceof RichTextField; + const isImage = layoutField instanceof ImageField; + const isInk = layoutField instanceof InkField; + const isCollection = this.selectedDoc.type === DocumentType.COL; + const isFreeForm = this.selectedDoc._viewType === CollectionViewType.Freeform; const considerPull = isText && this.considerGoogleDocsPull; const considerPush = isText && this.considerGoogleDocsPush; - const isImage = this.selectedDoc[Doc.LayoutFieldKey(this.selectedDoc)] instanceof ImageField; - const isInk = this.selectedDoc[Doc.LayoutFieldKey(this.selectedDoc)] instanceof InkField; - const isCollection = this.selectedDoc.type === DocumentType.COL ? true : false; - const isFreeForm = this.selectedDoc._viewType === "freeform" ? true : false; - const hasContext = this.selectedDoc.context ? true : false; - const collectionAcl = GetEffectiveAcl(this.selectedDocumentView?.props.ContainingCollectionDoc?.[DataSym]); - - return <div><div className="propertiesButtons" style={{ paddingBottom: "5.5px" }}> - <div className="propertiesButtons-button"> - {this.templateButton} - </div> - {/* <div className="propertiesButtons-button"> - {this.metadataButton} - </div> */} + + return <div className="propertiesButtons" style={{ paddingBottom: "5.5px" }}> <div className="propertiesButtons-button"> - {this.pinButton} + {this.titleButton} </div> <div className="propertiesButtons-button"> - {this.pinWithViewButton} + {this.captionButton} </div> - <div className="propertiesButtons-button" style={{ display: hasContext ? "" : "none" }}> - {this.copyButton} + <div className="propertiesButtons-button" style={{ display: isCollection ? "" : "none" }}> + {this.chromeButton} </div> <div className="propertiesButtons-button"> {this.lockButton} </div> - <div className="propertiesButtons-button"> - {this.downloadButton} + <div className="propertiesButtons-button" style={{ display: isText || isImage ? "" : "none" }}> + {this.dictationButton} </div> - {collectionAcl === AclAdmin || collectionAcl === AclEdit ? - <div className="propertiesButtons-button"> - {this.deleteButton} - </div> - : (null)} <div className="propertiesButtons-button"> {this.onClickButton} </div> - <div className="propertiesButtons-button"> - {this.sharingButton} - </div> - <div className="propertiesButtons-button"> - {this.contextButton} - </div> <div className="propertiesButtons-button" style={{ display: !considerPush ? "none" : "" }}> {this.considerGoogleDocsPush} </div> @@ -758,22 +525,16 @@ export class PropertiesButtons extends React.Component<{}, {}> { <div className="propertiesButtons-button" style={{ display: !isImage ? "none" : "" }}> {this.googlePhotosButton} </div> - {/* <div className="propertiesButtons-button" style={{ display: !isCollection ? "none" : "" }}> - {this.importButton} - </div> */} <div className="propertiesButtons-button" style={{ display: !isFreeForm ? "none" : "" }}> {this.clustersButton} </div> - - <div className="propertiesButtons-button" style={{ display: !isFreeForm ? "none" : "" }}> + <div className="propertiesButtons-button" style={{ display: !isFreeForm && !isText ? "none" : "" }}> {this.fitContentButton} </div> - <div className="propertiesButtons-button" style={{ display: !isInk ? "none" : "" }}> {this.maskButton} </div> - </div> </div>; } } diff --git a/src/client/views/PropertiesDocContextSelector.scss b/src/client/views/PropertiesDocContextSelector.scss new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/src/client/views/PropertiesDocContextSelector.scss diff --git a/src/client/views/PropertiesDocContextSelector.tsx b/src/client/views/PropertiesDocContextSelector.tsx new file mode 100644 index 000000000..427748fe7 --- /dev/null +++ b/src/client/views/PropertiesDocContextSelector.tsx @@ -0,0 +1,58 @@ +import { IReactionDisposer, observable, reaction, runInAction } from "mobx"; +import { observer } from "mobx-react"; +import * as React from "react"; +import { Doc } from "../../fields/Doc"; +import { Id } from "../../fields/FieldSymbols"; +import { NumCast, StrCast } from "../../fields/Types"; +import { CollectionViewType } from "./collections/CollectionView"; +import { CollectionDockingView } from "./collections/CollectionDockingView"; +import './PropertiesDocContextSelector.scss'; +import { SearchUtil } from "../util/SearchUtil"; + +type PropertiesDocContextSelectorProps = { + Document: Doc, + Stack?: any, + hideTitle?: boolean, + addDocTab(doc: Doc, location: string): void +}; + +@observer +export class PropertiesDocContextSelector extends React.Component<PropertiesDocContextSelectorProps> { + @observable private _docs: { col: Doc, target: Doc }[] = []; + @observable private _otherDocs: { col: Doc, target: Doc }[] = []; + _reaction: IReactionDisposer | undefined; + + componentDidMount() { this._reaction = reaction(() => this.props.Document, () => this.fetchDocuments(), { fireImmediately: true }); } + componentWillUnmount() { this._reaction?.(); } + async fetchDocuments() { + const aliases = await SearchUtil.GetAliasesOfDocument(this.props.Document); + const containerProtoSets = await Promise.all(aliases.map(async alias => ((await SearchUtil.Search("", true, { fq: `data_l:"${alias[Id]}"` })).docs))); + const containerProtos = containerProtoSets.reduce((p, set) => { set.map(s => p.add(s)); return p; }, new Set<Doc>()); + const containerSets = await Promise.all(Array.from(containerProtos.keys()).map(async container => SearchUtil.GetAliasesOfDocument(container))); + const containers = containerSets.reduce((p, set) => { set.map(s => p.add(s)); return p; }, new Set<Doc>()); + const doclayoutSets = await Promise.all(Array.from(containers.keys()).map(async (dp) => SearchUtil.GetAliasesOfDocument(dp))); + const doclayouts = Array.from(doclayoutSets.reduce((p, set) => { set.map(s => p.add(s)); return p; }, new Set<Doc>()).keys()); + runInAction(() => { + this._docs = doclayouts.filter(doc => !Doc.AreProtosEqual(doc, CollectionDockingView.Instance.props.Document)).filter(doc => !Doc.IsSystem(doc)).map(doc => ({ col: doc, target: this.props.Document })); + this._otherDocs = []; + }); + } + + getOnClick = (col: Doc, target: Doc) => { + col = Doc.IsPrototype(col) ? Doc.MakeDelegate(col) : col; + if (col._viewType === CollectionViewType.Freeform) { + col._panX = NumCast(target.x) + NumCast(target._width) / 2; + col._panY = NumCast(target.y) + NumCast(target._height) / 2; + } + this.props.addDocTab(col, "add:right"); + } + + render() { + return <div> + {this.props.hideTitle ? (null) : <p key="contexts">Contexts:</p>} + {this._docs.map(doc => <p key={doc.col[Id] + doc.target[Id]}><a onClick={() => this.getOnClick(doc.col, doc.target)}>{StrCast(doc.col.title)}</a></p>)} + {this._otherDocs.length ? <hr key="hr" /> : null} + {this._otherDocs.map(doc => <p key={"p" + doc.col[Id] + doc.target[Id]}><a onClick={() => this.getOnClick(doc.col, doc.target)}>{StrCast(doc.col.title)}</a></p>)} + </div>; + } +}
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/PropertiesView.scss b/src/client/views/PropertiesView.scss index 535581f2e..9fdc8bc47 100644 --- a/src/client/views/collections/collectionFreeForm/PropertiesView.scss +++ b/src/client/views/PropertiesView.scss @@ -6,7 +6,7 @@ cursor: auto; overflow-x: hidden; - overflow-y: scroll; + overflow-y: auto; .propertiesView-title { background-color: rgb(159, 159, 159); @@ -47,7 +47,7 @@ } .propertiesView-settings { - border-bottom: 1px solid black; + //border-bottom: 1px solid black; //padding: 8.5px; font-size: 12.5px; font-weight: bold; @@ -87,7 +87,7 @@ } .propertiesView-sharing { - border-bottom: 1px solid black; + //border-bottom: 1px solid black; //padding: 8.5px; .propertiesView-sharing-title { @@ -121,19 +121,30 @@ padding: 10px; margin-left: 5px; - .propertiesView-acls-checkbox { + .propertiesView-buttonContainer { float: right; - height: 20px; - margin-top: -20px; - margin-right: -15; - - .propertiesView-acls-checkbox-text { - font-size: 7px; - margin-top: -10px; - margin-left: 6px; + display: flex; + + button { + width: 15; + height: 15; + padding: 0; + margin-top: -5; + } + + .propertiesView-acls-checkbox { + margin-top: -20px; + + .propertiesView-acls-checkbox-text { + font-size: 7px; + margin-top: -10px; + margin-left: 6px; + } } } + + .change-buttons { display: flex; @@ -150,7 +161,7 @@ } .propertiesView-appearance { - border-bottom: 1px solid black; + //border-bottom: 1px solid black; //padding: 8.5px; .propertiesView-appearance-title { @@ -187,7 +198,7 @@ } .propertiesView-transform { - border-bottom: 1px solid black; + //border-bottom: 1px solid black; //padding: 8.5px; .propertiesView-transform-title { @@ -246,15 +257,19 @@ } } - .expansion-button-icon { - width: 11px; - height: 11px; - color: black; - margin-left: 27px; + .expansion-button { + margin-left: -20; - &:hover { - color: rgb(131, 131, 131); - cursor: pointer; + .expansion-button-icon { + width: 11px; + height: 11px; + color: black; + margin-left: 27px; + + &:hover { + color: rgb(131, 131, 131); + cursor: pointer; + } } } @@ -271,7 +286,7 @@ // display: inline-table; background-color: #ececec; max-height: 130px; - overflow-y: scroll; + overflow-y: auto; width: 92%; .propertiesView-sharingTable-item { @@ -303,12 +318,11 @@ margin-left: auto; .permissions-select { - z-index: 1; border: none; background-color: inherit; - width: 75px; - //text-align: justify; // for Edge - //text-align-last: end; + width: 87px; + text-align: justify; // for Edge + text-align-last: end; &:hover { cursor: pointer; @@ -323,7 +337,7 @@ } .propertiesView-fields { - border-bottom: 1px solid black; + //border-bottom: 1px solid black; //padding: 8.5px; .propertiesView-fields-title { @@ -396,6 +410,41 @@ } } + .propertiesView-contexts { + + .propertiesView-contexts-title { + font-weight: bold; + font-size: 12.5px; + padding: 4px; + display: flex; + color: white; + padding-left: 8px; + background-color: rgb(51, 51, 51); + + &:hover { + cursor: pointer; + } + + .propertiesView-contexts-title-icon { + float: right; + justify-items: right; + align-items: flex-end; + margin-left: auto; + margin-right: 9px; + + &:hover { + cursor: pointer; + } + } + } + + .propertiesView-contexts-content { + overflow: hidden; + padding: 10px; + } + + } + .propertiesView-layout { .propertiesView-layout-title { @@ -432,7 +481,7 @@ } .propertiesView-presTrails { - border-bottom: 1px solid black; + //border-bottom: 1px solid black; //padding: 8.5px; .propertiesView-presTrails-title { @@ -731,7 +780,9 @@ border: none; padding: 6px; padding-bottom: 2px; - + background: #eeeeee; + border-top: 1px solid; + border-left: 1px solid; &:hover { border: 0.75px solid rgb(122, 28, 28); diff --git a/src/client/views/collections/collectionFreeForm/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index ed451beab..5cab7726e 100644 --- a/src/client/views/collections/collectionFreeForm/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -1,46 +1,40 @@ import React = require("react"); -import { observer } from "mobx-react"; -import "./PropertiesView.scss"; -import { observable, action, computed, runInAction } from "mobx"; -import { Doc, Field, WidthSym, HeightSym, AclSym, AclPrivate, AclReadonly, AclAddonly, AclEdit, AclAdmin, Opt, DocCastAsync, DataSym } from "../../../../fields/Doc"; -import { ComputedField } from "../../../../fields/ScriptField"; -import { EditableView } from "../../EditableView"; -import { KeyValueBox } from "../../nodes/KeyValueBox"; -import { Cast, NumCast, StrCast } from "../../../../fields/Types"; -import { ContentFittingDocumentView } from "../../nodes/ContentFittingDocumentView"; -import { returnFalse, returnOne, emptyFunction, emptyPath, returnTrue, returnZero, returnEmptyFilter, Utils } from "../../../../Utils"; -import { Id } from "../../../../fields/FieldSymbols"; -import { Transform } from "../../../util/Transform"; -import { PropertiesButtons } from "../../PropertiesButtons"; -import { SelectionManager } from "../../../util/SelectionManager"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Tooltip, Checkbox } from "@material-ui/core"; -import SharingManager from "../../../util/SharingManager"; -import { DocumentType } from "../../../documents/DocumentTypes"; -import { SharingPermissions, GetEffectiveAcl } from "../../../../fields/util"; -import { InkField } from "../../../../fields/InkField"; -import { undoBatch, UndoManager } from "../../../util/UndoManager"; +import { Checkbox, Tooltip } from "@material-ui/core"; +import { action, computed, observable } from "mobx"; +import { observer } from "mobx-react"; import { ColorState, SketchPicker } from "react-color"; -import "./FormatShapePane.scss"; -import { PresBox } from "../../nodes/PresBox"; -import { DocumentManager } from "../../../util/DocumentManager"; -import FormatShapePane from "./FormatShapePane"; +import { AclAddonly, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, AclUnset, DataSym, Doc, Field, HeightSym, WidthSym } from "../../fields/Doc"; +import { Id } from "../../fields/FieldSymbols"; +import { InkField } from "../../fields/InkField"; +import { ComputedField } from "../../fields/ScriptField"; +import { Cast, NumCast, StrCast } from "../../fields/Types"; +import { GetEffectiveAcl, SharingPermissions } from "../../fields/util"; +import { emptyFunction, emptyPath, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, returnZero } from "../../Utils"; +import { DocumentType } from "../documents/DocumentTypes"; +import { DocumentManager } from "../util/DocumentManager"; +import { SelectionManager } from "../util/SelectionManager"; +import { SharingManager } from "../util/SharingManager"; +import { Transform } from "../util/Transform"; +import { undoBatch, UndoManager } from "../util/UndoManager"; +import { CollectionDockingView } from "./collections/CollectionDockingView"; +import { EditableView } from "./EditableView"; +import { InkStrokeProperties } from "./InkStrokeProperties"; +import { ContentFittingDocumentView } from "./nodes/ContentFittingDocumentView"; +import { KeyValueBox } from "./nodes/KeyValueBox"; +import { PresBox } from "./nodes/PresBox"; +import { PropertiesButtons } from "./PropertiesButtons"; +import { PropertiesDocContextSelector } from "./PropertiesDocContextSelector"; +import "./PropertiesView.scss"; +import { intersection } from "lodash"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; const _global = (window /* browser */ || global /* node */) as any; -// import * as fa from '@fortawesome/free-solid-svg-icons'; -// import { library } from "@fortawesome/fontawesome-svg-core"; - -// library.add(fa.faPlus, fa.faMinus, fa.faCog); - interface PropertiesViewProps { width: number; height: number; - renderDepth: number; - ScreenToLocalTransform: () => Transform; - onDown: (event: any) => void; } @observer @@ -49,26 +43,25 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @computed get MAX_EMBED_HEIGHT() { return 200; } + @computed get selectedDoc() { return SelectionManager.SelectedSchemaDoc() || this.selectedDocumentView?.rootDoc; } @computed get selectedDocumentView() { - if (SelectionManager.SelectedDocuments().length) { - return SelectionManager.SelectedDocuments()[0]; - } else if (PresBox.Instance && PresBox.Instance._selectedArray.length) { - return DocumentManager.Instance.getDocumentView(PresBox.Instance.rootDoc); - } else { return undefined; } + if (SelectionManager.SelectedDocuments().length) return SelectionManager.SelectedDocuments()[0]; + if (PresBox.Instance && PresBox.Instance._selectedArray) return DocumentManager.Instance.getDocumentView(PresBox.Instance.rootDoc); + return undefined; } @computed get isPres(): boolean { if (this.selectedDoc?.type === DocumentType.PRES) return true; return false; } - @computed get selectedDoc() { return this.selectedDocumentView?.rootDoc; } - @computed get dataDoc() { return this.selectedDocumentView?.dataDoc; } + @computed get dataDoc() { return this.selectedDoc?.[DataSym]; } @observable layoutFields: boolean = false; - @observable openActions: boolean = true; + @observable openOptions: boolean = true; @observable openSharing: boolean = true; @observable openFields: boolean = true; @observable openLayout: boolean = true; + @observable openContexts: boolean = true; @observable openAppearance: boolean = true; @observable openTransform: boolean = true; // @observable selectedUser: string = ""; @@ -81,27 +74,17 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @observable openAddSlide: boolean = false; @observable openSlideOptions: boolean = false; - @observable inActions: boolean = false; + @observable inOptions: boolean = false; @observable _controlBtn: boolean = false; @observable _lock: boolean = false; @computed get isInk() { return this.selectedDoc?.type === DocumentType.INK; } - @action rtfWidth = () => { - if (this.selectedDoc) { - return Math.min(this.selectedDoc?.[WidthSym](), this.props.width - 20); - } else { - return 0; - } + return !this.selectedDoc ? 0 : Math.min(this.selectedDoc?.[WidthSym](), this.props.width - 20); } - @action rtfHeight = () => { - if (this.selectedDoc) { - return this.rtfWidth() <= this.selectedDoc?.[WidthSym]() ? Math.min(this.selectedDoc?.[HeightSym](), this.MAX_EMBED_HEIGHT) : this.MAX_EMBED_HEIGHT; - } else { - return 0; - } + return !this.selectedDoc ? 0 : this.rtfWidth() <= this.selectedDoc?.[WidthSym]() ? Math.min(this.selectedDoc?.[HeightSym](), this.MAX_EMBED_HEIGHT) : this.MAX_EMBED_HEIGHT; } @action @@ -136,24 +119,26 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @computed get expandedField() { if (this.dataDoc && this.selectedDoc) { const ids: { [key: string]: string } = {}; - const doc = this.layoutFields ? Doc.Layout(this.selectedDoc) : this.dataDoc; - doc && Object.keys(doc).forEach(key => !(key in ids) && doc[key] !== ComputedField.undefined && (ids[key] = key)); + const docs = SelectionManager.SelectedDocuments().length < 2 ? [this.layoutFields ? Doc.Layout(this.selectedDoc) : this.dataDoc] : + SelectionManager.SelectedDocuments().map(dv => this.layoutFields ? Doc.Layout(dv.layoutDoc) : dv.dataDoc); + docs.forEach(doc => Object.keys(doc).forEach(key => !(key in ids) && doc[key] !== ComputedField.undefined && (ids[key] = key))); const rows: JSX.Element[] = []; for (const key of Object.keys(ids).slice().sort()) { - const contents = doc[key]; + const docvals = new Set<any>(); + docs.forEach(doc => docvals.add(doc[key])); + const contents = Array.from(docvals.keys()).length > 1 ? "-multiple" : docs[0][key]; if (key[0] === "#") { rows.push(<div style={{ display: "flex", overflowY: "visible", marginBottom: "2px" }} key={key}> <span style={{ fontWeight: "bold", whiteSpace: "nowrap" }}>{key}</span> </div>); } else { - let contentElement: (JSX.Element | null)[] | JSX.Element = []; - contentElement = <EditableView key="editableView" + const contentElement = <EditableView key="editableView" contents={contents !== undefined ? Field.toString(contents as Field) : "null"} height={13} fontSize={10} - GetValue={() => Field.toKeyValueString(doc, key)} - SetValue={(value: string) => KeyValueBox.SetField(doc, key, value, true)} + GetValue={() => contents !== undefined ? Field.toString(contents as Field) : "null"} + SetValue={(value: string) => { docs.map(doc => KeyValueBox.SetField(doc, key, value, true)); return true; }} />; rows.push(<div style={{ display: "flex", overflowY: "visible", marginBottom: "-1px" }} key={key}> <span style={{ fontWeight: "bold", whiteSpace: "nowrap" }}>{key + ":"}</span> @@ -176,45 +161,45 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { } @computed get noviceFields() { - if (this.dataDoc && this.selectedDoc) { + if (this.dataDoc) { const ids: { [key: string]: string } = {}; - const doc = this.dataDoc; - doc && Object.keys(doc).forEach(key => !(key in ids) && doc[key] !== ComputedField.undefined && (ids[key] = key)); + const docs = SelectionManager.SelectedDocuments().length < 2 ? [this.dataDoc] : SelectionManager.SelectedDocuments().map(dv => dv.dataDoc); + docs.forEach(doc => Object.keys(doc).forEach(key => !(key in ids) && doc[key] !== ComputedField.undefined && (ids[key] = key))); const rows: JSX.Element[] = []; - for (const key of Object.keys(ids).slice().sort()) { - if ((key[0] === key[0].toUpperCase() && key.substring(0, 3) !== "ACL" && key !== "UseCors") - || key[0] === "#" || key === "author" || - key === "creationDate" || key.indexOf("lastModified") !== -1) { - - const contents = doc[key]; - if (key[0] === "#") { + const noviceReqFields = ["author", "creationDate", "tags"]; + const noviceLayoutFields = ["_curPage"]; + const noviceKeys = [...Array.from(Object.keys(ids)).filter(key => key[0] === "#" || key.indexOf("lastModified") !== -1 || (key[0] === key[0].toUpperCase() && !key.startsWith("acl"))), + ...noviceReqFields, ...noviceLayoutFields]; + for (const key of noviceKeys.sort()) { + const docvals = new Set<any>(); + docs.forEach(doc => docvals.add(doc[key])); + const contents = Array.from(docvals.keys()).length > 1 ? "-multiple" : docs[0][key]; + if (key[0] === "#") { + rows.push(<div className="uneditable-field" key={key}> + <span style={{ fontWeight: "bold", whiteSpace: "nowrap" }}>{key}</span> + + </div>); + } else if (contents !== undefined) { + const value = Field.toString(contents as Field); + if (noviceReqFields.includes(key) || key.indexOf("lastModified") !== -1) { rows.push(<div className="uneditable-field" key={key}> - <span style={{ fontWeight: "bold", whiteSpace: "nowrap" }}>{key}</span> - - </div>); + <span style={{ fontWeight: "bold", whiteSpace: "nowrap" }}>{key + ": "}</span> + <div style={{ whiteSpace: "nowrap", overflowX: "hidden" }}>{value}</div> + </div>); } else { - const value = Field.toString(contents as Field); - if (key === "author" || key === "creationDate" || key.indexOf("lastModified") !== -1) { - rows.push(<div className="uneditable-field" key={key}> - <span style={{ fontWeight: "bold", whiteSpace: "nowrap" }}>{key + ": "}</span> - <div style={{ whiteSpace: "nowrap", overflowX: "hidden" }}>{value}</div> - </div>); - } else { - let contentElement: (JSX.Element | null)[] | JSX.Element = []; - contentElement = <EditableView key="editableView" - contents={contents !== undefined ? Field.toString(contents as Field) : "null"} - height={13} - fontSize={10} - GetValue={() => Field.toKeyValueString(doc, key)} - SetValue={(value: string) => KeyValueBox.SetField(doc, key, value, true)} - />; - - rows.push(<div style={{ display: "flex", overflowY: "visible", marginBottom: "-1px" }} key={key}> - <span style={{ fontWeight: "bold", whiteSpace: "nowrap" }}>{key + ":"}</span> - - {contentElement} - </div>); - } + const contentElement = <EditableView key="editableView" + contents={value} + height={13} + fontSize={10} + GetValue={() => contents !== undefined ? Field.toString(contents as Field) : "null"} + SetValue={(value: string) => { docs.map(doc => KeyValueBox.SetField(doc, key, value, true)); return true; }} + />; + + rows.push(<div style={{ display: "flex", overflowY: "visible", marginBottom: "-1px" }} key={key}> + <span style={{ fontWeight: "bold", whiteSpace: "nowrap" }}>{key + ":"}</span> + + {contentElement} + </div>); } } } @@ -233,18 +218,27 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @undoBatch setKeyValue = (value: string) => { - if (this.selectedDoc && this.dataDoc) { - const doc = this.layoutFields ? Doc.Layout(this.selectedDoc) : this.dataDoc; + const docs = SelectionManager.SelectedDocuments().length < 2 && this.selectedDoc ? [this.layoutFields ? Doc.Layout(this.selectedDoc) : this.dataDoc] : SelectionManager.SelectedDocuments().map(dv => this.layoutFields ? dv.layoutDoc : dv.dataDoc); + docs.forEach(doc => { if (value.indexOf(":") !== -1) { const newVal = value[0].toUpperCase() + value.substring(1, value.length); - KeyValueBox.SetField(doc, newVal.substring(0, newVal.indexOf(":")), newVal.substring(newVal.indexOf(":") + 1, newVal.length), true); + const splits = newVal.split(":"); + KeyValueBox.SetField(doc, splits[0], splits[1], true); + const tags = StrCast(doc.tags, ":"); + if (tags.includes(`${splits[0]}:`) && splits[1] === "undefined") { + KeyValueBox.SetField(doc, "tags", `"${tags.replace(splits[0] + ":", "")}"`, true); + } return true; } else if (value[0] === "#") { const newVal = value + `:'${value}'`; - KeyValueBox.SetField(doc, newVal.substring(0, newVal.indexOf(":")), newVal.substring(newVal.indexOf(":") + 1, newVal.length), true); + doc[DataSym][value] = value; + const tags = StrCast(doc.tags, ":"); + if (!tags.includes(`${value}:`)) { + doc[DataSym].tags = `${tags + value + ':'}`; + } return true; } - } + }); return false; } @@ -258,8 +252,15 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { ref && observer.observe(ref); } + @computed get contexts() { + return !this.selectedDoc ? (null) : <PropertiesDocContextSelector Document={this.selectedDoc} hideTitle={true} addDocTab={(doc, where) => CollectionDockingView.AddSplit(doc, "right")} />; + } + previewBackground = () => "lightgrey"; @computed get layoutPreview() { + if (SelectionManager.SelectedDocuments().length > 1) { + return "-- multiple selected --"; + } if (this.selectedDoc) { const layoutDoc = Doc.Layout(this.selectedDoc); const panelHeight = StrCast(Doc.LayoutField(layoutDoc)).includes("FormattedTextBox") ? this.rtfHeight : this.docHeight; @@ -269,21 +270,22 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { Document={layoutDoc} DataDoc={this.dataDoc} LibraryPath={emptyPath} - renderDepth={this.props.renderDepth + 1} + renderDepth={1} rootSelected={returnFalse} treeViewDoc={undefined} backgroundColor={this.previewBackground} fitToBox={true} FreezeDimensions={true} - NativeWidth={layoutDoc.type === - StrCast(Doc.LayoutField(layoutDoc)).includes("FormattedTextBox") ? this.rtfWidth : returnZero} - NativeHeight={layoutDoc.type === - StrCast(Doc.LayoutField(layoutDoc)).includes("FormattedTextBox") ? this.rtfHeight : returnZero} + dontCenter={true} + NativeWidth={layoutDoc.type === DocumentType.RTF ? this.rtfWidth : undefined} + NativeHeight={layoutDoc.type === DocumentType.RTF ? this.rtfHeight : undefined} PanelWidth={panelWidth} PanelHeight={panelHeight} focus={returnFalse} ScreenToLocalTransform={this.getTransform} docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} ContainingCollectionDoc={undefined} ContainingCollectionView={undefined} addDocument={returnFalse} @@ -309,20 +311,24 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { */ @undoBatch changePermissions = (e: any, user: string) => { - SharingManager.Instance.shareFromPropertiesSidebar(user, e.currentTarget.value as SharingPermissions, this.selectedDoc!); + const docs = SelectionManager.SelectedDocuments().length < 2 ? [this.selectedDoc!] : SelectionManager.SelectedDocuments().map(docView => docView.props.Document); + SharingManager.Instance.shareFromPropertiesSidebar(user, e.currentTarget.value as SharingPermissions, docs); } /** * @returns the options for the permissions dropdown. */ getPermissionsSelect(user: string, permission: string) { + const dropdownValues: string[] = Object.values(SharingPermissions); + if (permission === "-multiple-") dropdownValues.unshift(permission); + if (user === "Override") dropdownValues.unshift("None"); return <select className="permissions-select" value={permission} onChange={e => this.changePermissions(e, user)}> - {Object.values(SharingPermissions).map(permission => { + {dropdownValues.filter(permission => permission !== SharingPermissions.View).map(permission => { return ( - <option key={permission} value={permission} selected={this.selectedDoc![`ACL-${user.replace(".", "_")}`] === permission}> - {permission} + <option key={permission} value={permission}> + {permission === SharingPermissions.Add ? "Can Augment" : permission} </option>); })} </select>; @@ -345,8 +351,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @computed get expansionIcon() { return <Tooltip title={<div className="dash-tooltip">{"Show more permissions"}</div>}> <div className="expansion-button" onPointerDown={() => { - if (this.selectedDocumentView) { - SharingManager.Instance.open(this.selectedDocumentView); + if (this.selectedDocumentView || this.selectedDoc) { + SharingManager.Instance.open(this.selectedDocumentView?.props.Document === this.selectedDocumentView ? this.selectedDocumentView : undefined, this.selectedDoc); } }}> <FontAwesomeIcon className="expansion-button-icon" icon="ellipsis-h" color="black" size="sm" /> @@ -357,7 +363,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { /** * @returns a row of the permissions panel */ - sharingItem(name: string, effectiveAcl: symbol, permission: string) { + sharingItem(name: string, admin: boolean, permission: string, showExpansionIcon?: boolean) { return <div className="propertiesView-sharingTable-item" key={name + permission} // style={{ backgroundColor: this.selectedUser === name ? "#bcecfc" : "" }} // onPointerDown={action(() => this.selectedUser = this.selectedUser === name ? "" : name)} @@ -365,17 +371,18 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <div className="propertiesView-sharingTable-item-name" style={{ width: name !== "Me" ? "85px" : "80px" }}> {name} </div> {/* {name !== "Me" ? this.notifyIcon : null} */} <div className="propertiesView-sharingTable-item-permission"> - {effectiveAcl === AclAdmin && permission !== "Owner" ? this.getPermissionsSelect(name, permission) : permission} - {permission === "Owner" ? this.expansionIcon : null} + {admin && permission !== "Owner" ? this.getPermissionsSelect(name, permission) : permission} + {permission === "Owner" || showExpansionIcon ? this.expansionIcon : null} </div> </div>; } /** - * @returns the sharing and permissiosn panel. + * @returns the sharing and permissions panel. */ @computed get sharingTable() { const AclMap = new Map<symbol, string>([ + [AclUnset, "None"], [AclPrivate, SharingPermissions.None], [AclReadonly, SharingPermissions.View], [AclAddonly, SharingPermissions.Add], @@ -383,32 +390,46 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { [AclAdmin, SharingPermissions.Admin] ]); - const target = this.layoutDocAcls ? this.selectedDoc! : this.selectedDoc![DataSym]; + // all selected docs + const docs = SelectionManager.SelectedDocuments().length < 2 ? + [this.layoutDocAcls ? this.selectedDoc! : this.selectedDoc![DataSym]] + : SelectionManager.SelectedDocuments().map(docView => this.layoutDocAcls ? docView.props.Document : docView.props.Document[DataSym]); + + const target = docs[0]; + + // tslint:disable-next-line: no-unnecessary-callback-wrapper + const effectiveAcls = docs.map(doc => GetEffectiveAcl(doc)); + const showAdmin = effectiveAcls.every(acl => acl === AclAdmin); + + // users in common between all docs + const commonKeys = intersection(...docs.map(doc => this.layoutDocAcls ? doc?.[AclSym] && Object.keys(doc[AclSym]) : doc?.[DataSym][AclSym] && Object.keys(doc[DataSym][AclSym]))); - const effectiveAcl = GetEffectiveAcl(target); const tableEntries = []; // DocCastAsync(Doc.UserDoc().sidebarUsersDisplayed).then(sidebarUsersDisplayed => { - if (target[AclSym]) { - for (const [key, value] of Object.entries(target[AclSym])) { + if (commonKeys.length) { + for (const key of commonKeys) { const name = key.substring(4).replace("_", "."); - if (name !== Doc.CurrentUserEmail && name !== target.author && name !== "Public"/* && sidebarUsersDisplayed![name] !== false*/) { - tableEntries.push(this.sharingItem(name, effectiveAcl, AclMap.get(value as symbol)!)); + const uniform = docs.every(doc => this.layoutDocAcls ? doc?.[AclSym]?.[key] === docs[0]?.[AclSym]?.[key] : doc?.[DataSym]?.[AclSym]?.[key] === docs[0]?.[DataSym]?.[AclSym]?.[key]); + if (name !== Doc.CurrentUserEmail && name !== target.author && name !== "Public" && name !== "Override"/* && sidebarUsersDisplayed![name] !== false*/) { + tableEntries.push(this.sharingItem(name, showAdmin, uniform ? AclMap.get(this.layoutDocAcls ? target[AclSym][key] : target[DataSym][AclSym][key])! : "-multiple-")); } } } // if (Doc.UserDoc().sidebarUsersDisplayed) { // for (const [name, value] of Object.entries(sidebarUsersDisplayed!)) { - // if (value === true && !this.selectedDoc![`ACL-${name.substring(8).replace(".", "_")}`]) tableEntries.push(this.sharingItem(name.substring(8), effectiveAcl, SharingPermissions.None)); + // if (value === true && !this.selectedDoc![`acl-${name.substring(8).replace(".", "_")}`]) tableEntries.push(this.sharingItem(name.substring(8), effectiveAcl, SharingPermissions.None)); // } // } // }) + const ownerSame = Doc.CurrentUserEmail !== target.author && docs.filter(doc => doc).every(doc => doc.author === docs[0].author); // shifts the current user, owner, public to the top of the doc. - tableEntries.unshift(this.sharingItem("Public", effectiveAcl, (AclMap.get(target[AclSym]?.["ACL-Public"]) || SharingPermissions.None))); - tableEntries.unshift(this.sharingItem("Me", effectiveAcl, Doc.CurrentUserEmail === target.author ? "Owner" : AclMap.get(effectiveAcl)!)); - if (Doc.CurrentUserEmail !== target.author) tableEntries.unshift(this.sharingItem(StrCast(target.author), effectiveAcl, "Owner")); + tableEntries.unshift(this.sharingItem("Override", showAdmin, docs.filter(doc => doc).every(doc => doc["acl-Override"] === docs[0]["acl-Override"]) ? (AclMap.get(target[AclSym]?.["acl-Override"]) || "None") : "-multiple-")); + tableEntries.unshift(this.sharingItem("Public", showAdmin, docs.filter(doc => doc).every(doc => doc["acl-Public"] === docs[0]["acl-Public"]) ? (AclMap.get(target[AclSym]?.["acl-Public"]) || SharingPermissions.None) : "-multiple-")); + tableEntries.unshift(this.sharingItem("Me", showAdmin, docs.filter(doc => doc).every(doc => doc.author === Doc.CurrentUserEmail) ? "Owner" : effectiveAcls.every(acl => acl === effectiveAcls[0]) ? AclMap.get(effectiveAcls[0])! : "-multiple-", !ownerSame)); + if (ownerSame) tableEntries.unshift(this.sharingItem(StrCast(target.author), showAdmin, "Owner")); return <div className="propertiesView-sharingTable"> {tableEntries} @@ -429,21 +450,27 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { } @computed get editableTitle() { + const titles = new Set<string>(); + SelectionManager.SelectedDocuments().forEach(dv => titles.add(StrCast(dv.rootDoc.title))); + const title = Array.from(titles.keys()).length > 1 ? "--multiple selected--" : StrCast(this.selectedDoc?.title); return <div className="editable-title"><EditableView key="editableView" - contents={StrCast(this.selectedDoc?.title)} + contents={title} height={25} fontSize={14} - GetValue={() => StrCast(this.selectedDoc?.title)} + GetValue={() => title} SetValue={this.setTitle} /> </div>; } @undoBatch @action setTitle = (value: string) => { - if (this.dataDoc) { - this.selectedDoc && (this.selectedDoc.title = value); - KeyValueBox.SetField(this.dataDoc, "title", value, true); + if (SelectionManager.SelectedDocuments().length > 1) { + SelectionManager.SelectedDocuments().map(dv => Doc.SetInPlace(dv.rootDoc, "title", value, true)); + return true; + } else if (this.dataDoc) { + if (this.selectedDoc) Doc.SetInPlace(this.selectedDoc, "title", value, true); + else KeyValueBox.SetField(this.dataDoc, "title", value, true); return true; } return false; @@ -500,15 +527,16 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @computed get controlPointsButton() { - return <div className="inking-button"> + const formatInstance = InkStrokeProperties.Instance; + return !formatInstance ? (null) : <div className="inking-button"> <Tooltip title={<div className="dash-tooltip">{"Edit points"}</div>}> - <div className="inking-button-points" onPointerDown={action(() => FormatShapePane.Instance._controlBtn = !FormatShapePane.Instance._controlBtn)} style={{ backgroundColor: FormatShapePane.Instance._controlBtn ? "black" : "" }}> + <div className="inking-button-points" onPointerDown={action(() => formatInstance._controlBtn = !formatInstance._controlBtn)} style={{ backgroundColor: formatInstance._controlBtn ? "black" : "" }}> <FontAwesomeIcon icon="bezier-curve" color="white" size="lg" /> </div> </Tooltip> - <Tooltip title={<div className="dash-tooltip">{FormatShapePane.Instance._lock ? "Unlock ratio" : "Lock ratio"}</div>}> - <div className="inking-button-lock" onPointerDown={action(() => FormatShapePane.Instance._lock = !FormatShapePane.Instance._lock)} > - <FontAwesomeIcon icon={FormatShapePane.Instance._lock ? "lock" : "unlock"} color="white" size="lg" /> + <Tooltip title={<div className="dash-tooltip">{formatInstance._lock ? "Unlock ratio" : "Lock ratio"}</div>}> + <div className="inking-button-lock" onPointerDown={action(() => formatInstance._lock = !formatInstance._lock)} > + <FontAwesomeIcon icon={formatInstance._lock ? "lock" : "unlock"} color="white" size="lg" /> </div> </Tooltip> <Tooltip title={<div className="dash-tooltip">{"Rotate 90Ëš"}</div>}> @@ -528,7 +556,12 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <div className="inputBox-title"> {title} </div> <input className="inputBox-input" type="text" value={value} - onChange={e => setter(e.target.value)} /> + onChange={e => { + setter(e.target.value); + }} + onKeyPress={e => { + e.stopPropagation(); + }} /> <div className="inputBox-button"> <div className="inputBox-button-up" key="up2" onPointerDown={undoBatch(action(() => this.upDownButtons("up", key)))} > @@ -563,7 +596,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { const oldX = NumCast(this.selectedDoc?.x); const oldY = NumCast(this.selectedDoc?.y); this.selectedDoc && (this.selectedDoc._width = oldWidth + (dirs === "up" ? 10 : - 10)); - FormatShapePane.Instance._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) / oldWidth * NumCast(this.selectedDoc?._height))); + InkStrokeProperties.Instance?._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) / oldWidth * NumCast(this.selectedDoc?._height))); const doc = this.selectedDoc; if (doc?.type === DocumentType.INK && doc.x && doc.y && doc._height && doc._width) { const ink = Cast(doc.data, InkField)?.inkData; @@ -585,7 +618,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { const oX = NumCast(this.selectedDoc?.x); const oY = NumCast(this.selectedDoc?.y); this.selectedDoc && (this.selectedDoc._height = oHeight + (dirs === "up" ? 10 : - 10)); - FormatShapePane.Instance._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) / oHeight * NumCast(this.selectedDoc?._width))); + InkStrokeProperties.Instance?._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) / oHeight * NumCast(this.selectedDoc?._width))); const docu = this.selectedDoc; if (docu?.type === DocumentType.INK && docu.x && docu.y && docu._height && docu._width) { const ink = Cast(docu.data, InkField)?.inkData; @@ -623,12 +656,12 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { set shapeWid(value) { const oldWidth = NumCast(this.selectedDoc?._width); this.selectedDoc && (this.selectedDoc._width = Number(value)); - FormatShapePane.Instance._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) * NumCast(this.selectedDoc?._height)) / oldWidth); + InkStrokeProperties.Instance?._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) * NumCast(this.selectedDoc?._height)) / oldWidth); } set shapeHgt(value) { const oldHeight = NumCast(this.selectedDoc?._height); this.selectedDoc && (this.selectedDoc._height = Number(value)); - FormatShapePane.Instance._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) * NumCast(this.selectedDoc?._width)) / oldHeight); + InkStrokeProperties.Instance?._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) * NumCast(this.selectedDoc?._width)) / oldHeight); } @computed get hgtInput() { return this.inputBoxDuo("hgt", this.shapeHgt, (val: string) => { if (!isNaN(Number(val))) { this.shapeHgt = val; } return true; }, "H:", "wid", this.shapeWid, (val: string) => { if (!isNaN(Number(val))) { this.shapeWid = val; } return true; }, "W:"); } @@ -833,35 +866,33 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { if (this.selectedDoc && !this.isPres) { return <div className="propertiesView" style={{ width: this.props.width, + minWidth: this.props.width //overflowY: this.scrolling ? "scroll" : "visible" }} > <div className="propertiesView-title" style={{ width: this.props.width }}> Properties - {/* <div className="propertiesView-title-icon" onPointerDown={this.props.onDown}> - <FontAwesomeIcon icon="times" color="black" size="sm" /> - </div> */} </div> <div className="propertiesView-name"> {this.editableTitle} </div> - <div className="propertiesView-settings" onPointerEnter={() => runInAction(() => { this.inActions = true; })} - onPointerLeave={action(() => this.inActions = false)}> + <div className="propertiesView-settings" onPointerEnter={action(() => this.inOptions = true)} + onPointerLeave={action(() => this.inOptions = false)}> <div className="propertiesView-settings-title" - onPointerDown={() => runInAction(() => { this.openActions = !this.openActions; })} - style={{ backgroundColor: this.openActions ? "black" : "" }}> - Actions + onPointerDown={action(() => this.openOptions = !this.openOptions)} + style={{ backgroundColor: this.openOptions ? "black" : "" }}> + Options <div className="propertiesView-settings-title-icon"> - <FontAwesomeIcon icon={this.openActions ? "caret-down" : "caret-right"} size="lg" color="white" /> + <FontAwesomeIcon icon={this.openOptions ? "caret-down" : "caret-right"} size="lg" color="white" /> </div> </div> - {!this.openActions ? (null) : + {!this.openOptions ? (null) : <div className="propertiesView-settings-content"> <PropertiesButtons /> </div>} </div> <div className="propertiesView-sharing"> <div className="propertiesView-sharing-title" - onPointerDown={() => runInAction(() => { this.openSharing = !this.openSharing; })} + onPointerDown={action(() => this.openSharing = !this.openSharing)} style={{ backgroundColor: this.openSharing ? "black" : "" }}> Sharing {"&"} Permissions <div className="propertiesView-sharing-title-icon"> @@ -870,52 +901,27 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { </div> {!this.openSharing ? (null) : <div className="propertiesView-sharing-content"> - <div className="propertiesView-acls-checkbox"> - <Checkbox - color="primary" - onChange={action(() => this.layoutDocAcls = !this.layoutDocAcls)} - checked={this.layoutDocAcls} - />; - <div className="propertiesView-acls-checkbox-text">Layout</div> + <div className="propertiesView-buttonContainer"> + {!novice ? (<div className="propertiesView-acls-checkbox"> + <Checkbox + color="primary" + onChange={action(() => this.layoutDocAcls = !this.layoutDocAcls)} + checked={this.layoutDocAcls} + /> + <div className="propertiesView-acls-checkbox-text">Layout</div> + </div>) : (null)} + <button onPointerDown={() => SharingManager.Instance.distributeOverCollection(this.selectedDoc!)}> + <FontAwesomeIcon icon="redo-alt" color="white" size="1x" /> + </button> </div> {this.sharingTable} - {/* <div className="change-buttons"> - <button - onPointerDown={action(() => this.addButtonPressed = !this.addButtonPressed)} - > - <FontAwesomeIcon icon={fa.faPlus} size={"sm"} style={{ marginTop: -3, marginLeft: -3 }} /> - </button> - <button - id="sharingProperties-removeUser" - onPointerDown={() => this.handleUserChange(this.selectedUser, false)} - style={{ backgroundColor: this.selectedUser ? "#121721" : "#777777" }} - ><FontAwesomeIcon icon={fa.faMinus} size={"sm"} style={{ marginTop: -3, marginLeft: -3 }} /></button> - <button onClick={() => SharingManager.Instance.open(this.selectedDocumentView!)}><FontAwesomeIcon icon={fa.faCog} size={"sm"} style={{ marginTop: -3, marginLeft: -3 }} /></button> - {this.addButtonPressed ? - // <input type="text" onKeyDown={this.handleKeyPress} /> : - <select onChange={e => this.handleUserChange(e.target.value, true)}> - <option selected disabled hidden> - Add users - </option> - {SharingManager.Instance.users.map(user => - (<option value={user.user.email}> - {user.user.email} - </option>) - )} - {GroupManager.Instance.getAllGroups().map(group => - (<option value={StrCast(group.groupName)}> - {StrCast(group.groupName)} - </option>))} - </select> : - null} - </div> */} </div>} </div> {!this.isInk ? (null) : <div className="propertiesView-appearance"> <div className="propertiesView-appearance-title" - onPointerDown={() => runInAction(() => { this.openAppearance = !this.openAppearance; })} + onPointerDown={action(() => this.openAppearance = !this.openAppearance)} style={{ backgroundColor: this.openAppearance ? "black" : "" }}> Appearance <div className="propertiesView-appearance-title-icon"> @@ -930,7 +936,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { {this.isInk ? <div className="propertiesView-transform"> <div className="propertiesView-transform-title" - onPointerDown={() => runInAction(() => { this.openTransform = !this.openTransform; })} + onPointerDown={action(() => this.openTransform = !this.openTransform)} style={{ backgroundColor: this.openTransform ? "black" : "" }}> Transform <div className="propertiesView-transform-title-icon"> @@ -944,7 +950,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <div className="propertiesView-fields"> <div className="propertiesView-fields-title" - onPointerDown={() => runInAction(() => { this.openFields = !this.openFields; })} + onPointerDown={action(() => this.openFields = !this.openFields)} style={{ backgroundColor: this.openFields ? "black" : "" }}> Fields {"&"} Tags <div className="propertiesView-fields-title-icon"> @@ -960,12 +966,23 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { {novice ? this.noviceFields : this.expandedField} </div>} </div> + <div className="propertiesView-contexts"> + <div className="propertiesView-contexts-title" + onPointerDown={action(() => this.openContexts = !this.openContexts)} + style={{ backgroundColor: this.openContexts ? "black" : "" }}> + Contexts + <div className="propertiesView-contexts-title-icon"> + <FontAwesomeIcon icon={this.openContexts ? "caret-down" : "caret-right"} size="lg" color="white" /> + </div> + </div> + {this.openContexts ? <div className="propertiesView-contexts-content" >{this.contexts}</div> : null} + </div> <div className="propertiesView-layout"> <div className="propertiesView-layout-title" - onPointerDown={() => runInAction(() => { this.openLayout = !this.openLayout; })} + onPointerDown={action(() => this.openLayout = !this.openLayout)} style={{ backgroundColor: this.openLayout ? "black" : "" }}> Layout - <div className="propertiesView-layout-title-icon" onPointerDown={() => runInAction(() => { this.openLayout = !this.openLayout; })}> + <div className="propertiesView-layout-title-icon"> <FontAwesomeIcon icon={this.openLayout ? "caret-down" : "caret-right"} size="lg" color="white" /> </div> </div> @@ -975,6 +992,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { } if (this.isPres) { const selectedItem: boolean = PresBox.Instance?._selectedArray.length > 0; + const type = PresBox.Instance.activeItem?.type; return <div className="propertiesView" style={{ width: this.props.width }}> <div className="propertiesView-title" style={{ width: this.props.width }}> Presentation @@ -1001,9 +1019,9 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { {PresBox.Instance.transitionDropdown} </div> : null} </div>} - {!selectedItem ? (null) : <div className="propertiesView-presTrails"> + {!selectedItem || type === DocumentType.VID || type === DocumentType.AUDIO ? (null) : <div className="propertiesView-presTrails"> <div className="propertiesView-presTrails-title" - onPointerDown={() => runInAction(() => { this.openPresProgressivize = !this.openPresProgressivize; })} + onPointerDown={action(() => this.openPresProgressivize = !this.openPresProgressivize)} style={{ backgroundColor: this.openPresProgressivize ? "black" : "" }}> <FontAwesomeIcon icon={"tasks"} /> Progressivize <div className="propertiesView-presTrails-title-icon"> @@ -1014,9 +1032,9 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { {PresBox.Instance.progressivizeDropdown} </div> : null} </div>} - {!selectedItem ? (null) : <div className="propertiesView-presTrails"> + {!selectedItem || (type !== DocumentType.COL && type !== DocumentType.VID && type !== DocumentType.AUDIO) ? (null) : <div className="propertiesView-presTrails"> <div className="propertiesView-presTrails-title" - onPointerDown={() => runInAction(() => { this.openSlideOptions = !this.openSlideOptions; })} + onPointerDown={action(() => { this.openSlideOptions = !this.openSlideOptions; })} style={{ backgroundColor: this.openSlideOptions ? "black" : "" }}> <FontAwesomeIcon icon={"cog"} /> {PresBox.Instance.stringType} options <div className="propertiesView-presTrails-title-icon"> @@ -1029,7 +1047,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { </div>} <div className="propertiesView-presTrails"> <div className="propertiesView-presTrails-title" - onPointerDown={() => runInAction(() => { this.openAddSlide = !this.openAddSlide; })} + onPointerDown={action(() => { this.openAddSlide = !this.openAddSlide; })} style={{ backgroundColor: this.openAddSlide ? "black" : "" }}> <FontAwesomeIcon icon={"plus"} /> Add new slide <div className="propertiesView-presTrails-title-icon"> @@ -1040,19 +1058,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { {PresBox.Instance.newDocumentDropdown} </div> : null} </div> - {/* <div className="propertiesView-sharing"> - <div className="propertiesView-sharing-title" - onPointerDown={() => runInAction(() => { this.openSharing = !this.openSharing; })} - style={{ backgroundColor: this.openSharing ? "black" : "" }}> - Sharing {"&"} Permissions - <div className="propertiesView-sharing-title-icon"> - <FontAwesomeIcon icon={this.openSharing ? "caret-down" : "caret-right"} size="lg" color="white" /> - </div> - </div> - {this.openSharing ? <div className="propertiesView-sharing-content"> - {this.sharingTable} - </div> : null} - </div> */} </div>; } } diff --git a/src/client/views/ScriptingRepl.tsx b/src/client/views/ScriptingRepl.tsx index db087fb23..c598b2861 100644 --- a/src/client/views/ScriptingRepl.tsx +++ b/src/client/views/ScriptingRepl.tsx @@ -1,17 +1,12 @@ -import * as React from 'react'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, observable } from 'mobx'; import { observer } from 'mobx-react'; -import { observable, action } from 'mobx'; -import './ScriptingRepl.scss'; -import { Scripting, CompileScript, ts, Transformer } from '../util/Scripting'; +import * as React from 'react'; import { DocumentManager } from '../util/DocumentManager'; -import { OverlayView } from './OverlayView'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faCaretDown, faCaretRight } from '@fortawesome/free-solid-svg-icons'; +import { CompileScript, Scripting, Transformer, ts } from '../util/Scripting'; import { DocumentIconContainer } from './nodes/DocumentIcon'; - -library.add(faCaretDown); -library.add(faCaretRight); +import { OverlayView } from './OverlayView'; +import './ScriptingRepl.scss'; @observer export class ScriptingObjectDisplay extends React.Component<{ scrollToBottom: () => void, value: { [key: string]: any }, name?: string }> { diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx index eb20fc257..94efff4ee 100644 --- a/src/client/views/TemplateMenu.tsx +++ b/src/client/views/TemplateMenu.tsx @@ -1,16 +1,14 @@ import { action, observable, runInAction, ObservableSet, trace, computed } from "mobx"; import { observer } from "mobx-react"; -import { SelectionManager } from "../util/SelectionManager"; import { undoBatch } from "../util/UndoManager"; import './TemplateMenu.scss'; import { DocumentView } from "./nodes/DocumentView"; -import { Template } from "./Templates"; import React = require("react"); import { Doc, DocListCast } from "../../fields/Doc"; import { Docs, DocUtils, } from "../documents/Documents"; import { StrCast, Cast } from "../../fields/Types"; import { CollectionTreeView } from "./collections/CollectionTreeView"; -import { returnTrue, emptyFunction, returnFalse, returnOne, emptyPath, returnZero, returnEmptyFilter } from "../../Utils"; +import { returnTrue, emptyFunction, returnFalse, returnOne, emptyPath, returnZero, returnEmptyFilter, returnEmptyDoclist } from "../../Utils"; import { Transform } from "../util/Transform"; import { ScriptField, ComputedField } from "../../fields/ScriptField"; import { Scripting } from "../util/Scripting"; @@ -18,13 +16,13 @@ import { List } from "../../fields/List"; import { TraceMobx } from "../../fields/util"; @observer -class TemplateToggle extends React.Component<{ template: Template, checked: boolean, toggle: (event: React.ChangeEvent<HTMLInputElement>, template: Template) => void }> { +class TemplateToggle extends React.Component<{ template: string, checked: boolean, toggle: (event: React.ChangeEvent<HTMLInputElement>, template: string) => void }> { render() { if (this.props.template) { return ( <li className="templateToggle"> <input type="checkbox" checked={this.props.checked} onChange={(event) => this.props.toggle(event, this.props.template)} /> - {this.props.template.Name} + {this.props.template} </li> ); } else { @@ -46,7 +44,7 @@ class OtherToggle extends React.Component<{ checked: boolean, name: string, togg export interface TemplateMenuProps { docViews: DocumentView[]; - templates: Map<Template, boolean>; + templates: Map<string, boolean>; } @@ -69,8 +67,8 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { @undoBatch @action - toggleTemplate = (event: React.ChangeEvent<HTMLInputElement>, template: Template): void => { - this.props.docViews.forEach(d => Doc.Layout(d.layoutDoc)["_show" + template.Name] = event.target.checked ? template.Name.toLowerCase() : ""); + toggleTemplate = (event: React.ChangeEvent<HTMLInputElement>, template: string): void => { + this.props.docViews.forEach(d => Doc.Layout(d.layoutDoc)["_show" + template] = event.target.checked ? template.toLowerCase() : ""); } @action @@ -113,26 +111,28 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { const firstDoc = this.props.docViews[0].props.Document; const templateName = StrCast(firstDoc.layoutKey, "layout").replace("layout_", ""); const noteTypes = DocListCast(Cast(Doc.UserDoc()["template-notes"], Doc, null)?.data); - const addedTypes = DocListCast(Cast(Doc.UserDoc()["template-buttons"], Doc, null)?.data); + const addedTypes = Doc.UserDoc().noviceMode ? [] : DocListCast(Cast(Doc.UserDoc()["template-buttons"], Doc, null)?.data); const layout = Doc.Layout(firstDoc); const templateMenu: Array<JSX.Element> = []; this.props.templates.forEach((checked, template) => - templateMenu.push(<TemplateToggle key={template.Name} template={template} checked={checked} toggle={this.toggleTemplate} />)); + templateMenu.push(<TemplateToggle key={template} template={template} checked={checked} toggle={this.toggleTemplate} />)); templateMenu.push(<OtherToggle key={"audio"} name={"Audio"} checked={firstDoc._showAudio ? true : false} toggle={this.toggleAudio} />); - templateMenu.push(<OtherToggle key={"chrome"} name={"Chrome"} checked={layout._chromeStatus !== "disabled"} toggle={this.toggleChrome} />); templateMenu.push(<OtherToggle key={"default"} name={"Default"} checked={templateName === "layout"} toggle={this.toggleDefault} />); + !Doc.UserDoc().noviceMode && templateMenu.push(<OtherToggle key={"chrome"} name={"Chrome"} checked={layout._chromeStatus !== "disabled"} toggle={this.toggleChrome} />); addedTypes.concat(noteTypes).map(template => template.treeViewChecked = this.templateIsUsed(firstDoc, template)); this._addedKeys && Array.from(this._addedKeys).filter(key => !noteTypes.some(nt => nt.title === key)).forEach(template => templateMenu.push( <OtherToggle key={template} name={template} checked={templateName === template} toggle={e => this.toggleLayout(e, template)} />)); return <ul className="template-list" style={{ display: "block" }}> - <input placeholder="+ layout" ref={this._customRef} onKeyPress={this.onCustomKeypress} /> + {Doc.UserDoc().noviceMode ? (null) : <input placeholder="+ layout" ref={this._customRef} onKeyPress={this.onCustomKeypress} />} {templateMenu} - <CollectionTreeView + {Doc.UserDoc().noviceMode ? (null) : <CollectionTreeView Document={Doc.UserDoc().templateDocs as Doc} CollectionView={undefined} ContainingCollectionDoc={undefined} ContainingCollectionView={undefined} docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} rootSelected={returnFalse} onCheckedClick={this.scriptField} onChildClick={this.scriptField} @@ -149,8 +149,6 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { select={emptyFunction} renderDepth={1} addDocTab={returnFalse} - NativeHeight={returnZero} - NativeWidth={returnZero} PanelWidth={this.return100} PanelHeight={this.return100} treeViewHideHeaderFields={true} @@ -159,7 +157,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { fieldKey={"data"} moveDocument={returnFalse} removeDocument={returnFalse} - addDocument={returnFalse} /> + addDocument={returnFalse} />} </ul>; } } diff --git a/src/client/views/Templates.tsx b/src/client/views/Templates.tsx deleted file mode 100644 index a6dbaa650..000000000 --- a/src/client/views/Templates.tsx +++ /dev/null @@ -1,41 +0,0 @@ -export class Template { - constructor(name: string, layout: string) { - this._name = name; - this._layout = layout; - } - - private _name: string; - private _layout: string; - - get Name(): string { - return this._name; - } - - get Layout(): string { - return this._layout; - } -} - -export namespace Templates { - export const Caption = new Template("Caption", - `<div> - <div style="height:100%; width:100%;">{layout}</div> - <div style="bottom: 0; font-size:14px; width:100%; position:absolute"> - <FormattedTextBox {...props} fieldKey={"caption"} hideOnLeave={"true"} /> - </div> - </div>` ); - - export const Title = new Template("Title", - `<div> - <div style="height:25px; width:100%; background-color: rgba(0, 0, 0, .4); color: white; z-index: 100"> - <span style="text-align:center;width:100%;font-size:20px;position:absolute;overflow:hidden;white-space:nowrap;text-overflow:ellipsis">{props.Document.title}</span> - </div> - <div style="height:calc(100% - 25px);"> - <div style="width:100%;overflow:auto">{layout}</div> - </div> - </div>` ); - export const TitleHover = new Template("TitleHover", Title.Layout); - - export const TemplateList: Template[] = [Title, TitleHover, Caption]; -} - diff --git a/src/client/views/animationtimeline/Keyframe.tsx b/src/client/views/animationtimeline/Keyframe.tsx index 1b81c544a..e84022366 100644 --- a/src/client/views/animationtimeline/Keyframe.tsx +++ b/src/client/views/animationtimeline/Keyframe.tsx @@ -331,7 +331,7 @@ export class Keyframe extends React.Component<IProps> { }), TimelineMenu.Instance.addItem("button", "Show Data", action(() => { const kvp = Docs.Create.KVPDocument(kf, { _width: 300, _height: 300 }); - CollectionDockingView.AddRightSplit(kvp, emptyPath); + CollectionDockingView.AddSplit(kvp, "right"); })), TimelineMenu.Instance.addItem("button", "Delete", action(() => { (this.regiondata.keyframes as List<Doc>).splice(this.keyframes.indexOf(kf), 1); diff --git a/src/client/views/animationtimeline/Timeline.tsx b/src/client/views/animationtimeline/Timeline.tsx index df828897e..093310755 100644 --- a/src/client/views/animationtimeline/Timeline.tsx +++ b/src/client/views/animationtimeline/Timeline.tsx @@ -83,26 +83,22 @@ export class Timeline extends React.Component<FieldViewProps> { } /////////lifecycle functions//////////// - componentWillMount() { + @action + componentDidMount() { const relativeHeight = window.innerHeight / 20; //sets height to arbitrary size, relative to innerHeight this._titleHeight = relativeHeight < this.MAX_TITLE_HEIGHT ? relativeHeight : this.MAX_TITLE_HEIGHT; //check if relHeight is less than Maxheight. Else, just set relheight to max this.MIN_CONTAINER_HEIGHT = this._titleHeight + 130; //offset this.DEFAULT_CONTAINER_HEIGHT = this._titleHeight * 2 + 130; //twice the titleheight + offset - } - - componentDidMount() { - runInAction(() => { - if (!this.props.Document.AnimationLength) { //if animation length did not exist - this.props.Document.AnimationLength = this._time; //set it to default time - } else { - this._time = NumCast(this.props.Document.AnimationLength); //else, set time to animationlength stored from before - } - this._totalLength = this._tickSpacing * (this._time / this._tickIncrement); //the entire length of the timeline div (actual div part itself) - this._visibleLength = this._infoContainer.current!.getBoundingClientRect().width; //the visible length of the timeline (the length that you current see) - this._visibleStart = this._infoContainer.current!.scrollLeft; //where the div starts - this.props.Document.isATOn = !this.props.Document.isATOn; //turns the boolean on, saying AT (animation timeline) is on - this.toggleHandle(); - }); + if (!this.props.Document.AnimationLength) { //if animation length did not exist + this.props.Document.AnimationLength = this._time; //set it to default time + } else { + this._time = NumCast(this.props.Document.AnimationLength); //else, set time to animationlength stored from before + } + this._totalLength = this._tickSpacing * (this._time / this._tickIncrement); //the entire length of the timeline div (actual div part itself) + this._visibleLength = this._infoContainer.current!.getBoundingClientRect().width; //the visible length of the timeline (the length that you current see) + this._visibleStart = this._infoContainer.current!.scrollLeft; //where the div starts + this.props.Document.isATOn = !this.props.Document.isATOn; //turns the boolean on, saying AT (animation timeline) is on + this.toggleHandle(); } componentWillUnmount() { @@ -385,10 +381,10 @@ export class Timeline extends React.Component<FieldViewProps> { const ttime = `Total: ${this.toReadTime(this._time)}`; return ( <div style={{ flexDirection: "column" }}> - <div className="animation-text" style={{ fontSize: "10pt", width: "100%", display: !this.props.Document.isATOn ? "block" : "none" }}> + <div className="animation-text" style={{ fontSize: "10px", width: "100%", display: !this.props.Document.isATOn ? "block" : "none" }}> {ctime} </div> - <div className="animation-text" style={{ fontSize: "10pt", width: "100%", display: !this.props.Document.isATOn ? "block" : "none" }}> + <div className="animation-text" style={{ fontSize: "10px", width: "100%", display: !this.props.Document.isATOn ? "block" : "none" }}> {ttime} </div> </div> @@ -488,7 +484,6 @@ export class Timeline extends React.Component<FieldViewProps> { render() { setTimeout(() => this.changeLengths(), 0); - trace(); // change visible and total width return ( <div style={{ visibility: "visible" }}> diff --git a/src/client/views/collections/CollectionCarousel3DView.scss b/src/client/views/collections/CollectionCarousel3DView.scss index 5f8895c1f..652293ed6 100644 --- a/src/client/views/collections/CollectionCarousel3DView.scss +++ b/src/client/views/collections/CollectionCarousel3DView.scss @@ -8,6 +8,8 @@ display: flex; position: absolute; top: 15%; + height: 60%; + width: 100%; align-items: center; transition: transform 0.3s cubic-bezier(0.455, 0.03, 0.515, 0.955); diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx index 0f3b6f212..4f1ef6e61 100644 --- a/src/client/views/collections/CollectionCarousel3DView.tsx +++ b/src/client/views/collections/CollectionCarousel3DView.tsx @@ -1,20 +1,18 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { computed } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { documentSchema, collectionSchema } from '../../../fields/documentSchemas'; +import { Doc } from '../../../fields/Doc'; +import { collectionSchema, documentSchema } from '../../../fields/documentSchemas'; +import { Id } from '../../../fields/FieldSymbols'; import { makeInterface } from '../../../fields/Schema'; -import { NumCast, StrCast, ScriptCast } from '../../../fields/Types'; +import { ScriptField } from '../../../fields/ScriptField'; +import { NumCast, ScriptCast, StrCast } from '../../../fields/Types'; +import { returnFalse, Utils, OmitKeys } from '../../../Utils'; import { DragManager } from '../../util/DragManager'; import { ContentFittingDocumentView } from '../nodes/ContentFittingDocumentView'; import "./CollectionCarousel3DView.scss"; import { CollectionSubView } from './CollectionSubView'; -import { Doc } from '../../../fields/Doc'; -import { ContextMenu } from '../ContextMenu'; -import { ObjectField } from '../../../fields/ObjectField'; -import { returnFalse, Utils } from '../../../Utils'; -import { ScriptField } from '../../../fields/ScriptField'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Id } from '../../../fields/FieldSymbols'; type Carousel3DDocument = makeInterface<[typeof documentSchema, typeof collectionSchema]>; const Carousel3DDocument = makeInterface(documentSchema, collectionSchema); @@ -44,7 +42,7 @@ export class CollectionCarousel3DView extends CollectionSubView(Carousel3DDocume const displayDoc = (childPair: { layout: Doc, data: Doc }) => { const script = ScriptField.MakeScript("child._showCaption = 'caption'", { child: Doc.name }, { child: childPair.layout }); const onChildClick = script && (() => script); - return <ContentFittingDocumentView {...this.props} + return <ContentFittingDocumentView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} onDoubleClick={this.onChildDoubleClick} onClick={onChildClick} renderDepth={this.props.renderDepth + 1} diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx index 8a27f8102..c5910b0be 100644 --- a/src/client/views/collections/CollectionCarouselView.tsx +++ b/src/client/views/collections/CollectionCarouselView.tsx @@ -13,7 +13,7 @@ import { Doc } from '../../../fields/Doc'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; import { ContextMenu } from '../ContextMenu'; import { ObjectField } from '../../../fields/ObjectField'; -import { returnFalse } from '../../../Utils'; +import { returnFalse, returnZero, OmitKeys } from '../../../Utils'; import { ScriptField } from '../../../fields/ScriptField'; type CarouselDocument = makeInterface<[typeof documentSchema, typeof collectionSchema]>; @@ -45,17 +45,18 @@ export class CollectionCarouselView extends CollectionSubView(CarouselDocument) onContentClick = () => ScriptCast(this.layoutDoc.onChildClick); @computed get content() { const index = NumCast(this.layoutDoc._itemIndex); - return !(this.childLayoutPairs?.[index]?.layout instanceof Doc) ? (null) : + const curDoc = this.childLayoutPairs?.[index]; + return !(curDoc?.layout instanceof Doc) ? (null) : <> <div className="collectionCarouselView-image" key="image"> - <ContentFittingDocumentView {...this.props} + <ContentFittingDocumentView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} onDoubleClick={this.onContentDoubleClick} onClick={this.onContentClick} renderDepth={this.props.renderDepth + 1} LayoutTemplate={this.props.ChildLayoutTemplate} LayoutTemplateString={this.props.ChildLayoutString} - Document={this.childLayoutPairs[index].layout} - DataDoc={this.childLayoutPairs[index].data} + Document={curDoc.layout} + DataDoc={curDoc.data} PanelHeight={this.panelHeight} ScreenToLocalTransform={this.props.ScreenToLocalTransform} bringToFront={returnFalse} @@ -71,7 +72,7 @@ export class CollectionCarouselView extends CollectionSubView(CarouselDocument) <FormattedTextBox key={index} {...this.props} xMargin={NumCast(this.layoutDoc["_carousel-caption-xMargin"])} yMargin={NumCast(this.layoutDoc["_carousel-caption-yMargin"])} - Document={this.childLayoutPairs[index].layout} DataDoc={undefined} fieldKey={"caption"} /> + Document={curDoc.layout} DataDoc={undefined} fieldKey={"caption"} /> </div> </>; } diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss index 6ebd5103b..8b1594b21 100644 --- a/src/client/views/collections/CollectionDockingView.scss +++ b/src/client/views/collections/CollectionDockingView.scss @@ -1,24 +1,5 @@ @import "../../views/globalCssVariables.scss"; -.miniMap { - position: absolute; - overflow: hidden; - right: 10; - bottom: 10; - border: solid 1px; - box-shadow: black 0.4vw 0.4vw 0.8vw; - - .miniOverlay { - width: 100%; - height: 100%; - position: absolute; - - .miniThumb { - background: #25252525; - position: absolute; - } - } -} .lm_title { margin-top: 3px; @@ -54,6 +35,7 @@ padding-right: 20px; margin-top: -1px; border-bottom: 1px black; + .collectionDockingView-gear { display: none; } @@ -63,25 +45,14 @@ padding-right: 20px; margin-top: 1px; border-bottom: unset; + .collectionDockingView-gear { display: inline-block; } } .lm_popout { - display: none; -} - -.messageCounter { - width: 18px; - height: 20px; - text-align: center; - border-radius: 20px; - margin-left: 5px; - transform: translate(0px, -8px); - display: inline-block; - background: transparent; - border: 1px #999999 solid; + display: inline; } .collectiondockingview-container { @@ -101,7 +72,7 @@ margin: auto; } - .collectionDockingView-dragAsDocument { + .collectionDockingView-drag { touch-action: none; position: absolute; padding-left: 5px; @@ -119,6 +90,10 @@ transform: scale(1.2); } + .lm_controls .lm_popout { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAUCAAAAABHICnvAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QAAKqNIzIAAAAHdElNRQfkCBsXMgbrEyzaAAAAT0lEQVQY02NgIAcIu8tgEW3/u4IDQ5B14/8LQlhFhckVFfCJjIyIOfP/QWpEZGSQJFS05s9fIPj3/z+YmseCTxS7CZS7DI+PsYcOjpAkDAA6H0KZxzDzlgAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMC0wOC0yN1QyMzo1MDowNi0wNDowMDvgVpQAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjAtMDgtMjdUMjM6NTA6MDYtMDQ6MDBKve4oAAAAAElFTkSuQmCC) + } + .lm_maximised .lm_controls .lm_maximise { opacity: 1; transform: scale(0.8); diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 43da0d3cf..2eaa284cc 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -1,46 +1,33 @@ import 'golden-layout/src/css/goldenlayout-base.css'; import 'golden-layout/src/css/goldenlayout-dark-theme.css'; -import { action, computed, Lambda, observable, reaction, runInAction, trace, IReactionDisposer } from "mobx"; +import { action, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import * as ReactDOM from 'react-dom'; import * as GoldenLayout from "../../../client/goldenLayout"; -import { DateField } from '../../../fields/DateField'; -import { Doc, DocListCast, Field, Opt, DataSym } from "../../../fields/Doc"; +import { Doc, DocListCast, Opt } from "../../../fields/Doc"; import { Id } from '../../../fields/FieldSymbols'; -import { FieldId } from "../../../fields/RefField"; +import { InkTool } from '../../../fields/InkField'; +import { List } from '../../../fields/List'; import { Cast, NumCast, StrCast } from "../../../fields/Types"; -import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, returnOne, returnTrue, Utils, returnZero, returnEmptyFilter, setupMoveUpEvents, returnFalse, emptyPath, aggregateBounds } from "../../../Utils"; import { DocServer } from "../../DocServer"; import { Docs } from '../../documents/Documents'; -import { DocumentManager } from '../../util/DocumentManager'; -import { DragManager, dropActionType } from "../../util/DragManager"; +import { CurrentUserUtils } from '../../util/CurrentUserUtils'; +import { DragManager } from "../../util/DragManager"; +import { InteractionUtils } from '../../util/InteractionUtils'; import { Scripting } from '../../util/Scripting'; -import { SelectionManager } from '../../util/SelectionManager'; -import { Transform } from '../../util/Transform'; -import { undoBatch } from "../../util/UndoManager"; -import { MainView } from '../MainView'; -import { DocumentView } from "../nodes/DocumentView"; +import { undoBatch, UndoManager } from "../../util/UndoManager"; import "./CollectionDockingView.scss"; -import { SubCollectionViewProps } from "./CollectionSubView"; -import { DockingViewButtonSelector } from './ParentDocumentSelector'; -import React = require("react"); +import { CollectionSubView, SubCollectionViewProps } from "./CollectionSubView"; import { CollectionViewType } from './CollectionView'; -import { SnappingManager } from '../../util/SnappingManager'; -import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; -import { listSpec } from '../../../fields/Schema'; -import { clamp } from 'lodash'; -import { PresBox } from '../nodes/PresBox'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { InteractionUtils } from '../../util/InteractionUtils'; -import { InkTool } from '../../../fields/InkField'; +import { TabDocView } from './TabDocView'; +import React = require("react"); +import { stat } from 'fs'; const _global = (window /* browser */ || global /* node */) as any; @observer -export class CollectionDockingView extends React.Component<SubCollectionViewProps> { - @observable public static Instances: CollectionDockingView[] = []; - @computed public static get Instance() { return CollectionDockingView.Instances[0]; } - public static makeDocumentConfig(document: Doc, width?: number, libraryPath?: Doc[]) { +export class CollectionDockingView extends CollectionSubView(doc => doc) { + @observable public static Instance: CollectionDockingView; + public static makeDocumentConfig(document: Doc, panelName?: string, width?: number) { return { type: 'react-component', component: 'DocumentFrameRenderer', @@ -48,341 +35,219 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp width: width, props: { documentId: document[Id], - libraryPath: libraryPath?.map(d => d[Id]) + panelName // name of tab that can be used to close or replace its contents } }; } - @computed public get initialized() { - return this._goldenLayout !== null; - } - - @observable private _goldenLayout: any = null; + private _reactionDisposer?: IReactionDisposer; private _containerRef = React.createRef<HTMLDivElement>(); - private _flush: boolean = false; + private _flush: UndoManager.Batch | undefined; private _ignoreStateChange = ""; - private _isPointerDown = false; - private _maximizedSrc: Opt<DocumentView>; + public tabMap: Set<any> = new Set(); + public get initialized() { return this._goldenLayout !== null; } + public get HasFullScreen() { return this._goldenLayout._maximisedItem !== null; } + @observable private _goldenLayout: any = null; constructor(props: SubCollectionViewProps) { super(props); - runInAction(() => !CollectionDockingView.Instances ? CollectionDockingView.Instances = [this] : CollectionDockingView.Instances.push(this)); + runInAction(() => CollectionDockingView.Instance = this); //Why is this here? (window as any).React = React; (window as any).ReactDOM = ReactDOM; DragManager.StartWindowDrag = this.StartOtherDrag; } + public StartOtherDrag = (e: any, dragDocs: Doc[]) => { - let config: any; - if (dragDocs.length === 1) { - config = CollectionDockingView.makeDocumentConfig(dragDocs[0]); - } else { - config = { - type: 'row', - content: dragDocs.map((doc, i) => { - CollectionDockingView.makeDocumentConfig(doc); - }) - }; - } - const div = document.createElement("div"); - const dragSource = this._goldenLayout.createDragSource(div, config); - dragSource._dragListener.on("dragStop", () => { - dragSource.destroy(); - }); + !this._flush && (this._flush = UndoManager.StartBatch("golden layout drag")); + const config = dragDocs.length === 1 ? CollectionDockingView.makeDocumentConfig(dragDocs[0]) : + { type: 'row', content: dragDocs.map((doc, i) => CollectionDockingView.makeDocumentConfig(doc)) }; + const dragSource = this._goldenLayout.createDragSource(document.createElement("div"), config); + //dragSource._dragListener.on("dragStop", dragSource.destroy); dragSource._dragListener.onMouseDown(e); } @undoBatch - @action - public OpenFullScreen(docView: DocumentView, libraryPath?: Doc[]) { - if (docView.props.Document._viewType === CollectionViewType.Docking && docView.props.Document.layoutKey === "layout") { - return MainView.Instance.openWorkspace(docView.props.Document); - } - const document = Doc.MakeAlias(docView.props.Document); - const newItemStackConfig = { - type: 'stack', - content: [CollectionDockingView.makeDocumentConfig(document, undefined, libraryPath)] - }; - const docconfig = this._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, this._goldenLayout); - this._goldenLayout.root.contentItems[0].addChild(docconfig); - docconfig.callDownwards('_$init'); - this._goldenLayout._$maximiseItem(docconfig); - this._maximizedSrc = docView; - this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig()); + public CloseFullScreen = () => { + this._goldenLayout._maximisedItem?.toggleMaximise(); this.stateChanged(); - SelectionManager.DeselectAll(); } - public CloseFullScreen = () => { - const target = this._goldenLayout._maximisedItem; - if (target !== null && this._maximizedSrc) { - this._goldenLayout._maximisedItem.remove(); - SelectionManager.SelectDoc(this._maximizedSrc, false); - this._maximizedSrc = undefined; - this.stateChanged(); + @undoBatch + public static CloseSplit(document: Opt<Doc>, panelName?: string): boolean { + const tab = Array.from(CollectionDockingView.Instance.tabMap.keys()).find((tab) => panelName ? tab.contentItem.config.props.panelName === panelName : tab.DashDoc === document); + if (tab) { + const j = tab.header.parent.contentItems.indexOf(tab.contentItem); + if (j !== -1) { + tab.header.parent.contentItems[j].remove(); + return CollectionDockingView.Instance.layoutChanged(); + } } - } - public HasFullScreen = () => { - return this._goldenLayout._maximisedItem !== null; + return false; } @undoBatch - @action - public static CloseRightSplit(document: Opt<Doc>): boolean { + public static OpenFullScreen(doc: Doc, libraryPath?: Doc[]) { const instance = CollectionDockingView.Instance; - const tryClose = (childItem: any) => { - if (childItem.config?.component === "DocumentFrameRenderer") { - const docView = DocumentManager.Instance.getDocumentViewById(childItem.config.props.documentId); - if (docView && ((!document && docView.Document.isDisplayPanel) || (document && Doc.AreProtosEqual(docView.props.Document, document)))) { - childItem.remove(); - instance.layoutChanged(document); - return true; - } - } - return false; + if (doc._viewType === CollectionViewType.Docking && doc.layoutKey === "layout") { + return CurrentUserUtils.openDashboard(Doc.UserDoc(), doc); + } + const newItemStackConfig = { + type: 'stack', + content: [CollectionDockingView.makeDocumentConfig(Doc.MakeAlias(doc))] }; - const retVal = !instance?._goldenLayout.root.contentItems[0].isRow ? false : - Array.from(instance._goldenLayout.root.contentItems[0].contentItems).some((child: any) => Array.from(child.contentItems).some(tryClose)); - - retVal && instance.stateChanged(); - return retVal; + const docconfig = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout); + instance._goldenLayout.root.contentItems[0].addChild(docconfig); + docconfig.callDownwards('_$init'); + instance._goldenLayout._$maximiseItem(docconfig); + instance._goldenLayout.emit('stateChanged'); + instance._ignoreStateChange = JSON.stringify(instance._goldenLayout.toConfig()); + instance.stateChanged(); + return true; } - @action - layoutChanged(removed?: Doc) { - this._goldenLayout.root.callDownwards('setSize', [this._goldenLayout.width, this._goldenLayout.height]); - this._goldenLayout.emit('stateChanged'); - this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig()); - if (removed) CollectionDockingView.Instance._removedDocs.push(removed); - this.stateChanged(); - } @undoBatch - @action - public static ReplaceRightSplit(document: Doc, libraryPath?: Doc[], addToSplit?: boolean): boolean { - if (!CollectionDockingView.Instance) return false; + public static ReplaceTab(document: Doc, panelName: string, stack: any, addToSplit?: boolean): boolean { const instance = CollectionDockingView.Instance; - let retVal = false; - if (instance._goldenLayout.root.contentItems[0].isRow) { - retVal = Array.from(instance._goldenLayout.root.contentItems[0].contentItems).some((child: any) => { - if (child.contentItems.length === 1 && child.contentItems[0].config.component === "DocumentFrameRenderer" && - DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)?.Document.isDisplayPanel) { - const newItemStackConfig = CollectionDockingView.makeDocumentConfig(document, undefined, libraryPath); - child.addChild(newItemStackConfig, undefined); - !addToSplit && child.contentItems[0].remove(); - instance.layoutChanged(document); - return true; - } - return Array.from(child.contentItems).filter((tab: any) => tab.config.component === "DocumentFrameRenderer").some((tab: any, j: number) => { - if (DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)?.Document.isDisplayPanel) { - const newItemStackConfig = CollectionDockingView.makeDocumentConfig(document, undefined, libraryPath); - child.addChild(newItemStackConfig, undefined); - !addToSplit && child.contentItems[j].remove(); - instance.layoutChanged(document); - return true; - } - return false; - }); - }); + if (!instance) return false; + const newConfig = CollectionDockingView.makeDocumentConfig(document, panelName); + if (!panelName && stack) { + const activeContentItemIndex = stack.contentItems.findIndex((item: any) => item.config === stack._activeContentItem.config); + const newContentItem = stack.layoutManager.createContentItem(newConfig, instance._goldenLayout); + stack.addChild(newContentItem.contentItems[0], undefined); + stack.contentItems[activeContentItemIndex].remove(); + return CollectionDockingView.Instance.layoutChanged(); } - if (retVal) { - instance.stateChanged(); + const tab = Array.from(CollectionDockingView.Instance.tabMap.keys()).find((tab) => tab.contentItem.config.props.panelName === panelName); + if (tab) { + tab.header.parent.addChild(newConfig, undefined); + const j = tab.header.parent.contentItems.indexOf(tab.contentItem); + !addToSplit && j !== -1 && tab.header.parent.contentItems[j].remove(); + return CollectionDockingView.Instance.layoutChanged(); } - return retVal; + return CollectionDockingView.AddSplit(document, panelName, stack, panelName); } - // - // Creates a vertical split on the right side of the docking view, and then adds the Document to the right of that split - // - @undoBatch - @action - public static AddRightSplit(document: Doc, libraryPath?: Doc[]) { - if (!CollectionDockingView.Instance) return false; - const instance = CollectionDockingView.Instance; - const newItemStackConfig = { - type: 'stack', - content: [CollectionDockingView.makeDocumentConfig(document, undefined, libraryPath)] - }; - const newContentItem = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout); - - if (instance._goldenLayout.root.contentItems.length === 0) { - instance._goldenLayout.root.addChild(newContentItem); - } else if (instance._goldenLayout.root.contentItems[0].isRow) { - instance._goldenLayout.root.contentItems[0].addChild(newContentItem); - } else { - const collayout = instance._goldenLayout.root.contentItems[0]; - const newRow = collayout.layoutManager.createContentItem({ type: "row" }, instance._goldenLayout); - collayout.parent.replaceChild(collayout, newRow); - - newRow.addChild(newContentItem, undefined, true); - newRow.addChild(collayout, 0, true); - - collayout.config.width = 50; - newContentItem.config.width = 50; - } - newContentItem.callDownwards('_$init'); - instance.layoutChanged(); - return true; + @undoBatch + public static ToggleSplit(doc: Doc, location: string, stack?: any, panelName?: string) { + return Array.from(CollectionDockingView.Instance.tabMap.keys()).findIndex((tab) => tab.DashDoc === doc) !== -1 ? + CollectionDockingView.CloseSplit(doc) : CollectionDockingView.AddSplit(doc, location, stack, panelName); } - // // Creates a split on any side of the docking view based on the passed input pullSide and then adds the Document to the requested side // @undoBatch - @action - public static AddSplit(document: Doc, pullSide: string, libraryPath?: Doc[]) { - if (!CollectionDockingView.Instance) return false; + public static AddSplit(document: Doc, pullSide: string, stack?: any, panelName?: string) { + if (document._viewType === CollectionViewType.Docking) return CurrentUserUtils.openDashboard(Doc.UserDoc(), document); const instance = CollectionDockingView.Instance; - const newItemStackConfig = { - type: 'stack', - content: [CollectionDockingView.makeDocumentConfig(document, undefined, libraryPath)] - }; - - const newContentItem = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout); - - if (instance._goldenLayout.root.contentItems.length === 0) { // if no rows / columns - instance._goldenLayout.root.addChild(newContentItem); - } else if (instance._goldenLayout.root.contentItems[0].isRow) { // if row - if (pullSide === "left") { - instance._goldenLayout.root.contentItems[0].addChild(newContentItem, 0); - } else if (pullSide === "right") { - instance._goldenLayout.root.contentItems[0].addChild(newContentItem); - } else if (pullSide === "top" || pullSide === "bottom") { - // if not going in a row layout, must add already existing content into column - const rowlayout = instance._goldenLayout.root.contentItems[0]; - const newColumn = rowlayout.layoutManager.createContentItem({ type: "column" }, instance._goldenLayout); - rowlayout.parent.replaceChild(rowlayout, newColumn); - if (pullSide === "top") { - newColumn.addChild(rowlayout, undefined, true); - newColumn.addChild(newContentItem, 0, true); - } else if (pullSide === "bottom") { - newColumn.addChild(newContentItem, undefined, true); - newColumn.addChild(rowlayout, 0, true); - } + if (!instance) return false; + const docContentConfig = CollectionDockingView.makeDocumentConfig(document, panelName); - rowlayout.config.height = 50; - newContentItem.config.height = 50; - } - } else if (instance._goldenLayout.root.contentItems[0].isColumn) { // if column - if (pullSide === "top") { - instance._goldenLayout.root.contentItems[0].addChild(newContentItem, 0); - } else if (pullSide === "bottom") { - instance._goldenLayout.root.contentItems[0].addChild(newContentItem); - } else if (pullSide === "left" || pullSide === "right") { - // if not going in a row layout, must add already existing content into column - const collayout = instance._goldenLayout.root.contentItems[0]; - const newRow = collayout.layoutManager.createContentItem({ type: "row" }, instance._goldenLayout); - collayout.parent.replaceChild(collayout, newRow); - - if (pullSide === "left") { - newRow.addChild(collayout, undefined, true); - newRow.addChild(newContentItem, 0, true); - } else if (pullSide === "right") { - newRow.addChild(newContentItem, undefined, true); - newRow.addChild(collayout, 0, true); - } - - collayout.config.width = 50; - newContentItem.config.width = 50; + if (!pullSide && stack) { + stack.addChild(docContentConfig, undefined); + stack.setActiveContentItem(stack.contentItems[stack.contentItems.length - 1]); + } else { + const newItemStackConfig = { type: 'stack', content: [docContentConfig] }; + const newContentItem = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout); + if (instance._goldenLayout.root.contentItems.length === 0) { // if no rows / columns + instance._goldenLayout.root.addChild(newContentItem); + } else if (instance._goldenLayout.root.contentItems[0].isStack) { + instance._goldenLayout.root.contentItems[0].addChild(docContentConfig); + } else if (instance._goldenLayout.root.contentItems.length === 1 && instance._goldenLayout.root.contentItems[0].contentItems.length === 1 && + instance._goldenLayout.root.contentItems[0].contentItems[0].contentItems.length === 0) { + instance._goldenLayout.root.contentItems[0].contentItems[0].addChild(docContentConfig); } - } - - newContentItem.callDownwards('_$init'); - instance.layoutChanged(); - return true; - } - + else if (instance._goldenLayout.root.contentItems[0].isRow) { // if row + switch (pullSide) { + default: + case "right": instance._goldenLayout.root.contentItems[0].addChild(newContentItem); break; + case "left": instance._goldenLayout.root.contentItems[0].addChild(newContentItem, 0); break; + case "top": + case "bottom": + // if not going in a row layout, must add already existing content into column + const rowlayout = instance._goldenLayout.root.contentItems[0]; + const newColumn = rowlayout.layoutManager.createContentItem({ type: "column" }, instance._goldenLayout); + rowlayout.parent.replaceChild(rowlayout, newColumn); + if (pullSide === "top") { + newColumn.addChild(rowlayout, undefined, true); + newColumn.addChild(newContentItem, 0, true); + } else if (pullSide === "bottom") { + newColumn.addChild(newContentItem, undefined, true); + newColumn.addChild(rowlayout, 0, true); + } - // - // Creates a vertical split on the right side of the docking view, and then adds the Document to that split - // - @undoBatch - @action - public static UseRightSplit(document: Doc, libraryPath?: Doc[], shiftKey?: boolean) { - document.isDisplayPanel = true; - if (shiftKey || !CollectionDockingView.ReplaceRightSplit(document, libraryPath, shiftKey)) { - CollectionDockingView.AddRightSplit(document, libraryPath); - } - } + rowlayout.config.height = 50; + newContentItem.config.height = 50; + } + } else {// if (instance._goldenLayout.root.contentItems[0].isColumn) { // if column + switch (pullSide) { + case "top": instance._goldenLayout.root.contentItems[0].addChild(newContentItem, 0); break; + case "bottom": instance._goldenLayout.root.contentItems[0].addChild(newContentItem); break; + case "left": + case "right": + default: + // if not going in a row layout, must add already existing content into column + const collayout = instance._goldenLayout.root.contentItems[0]; + const newRow = collayout.layoutManager.createContentItem({ type: "row" }, instance._goldenLayout); + collayout.parent.replaceChild(collayout, newRow); + + if (pullSide === "left") { + newRow.addChild(collayout, undefined, true); + newRow.addChild(newContentItem, 0, true); + } else { + newRow.addChild(newContentItem, undefined, true); + newRow.addChild(collayout, 0, true); + } - @undoBatch - @action - public AddTab = (stack: any, document: Doc, libraryPath?: Doc[]) => { - Doc.GetProto(document).lastOpened = new DateField; - const docContentConfig = CollectionDockingView.makeDocumentConfig(document, undefined, libraryPath); - if (stack === undefined) { - let stack: any = this._goldenLayout.root; - while (!stack.isStack) { - if (stack.contentItems.length) { - stack = stack.contentItems[0]; - } else { - stack.addChild({ type: 'stack', content: [docContentConfig] }); - stack = undefined; - break; + collayout.config.width = 50; + newContentItem.config.width = 50; } } - if (stack) { - stack.addChild(docContentConfig); - } - } else { - stack.addChild(docContentConfig, undefined); + instance._ignoreStateChange = JSON.stringify(instance._goldenLayout.toConfig()); + newContentItem.callDownwards('_$init'); } - this.layoutChanged(); - return true; + + return instance.layoutChanged(); } @undoBatch @action - public ReplaceTab = (stack: any, document: Doc, libraryPath?: Doc[]) => { - Doc.GetProto(document).lastOpened = new DateField; - const docContentConfig = CollectionDockingView.makeDocumentConfig(document, undefined, libraryPath); - if (stack === undefined) { - let stack: any = this._goldenLayout.root; - while (!stack.isStack) { - if (stack.contentItems.length) { - stack = stack.contentItems[0]; - } else { - stack.addChild({ type: 'stack', content: [docContentConfig] }); - stack = undefined; - break; - } - } - if (stack) { - stack.addChild(docContentConfig); - } - } else { - stack.addChild(docContentConfig, undefined); - } - this.layoutChanged(); + layoutChanged() { + this._goldenLayout.root.callDownwards('setSize', [this._goldenLayout.width, this._goldenLayout.height]); + this._goldenLayout.emit('stateChanged'); + this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig()); + this.stateChanged(); return true; } - setupGoldenLayout() { + async setupGoldenLayout() { const config = StrCast(this.props.Document.dockingConfig); if (config) { - if (!this._goldenLayout) { - runInAction(() => this._goldenLayout = new GoldenLayout(JSON.parse(config))); - } - else { + const matches = config.match(/\"documentId\":\"[a-z0-9-]+\"/g); + const docids = matches?.map(m => m.replace("\"documentId\":\"", "").replace("\"", "")) ?? []; + await Promise.all(docids.map(id => DocServer.GetRefField(id))); + + if (this._goldenLayout) { if (config === JSON.stringify(this._goldenLayout.toConfig())) { return; + } else { + try { + this._goldenLayout.unbind('tabCreated', this.tabCreated); + this._goldenLayout.unbind('tabDestroyed', this.tabDestroyed); + this._goldenLayout.unbind('stackCreated', this.stackCreated); + } catch (e) { } } - try { - this._goldenLayout.unbind('itemDropped', this.itemDropped); - this._goldenLayout.unbind('tabCreated', this.tabCreated); - this._goldenLayout.unbind('tabDestroyed', this.tabDestroyed); - this._goldenLayout.unbind('stackCreated', this.stackCreated); - } catch (e) { } - this._goldenLayout.destroy(); - runInAction(() => this._goldenLayout = new GoldenLayout(JSON.parse(config))); } - this._goldenLayout.on('itemDropped', this.itemDropped); + this.tabMap.clear(); + this._goldenLayout?.destroy(); + runInAction(() => this._goldenLayout = new GoldenLayout(JSON.parse(config))); this._goldenLayout.on('tabCreated', this.tabCreated); this._goldenLayout.on('tabDestroyed', this.tabDestroyed); this._goldenLayout.on('stackCreated', this.stackCreated); - this._goldenLayout.registerComponent('DocumentFrameRenderer', DockedFrameRenderer); + this._goldenLayout.registerComponent('DocumentFrameRenderer', TabDocView); this._goldenLayout.container = this._containerRef.current; if (this._goldenLayout.config.maximisedItemId === '__glMaximised') { try { @@ -394,582 +259,156 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp this._goldenLayout.init(); } } - reactionDisposer?: Lambda; + componentDidMount: () => void = () => { if (this._containerRef.current) { - const observer = new _global.ResizeObserver(action((entries: any) => { - for (const entry of entries) { - this.onResize(null as any); - } - })); - observer.observe(this._containerRef.current); - this.reactionDisposer = reaction( - () => this.props.Document.dockingConfig, - () => { - if (!this._goldenLayout || this._ignoreStateChange !== JSON.stringify(this._goldenLayout.toConfig())) { - // Because this is in a set timeout, if this component unmounts right after mounting, - // we will leak a GoldenLayout, because we try to destroy it before we ever create it - setTimeout(() => this.setupGoldenLayout(), 1); - DocListCast((Doc.UserDoc().myWorkspaces as Doc).data).map(d => d.workspaceBrush = false); - this.props.Document.workspaceBrush = true; + new _global.ResizeObserver(this.onResize).observe(this._containerRef.current); + this._reactionDisposer = reaction(() => StrCast(this.props.Document.dockingConfig), + config => { + if (!this._goldenLayout || this._ignoreStateChange !== config) { // bcz: TODO! really need to diff config with ignoreStateChange and modify the current goldenLayout instead of building a new one. + this.setupGoldenLayout(); } this._ignoreStateChange = ""; - }, { fireImmediately: true }); - - window.addEventListener('resize', this.onResize); // bcz: would rather add this event to the parent node, but resize events only come from Window + }); + setTimeout(() => this.setupGoldenLayout(), 0); + //window.addEventListener('resize', this.onResize); // bcz: would rather add this event to the parent node, but resize events only come from Window } } + componentWillUnmount: () => void = () => { try { - this.props.Document.workspaceBrush = false; - this._goldenLayout.unbind('itemDropped', this.itemDropped); - this._goldenLayout.unbind('tabCreated', this.tabCreated); this._goldenLayout.unbind('stackCreated', this.stackCreated); this._goldenLayout.unbind('tabDestroyed', this.tabDestroyed); - } catch (e) { - - } - this._goldenLayout && this._goldenLayout.destroy(); - runInAction(() => { - CollectionDockingView.Instances.splice(CollectionDockingView.Instances.indexOf(this), 1); - this._goldenLayout = null; - }); + } catch (e) { } + this._goldenLayout?.destroy(); window.removeEventListener('resize', this.onResize); - this.reactionDisposer && this.reactionDisposer(); + this._reactionDisposer?.(); } + @action onResize = (event: any) => { const cur = this._containerRef.current; - // bcz: since GoldenLayout isn't a React component itself, we need to notify it to resize when its document container's size has changed - this._goldenLayout?.updateSize(cur!.getBoundingClientRect().width, cur!.getBoundingClientRect().height); + cur && this._goldenLayout?.updateSize(cur.getBoundingClientRect().width, cur.getBoundingClientRect().height); } @action - onPointerUp = (e: React.PointerEvent): void => { + onPointerUp = (e: MouseEvent): void => { + window.removeEventListener("pointerup", this.onPointerUp); if (this._flush) { - this._flush = false; setTimeout(() => { CollectionDockingView.Instance._ignoreStateChange = JSON.stringify(CollectionDockingView.Instance._goldenLayout.toConfig()); this.stateChanged(); + this._flush?.end(); + this._flush = undefined; }, 10); } } + @action onPointerDown = (e: React.PointerEvent): void => { - this._isPointerDown = true; - const onPointerUp = action(() => { - window.removeEventListener("pointerup", onPointerUp); - this._isPointerDown = false; - }); - window.addEventListener("pointerup", onPointerUp); - const className = (e.target as any).className; - if (className === "lm_drag_handle" || className === "lm_close" || className === "lm_maximise" || className === "lm_minimise" || className === "lm_close_tab") { - this._flush = true; + window.addEventListener("mouseup", this.onPointerUp); + if (!(e.target as HTMLElement).closest("*.lm_content") && ((e.target as HTMLElement).closest("*.lm_tab") || (e.target as HTMLElement).closest("*.lm_stack"))) { + this._flush = UndoManager.StartBatch("golden layout edit"); } - if (e.nativeEvent.cancelBubble || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (Doc.GetSelectedTool() === InkTool.Highlighter || Doc.GetSelectedTool() === InkTool.Pen)) { - return; - } else { + if (!e.nativeEvent.cancelBubble && !InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE) && + Doc.GetSelectedTool() !== InkTool.Highlighter && Doc.GetSelectedTool() !== InkTool.Pen) { e.stopPropagation(); } } - updateDataField = async (json: string) => { + public static Copy(doc: Doc) { + let json = StrCast(doc.dockingConfig); const matches = json.match(/\"documentId\":\"[a-z0-9-]+\"/g); - const docids = matches?.map(m => m.replace("\"documentId\":\"", "").replace("\"", "")); + const docids = matches?.map(m => m.replace("\"documentId\":\"", "").replace("\"", "")) || []; + const docs = docids.map(id => DocServer.GetCachedRefField(id)).filter(f => f).map(f => f as Doc); + const newtabs = docs.map(doc => { + const copy = Doc.MakeAlias(doc); + json = json.replace(doc[Id], copy[Id]); + return copy; + }); + const copy = Docs.Create.DockDocument(newtabs, json, { title: "Snapshot: " + doc.title }); + const docsublists = DocListCast(doc.data); + const copysublists = DocListCast(copy.data); + const docother = Cast(docsublists[1], Doc, null); + const copyother = Cast(copysublists[1], Doc, null); + const newother = DocListCast(docother.data).map(doc => Doc.MakeAlias(doc)); + Doc.GetProto(copyother).data = new List<Doc>(newother); - if (docids) { - const docs = (await Promise.all(docids.map(id => DocServer.GetRefField(id)))).filter(f => f).map(f => f as Doc); - docs.map(doc => Doc.AddDocToList(Doc.GetProto(this.props.Document), this.props.fieldKey, doc)); - // Doc.GetProto(this.props.Document)[this.props.fieldKey] = new List<Doc>(docs); - } + return copy; } - @undoBatch + @action stateChanged = () => { const json = JSON.stringify(this._goldenLayout.toConfig()); - this.props.Document.dockingConfig = json; - this.updateDataField(json); - } - - itemDropped = () => { - CollectionDockingView.Instance._ignoreStateChange = JSON.stringify(CollectionDockingView.Instance._goldenLayout.toConfig()); - this.stateChanged(); - } - - htmlToElement(html: string) { - const template = document.createElement('template'); - html = html.trim(); // Never return a text node of whitespace as the result - template.innerHTML = html; - return template.content.firstChild; - } - - tabCreated = async (tab: any) => { - tab.titleElement[0].Tab = tab; - if (tab.hasOwnProperty("contentItem") && tab.contentItem.config.type !== "stack") { - if (tab.contentItem.config.fixed) { - tab.contentItem.parent.config.fixed = true; - } + const matches = json.match(/\"documentId\":\"[a-z0-9-]+\"/g); + const docids = matches?.map(m => m.replace("\"documentId\":\"", "").replace("\"", "")); + const docs = !docids ? [] : docids.map(id => DocServer.GetCachedRefField(id)).filter(f => f).map(f => f as Doc); - const doc = await DocServer.GetRefField(tab.contentItem.config.props.documentId) as Doc; - if (doc instanceof Doc) { - tab.titleElement[0].onclick = (e: any) => { - if (Date.now() - tab.titleElement[0].lastClick < 1000) tab.titleElement[0].select(); - tab.titleElement[0].lastClick = Date.now(); - tab.titleElement[0].focus(); - }; - tab.titleElement[0].onchange = (e: any) => { - tab.titleElement[0].size = e.currentTarget.value.length + 1; - Doc.GetProto(doc).title = e.currentTarget.value, true; - }; - tab.titleElement[0].size = StrCast(doc.title).length + 1; - tab.titleElement[0].value = doc.title; - tab.titleElement[0].style["max-width"] = "100px"; - const gearSpan = document.createElement("span"); - gearSpan.className = "collectionDockingView-gear"; - gearSpan.style.position = "relative"; - gearSpan.style.paddingLeft = "0px"; - gearSpan.style.paddingRight = "12px"; - const stack = tab.contentItem.parent; - tab.element[0].onpointerdown = (e: any) => { - const view = DocumentManager.Instance.getDocumentView(doc); - view && SelectionManager.SelectDoc(view, false); - }; - // shifts the focus to this tab when another tab is dragged over it - tab.element[0].onmouseenter = (e: any) => { - if (!this._isPointerDown || !SnappingManager.GetIsDragging()) return; - const activeContentItem = tab.header.parent.getActiveContentItem(); - if (tab.contentItem !== activeContentItem) { - tab.header.parent.setActiveContentItem(tab.contentItem); - } - tab.setActive(true); - }; - const onDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, (e) => { - if (!(e as any).defaultPrevented) { - const dragData = new DragManager.DocumentDragData([doc]); - dragData.dropAction = doc.dropAction as dropActionType; - DragManager.StartDocumentDrag([gearSpan], dragData, e.clientX, e.clientY); - return true; - } - return false; - }, returnFalse, emptyFunction); - }; - - tab.buttonDisposer = reaction(() => ((view: Opt<DocumentView>) => view ? [view] : [])(DocumentManager.Instance.getDocumentView(doc)), - (views) => { - if (views.length) { - ReactDOM.render(<span title="Drag as document" className="collectionDockingView-dragAsDocument" onPointerDown={onDown} > - <DockingViewButtonSelector views={() => views} Stack={stack} /> - </span>, - gearSpan); - tab.buttonDisposer?.(); - } - }, { fireImmediately: true }); - - tab.reactComponents = [gearSpan]; - tab.element.append(gearSpan); - tab.reactionDisposer = reaction(() => ({ title: doc.title, degree: Doc.IsBrushedDegree(doc) }), ({ title, degree }) => { - tab.titleElement[0].textContent = title, { fireImmediately: true }; - tab.titleElement[0].style.padding = degree ? 0 : 2; - tab.titleElement[0].style.border = `${["gray", "gray", "gray"][degree]} ${["none", "dashed", "solid"][degree]} 2px`; - }); - //TODO why can't this just be doc instead of the id? - tab.titleElement[0].DashDocId = tab.contentItem.config.props.documentId; - } - } - tab.closeElement.off('click') //unbind the current click handler - .click(async function () { - tab.reactionDisposer?.(); - tab.buttonDisposer?.(); - const doc = await DocServer.GetRefField(tab.contentItem.config.props.documentId); - if (doc instanceof Doc) { - const theDoc = doc; - CollectionDockingView.Instance._removedDocs.push(theDoc); - - const recent = await Cast(Doc.UserDoc().myRecentlyClosed, Doc); - if (recent) { - Doc.AddDocToList(recent, "data", doc, undefined, true, true); - } - SelectionManager.DeselectAll(); - } - CollectionDockingView.Instance._ignoreStateChange = JSON.stringify(CollectionDockingView.Instance._goldenLayout.toConfig()); - tab.contentItem.remove(); - CollectionDockingView.Instance._ignoreStateChange = JSON.stringify(CollectionDockingView.Instance._goldenLayout.toConfig()); - }); + this.props.Document.dockingConfig = json; + const sublists = DocListCast(this.props.Document[this.props.fieldKey]); + const tabs = Cast(sublists[0], Doc, null); + const other = Cast(sublists[1], Doc, null); + const tabdocs = DocListCast(tabs.data); + const otherdocs = DocListCast(other.data); + Doc.GetProto(tabs).data = new List<Doc>(docs); + const otherSet = new Set<Doc>(); + otherdocs.filter(doc => !docs.includes(doc)).forEach(doc => otherSet.add(doc)); + tabdocs.filter(doc => !docs.includes(doc)).forEach(doc => otherSet.add(doc)); + Doc.GetProto(other).data = new List<Doc>(Array.from(otherSet.values())); } tabDestroyed = (tab: any) => { - if (tab.reactComponents) { - for (const ele of tab.reactComponents) { - ReactDOM.unmountComponentAtNode(ele); - } - } + this.tabMap.delete(tab); + tab._disposers && Object.values(tab._disposers).forEach((disposer: any) => disposer?.()); + tab.reactComponents?.forEach((ele: any) => ReactDOM.unmountComponentAtNode(ele)); + } + tabCreated = (tab: any) => { + tab.contentItem.element[0]?.firstChild?.firstChild?.InitTab?.(tab); // have to explicitly initialize tabs that reuse contents from previous abs (ie, when dragging a tab around a new tab is created for the old content) } - _removedDocs: Doc[] = []; stackCreated = (stack: any) => { - //stack.header.controlsContainer.find('.lm_popout').hide(); - stack.header.element.on('mousedown', (e: any) => { - if (e.target === stack.header.element[0] && e.button === 1) { - this.AddTab(stack, Docs.Create.FreeformDocument([], { _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), title: "Untitled Collection" })); + stack.header?.element.on('mousedown', (e: any) => { + if (e.target === stack.header?.element[0] && e.button === 2) { + const emptyPane = CurrentUserUtils.EmptyPane; + emptyPane["dragFactory-count"] = NumCast(emptyPane["dragFactory-count"]) + 1; + CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([], { + _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), title: `Untitled Tab ${NumCast(emptyPane["dragFactory-count"])}` + }), "", stack); } }); - // starter code for bezel to add new pane - // stack.element.on("touchstart", (e: TouchEvent) => { - // if (e.targetTouches.length === 2) { - // let pt1 = e.targetTouches.item(0); - // let pt2 = e.targetTouches.item(1); - // let threshold = 40 * window.devicePixelRatio; - // if (pt1 && pt2 && InteractionUtils.TwoPointEuclidist(pt1, pt2) < threshold) { - // let edgeThreshold = 30 * window.devicePixelRatio; - // let center = InteractionUtils.CenterPoint([pt1, pt2]); - // let stackRect: DOMRect = stack.element.getBoundingClientRect(); - // let nearLeft = center.X - stackRect.x < edgeThreshold; - // let nearTop = center.Y - stackRect.y < edgeThreshold; - // let nearRight = stackRect.right - center.X < edgeThreshold; - // let nearBottom = stackRect.bottom - center.Y < edgeThreshold; - // let ns = [nearLeft, nearTop, nearRight, nearBottom].filter(n => n); - // if (ns.length === 1) { - - // } - // } - // } - // }); - stack.header.controlsContainer.find('.lm_close') //get the close icon + stack.header?.controlsContainer.find('.lm_close') //get the close icon .off('click') //unbind the current click handler - .click(action(async function () { + .click(action(() => { //if (confirm('really close this?')) { - - stack.remove(); - stack.contentItems.forEach(async (contentItem: any) => { - const doc = await DocServer.GetRefField(contentItem.config.props.documentId); - if (doc instanceof Doc) { - let recent: Doc | undefined; - if (recent = await Cast(Doc.UserDoc().myRecentlyClosed, Doc)) { - Doc.AddDocToList(recent, "data", doc, undefined, true, true); - } - const theDoc = doc; - CollectionDockingView.Instance._removedDocs.push(theDoc); - } - }); - //} + if (!stack.parent.parent.isRoot || stack.parent.contentItems.length > 1) { + stack.remove(); + stack.contentItems.forEach((contentItem: any) => Doc.AddDocToList(CurrentUserUtils.MyRecentlyClosed, "data", contentItem.tab.DashDoc, undefined, true, true)); + } else { + alert('cant delete the last stack'); + } })); - stack.header.controlsContainer.find('.lm_popout') //get the close icon + stack.header?.controlsContainer.find('.lm_popout') //get the close icon .off('click') //unbind the current click handler - .click(action(function () { - stack.config.fixed = !stack.config.fixed; - // const url = Utils.prepend("/doc/" + stack.contentItems[0].tab.contentItem.config.props.documentId); - // let win = window.open(url, stack.contentItems[0].tab.title, "width=300,height=400"); + .click(action(() => { + // stack.config.fixed = !stack.config.fixed; // force the stack to have a fixed size + const emptyPane = CurrentUserUtils.EmptyPane; + emptyPane["dragFactory-count"] = NumCast(emptyPane["dragFactory-count"]) + 1; + CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([], { + _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), title: `Untitled Tab ${NumCast(emptyPane["dragFactory-count"])}` + }), "", stack); })); } render() { - if (this.props.renderDepth > 0) { - return <div style={{ width: "100%", height: "100%" }}>Nested workspaces can't be rendered</div>; - } - return <div className="collectiondockingview-container" id="menuContainer" - onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} ref={this._containerRef} />; - } - -} - -interface DockedFrameProps { - documentId: FieldId; - glContainer: any; - libraryPath: (FieldId[]); - //collectionDockingView: CollectionDockingView -} -@observer -export class DockedFrameRenderer extends React.Component<DockedFrameProps> { - _mainCont: HTMLDivElement | null = null; - @observable private _libraryPath: Doc[] = []; - @observable private _panelWidth = 0; - @observable private _panelHeight = 0; - @observable private _document: Opt<Doc>; - @observable private _isActive: boolean = false; - _tabReaction: IReactionDisposer | undefined; - - get _stack(): any { - return (this.props as any).glContainer.parent.parent; - } - get _tab(): any { - const tab = (this.props as any).glContainer.tab?.element[0] as HTMLElement; - return tab?.getElementsByClassName("lm_title")?.[0]; - } - constructor(props: any) { - super(props); - DocServer.GetRefField(this.props.documentId).then(action((f: Opt<Field>) => this._document = f as Doc)); - this.props.libraryPath && this.setupLibraryPath(); - } - - async setupLibraryPath() { - Promise.all(this.props.libraryPath.map(async docid => { - const d = await DocServer.GetRefField(docid); - return d instanceof Doc ? d : undefined; - })).then(action((list: (Doc | undefined)[]) => this._libraryPath = list.filter(d => d).map(d => d as Doc))); - } - - /** - * Adds a document to the presentation view - **/ - @undoBatch - @action - public static PinDoc(doc: Doc, unpin = false) { - if (unpin) DockedFrameRenderer.UnpinDoc(doc); - else { - //add this new doc to props.Document - const curPres = Cast(Doc.UserDoc().activePresentation, Doc) as Doc; - if (curPres) { - const pinDoc = Doc.MakeAlias(doc); - pinDoc.presentationTargetDoc = doc; - pinDoc.presZoomButton = true; - pinDoc.context = curPres; - Doc.AddDocToList(curPres, "data", pinDoc); - if (curPres.expandBoolean) pinDoc.presExpandInlineButton = true; - if (!DocumentManager.Instance.getDocumentView(curPres)) { - CollectionDockingView.AddRightSplit(curPres); - } - DocumentManager.Instance.jumpToDocument(doc, false, undefined, Cast(doc.context, Doc, null)); - } - } - } - /** - * Adds a document to the presentation view - **/ - @undoBatch - @action - public static UnpinDoc(doc: Doc) { - //add this new doc to props.Document - const curPres = Cast(Doc.UserDoc().activePresentation, Doc) as Doc; - if (curPres) { - const ind = DocListCast(curPres.data).findIndex((val) => Doc.AreProtosEqual(val, doc)); - ind !== -1 && Doc.RemoveDocFromList(curPres, "data", DocListCast(curPres.data)[ind]); - } - } - - componentDidMount() { - const observer = new _global.ResizeObserver(action((entries: any) => { - for (const entry of entries) { - this._panelWidth = entry.contentRect.width; - this._panelHeight = entry.contentRect.height; - } - })); - observer.observe(this.props.glContainer._element[0]); - this.props.glContainer.layoutManager.on("activeContentItemChanged", this.onActiveContentItemChanged); - this.props.glContainer.on("tab", this.onActiveContentItemChanged); - this.onActiveContentItemChanged(); - this._tabReaction = reaction(() => ({ views: SelectionManager.SelectedDocuments(), color: StrCast(this._document?._backgroundColor, this._document && CollectionDockingView.Instance?.props.backgroundColor?.(this._document, 0) || "white") }), - (data) => { - const selected = data.views.some(v => Doc.AreProtosEqual(v.props.Document, this._document)); - this._tab && (this._tab.style.backgroundColor = selected ? data.color : ""); - } - ); - } - - componentWillUnmount() { - this._tabReaction?.(); - this.props.glContainer.layoutManager.off("activeContentItemChanged", this.onActiveContentItemChanged); - this.props.glContainer.off("tab", this.onActiveContentItemChanged); - } - - @action.bound - private onActiveContentItemChanged() { - if (this.props.glContainer.tab) { - this._isActive = this.props.glContainer.tab.isActive; - setTimeout(() => { - const dv = this._document && DocumentManager.Instance.getFirstDocumentView(this._document); - dv && SelectionManager.SelectDoc(dv, 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. - } - } - - get layoutDoc() { return this._document && Doc.Layout(this._document); } - nativeAspect = () => this.nativeWidth() ? this.nativeWidth() / this.nativeHeight() : 0; - panelWidth = () => this.layoutDoc?.maxWidth ? Math.min(Math.max(NumCast(this.layoutDoc._width), NumCast(this.layoutDoc._nativeWidth)), this._panelWidth) : - (this.nativeAspect() && this.nativeAspect() < this._panelWidth / this._panelHeight ? this._panelHeight * this.nativeAspect() : this._panelWidth) - panelHeight = () => this.nativeAspect() && this.nativeAspect() > this._panelWidth / this._panelHeight ? this._panelWidth / this.nativeAspect() : this._panelHeight; - - nativeWidth = () => !this.layoutDoc!._fitWidth ? NumCast(this.layoutDoc!._nativeWidth) || this._panelWidth : 0; - nativeHeight = () => !this.layoutDoc!._fitWidth ? NumCast(this.layoutDoc!._nativeHeight) || this._panelHeight : 0; - - contentScaling = () => { - const nativeH = this.nativeHeight(); - const nativeW = this.nativeWidth(); - let scaling = 1; - if (!this.layoutDoc?._fitWidth && (!nativeW || !nativeH)) { - scaling = 1; - } else if (NumCast(this.layoutDoc!._nativeWidth) && ((this.layoutDoc?._fitWidth) || - this._panelHeight / NumCast(this.layoutDoc!._nativeHeight) > this._panelWidth / NumCast(this.layoutDoc!._nativeWidth))) { - scaling = this._panelWidth / NumCast(this.layoutDoc!._nativeWidth); - } else if (nativeW && nativeH) { - // if (this.layoutDoc!.type === DocumentType.PDF || this.layoutDoc!.type === DocumentType.WEB) { - // if ((this.layoutDoc?._fitWidth) || - // this._panelHeight / NumCast(this.layoutDoc!._nativeHeight) > this._panelWidth / NumCast(this.layoutDoc!._nativeWidth)) { - // return this._panelWidth / NumCast(this.layoutDoc!._nativeWidth); - // } else { - // return this._panelHeight / NumCast(this.layoutDoc!._nativeHeight); - // } - // } - const wscale = this.panelWidth() / nativeW; - scaling = wscale * nativeH > this._panelHeight ? this._panelHeight / nativeH : wscale; - } else scaling = 1; - return scaling; - } - - ScreenToLocalTransform = () => { - if (this._mainCont && this._mainCont.children) { - const { translateX, translateY } = Utils.GetScreenTransform(this._mainCont.children[0].firstChild as HTMLElement); - const scale = Utils.GetScreenTransform(this._mainCont).scale; - return CollectionDockingView.Instance?.props.ScreenToLocalTransform().translate(-translateX, -translateY).scale(1 / this.contentScaling() / scale); - } - return Transform.Identity(); - } - get previewPanelCenteringOffset() { return this.nativeWidth() ? (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2 : 0; } - get widthpercent() { return this.nativeWidth() ? `${(this.nativeWidth() * this.contentScaling()) / this._panelWidth * 100}%` : undefined; } - - addDocTab = (doc: Doc, location: string, libraryPath?: Doc[]) => { - SelectionManager.DeselectAll(); - if (doc._viewType === CollectionViewType.Docking && doc.layoutKey === "layout") { - return MainView.Instance.openWorkspace(doc); - } else if (location === "onRight") { - return CollectionDockingView.AddRightSplit(doc, libraryPath); - } else if (location === "close") { - return CollectionDockingView.CloseRightSplit(doc); - } else if (location === "replace") { - CollectionDockingView.UseRightSplit(doc); - return true; - } else {// if (location === "inPlace") { - return CollectionDockingView.Instance.AddTab(this._stack, doc, libraryPath); - } - } - - @computed get renderContentBounds() { - const bounds = this._document ? Cast(this._document._renderContentBounds, listSpec("number"), [0, 0, this.returnMiniSize(), this.returnMiniSize()]) : [0, 0, 0, 0]; - const xbounds = bounds[2] - bounds[0]; - const ybounds = bounds[3] - bounds[1]; - const dim = Math.max(xbounds, ybounds); - return { l: bounds[0] + xbounds / 2 - dim / 2, t: bounds[1] + ybounds / 2 - dim / 2, cx: bounds[0] + xbounds / 2, cy: bounds[1] + ybounds / 2, dim }; - } - @computed get miniLeft() { return 50 + (NumCast(this._document?._panX) - this.renderContentBounds.cx) / this.renderContentBounds.dim * 100 - this.miniWidth / 2; } - @computed get miniTop() { return 50 + (NumCast(this._document?._panY) - this.renderContentBounds.cy) / this.renderContentBounds.dim * 100 - this.miniHeight / 2; } - @computed get miniWidth() { return this.panelWidth() / NumCast(this._document?._viewScale, 1) / this.renderContentBounds.dim * 100; } - @computed get miniHeight() { return this.panelHeight() / NumCast(this._document?._viewScale, 1) / this.renderContentBounds.dim * 100; } - childLayoutTemplate = () => Cast(this._document?.childLayoutTemplate, Doc, null); - returnMiniSize = () => NumCast(this._document?._miniMapSize, 150); - miniDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => { - this._document!._panX = clamp(NumCast(this._document!._panX) + delta[0] / this.returnMiniSize() * this.renderContentBounds.dim, this.renderContentBounds.l, this.renderContentBounds.l + this.renderContentBounds.dim); - this._document!._panY = clamp(NumCast(this._document!._panY) + delta[1] / this.returnMiniSize() * this.renderContentBounds.dim, this.renderContentBounds.t, this.renderContentBounds.t + this.renderContentBounds.dim); - return false; - }), emptyFunction, emptyFunction); - } - getCurrentFrame = (): number => { - const presTargetDoc = Cast(PresBox.Instance.childDocs[PresBox.Instance.itemIndex].presentationTargetDoc, Doc, null); - const currentFrame = Cast(presTargetDoc.currentFrame, "number", null); - return currentFrame; - } - - renderMiniMap() { - return <div className="miniMap" style={{ - width: this.returnMiniSize(), height: this.returnMiniSize(), background: StrCast(this._document!._backgroundColor, - StrCast(this._document!.backgroundColor, CollectionDockingView.Instance.props.backgroundColor?.(this._document!, 0))), - }}> - <CollectionFreeFormView - Document={this._document!} - LibraryPath={emptyPath} - CollectionView={undefined} - ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} - ChildLayoutTemplate={this.childLayoutTemplate} // bcz: Ugh .. should probably be rendering a CollectionView or the minimap should be part of the collectionFreeFormView to avoid havin to set stuff like this. - noOverlay={true} // don't render overlay Docs since they won't scale - active={returnTrue} - select={emptyFunction} - dropAction={undefined} - isSelected={returnFalse} - dontRegisterView={true} - annotationsKey={""} - fieldKey={Doc.LayoutFieldKey(this._document!)} - bringToFront={emptyFunction} - rootSelected={returnTrue} - addDocument={returnFalse} - moveDocument={returnFalse} - removeDocument={returnFalse} - ContentScaling={returnOne} - PanelWidth={this.returnMiniSize} - PanelHeight={this.returnMiniSize} - NativeHeight={returnZero} - NativeWidth={returnZero} - ScreenToLocalTransform={this.ScreenToLocalTransform} - renderDepth={0} - whenActiveChanged={emptyFunction} - focus={emptyFunction} - backgroundColor={CollectionDockingView.Instance.props.backgroundColor} - addDocTab={this.addDocTab} - pinToPres={DockedFrameRenderer.PinDoc} - docFilters={returnEmptyFilter} - fitToBox={true} - /> - <div className="miniOverlay" onPointerDown={this.miniDown} > - <div className="miniThumb" style={{ - width: `${this.miniWidth}%`, - height: `${this.miniHeight}%`, - left: `${this.miniLeft}%`, - top: `${this.miniTop}%`, - }} - /> - </div> + return <div className="collectiondockingview-container" onPointerDown={this.onPointerDown} ref={this._containerRef}> + {this.props.renderDepth > 0 ? "Nested dashboards can't be rendered" : (null)} </div>; } - @computed get docView() { - TraceMobx(); - if (!this._document) return (null); - const document = this._document; - const resolvedDataDoc = !Doc.AreProtosEqual(this._document[DataSym], this._document) ? this._document[DataSym] : undefined; - return <> - <DocumentView key={document[Id]} - LibraryPath={this._libraryPath} - Document={document} - DataDoc={resolvedDataDoc} - bringToFront={emptyFunction} - rootSelected={returnTrue} - addDocument={undefined} - removeDocument={undefined} - ContentScaling={this.contentScaling} - PanelWidth={this.panelWidth} - PanelHeight={this.panelHeight} - NativeHeight={this.nativeHeight} - NativeWidth={this.nativeWidth} - ScreenToLocalTransform={this.ScreenToLocalTransform} - renderDepth={0} - parentActive={returnTrue} - whenActiveChanged={emptyFunction} - focus={emptyFunction} - backgroundColor={CollectionDockingView.Instance.props.backgroundColor} - addDocTab={this.addDocTab} - pinToPres={DockedFrameRenderer.PinDoc} - docFilters={returnEmptyFilter} - ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} /> - {document._viewType === CollectionViewType.Freeform && !this._document?.hideMinimap ? this.renderMiniMap() : (null)} - </>; - } - - render() { - return (!this._isActive || !this.layoutDoc) ? (null) : - (<div className="collectionDockingView-content" ref={ref => this._mainCont = ref} - style={{ - transform: `translate(${this.previewPanelCenteringOffset}px, 0px)`, - height: this.layoutDoc && this.layoutDoc._fitWidth ? undefined : "100%", - width: this.widthpercent - }}> - {this.docView} - </div >); - } } -Scripting.addGlobal(function openOnRight(doc: any) { CollectionDockingView.AddRightSplit(doc); }, + +Scripting.addGlobal(function openOnRight(doc: any) { CollectionDockingView.AddSplit(doc, "right"); }, "opens up the inputted document on the right side of the screen", "(doc: any)"); -Scripting.addGlobal(function useRightSplit(doc: any, shiftKey?: boolean) { CollectionDockingView.UseRightSplit(doc, undefined, shiftKey); }); +Scripting.addGlobal(function useRightSplit(doc: any, shiftKey?: boolean) { CollectionDockingView.ReplaceTab(doc, "right", undefined, shiftKey); }); diff --git a/src/client/views/collections/ParentDocumentSelector.scss b/src/client/views/collections/CollectionDockingViewMenu.scss index bc9cf4848..4157f0f7e 100644 --- a/src/client/views/collections/ParentDocumentSelector.scss +++ b/src/client/views/collections/CollectionDockingViewMenu.scss @@ -1,15 +1,14 @@ -.parentDocumentSelector-linkFlyout { + +.dockingViewButtonSelector { div { overflow: visible !important; } - .metadataEntry-outerDiv { - overflow: hidden !important; - pointer-events: all; - } + display: inline-block; + width:100%; + height:100%; } - -.parentDocumentSelector-flyout { +.dockingViewButtonSelector-flyout { position: relative; z-index: 9999; background-color: #eeeeee; @@ -32,32 +31,4 @@ border-right: 0px; border-left: 0px; } -} - -.parentDocumentSelector-button { - pointer-events: all; - position: relative; - display: inline-block; - - svg { - // width:20px !important; - //height:20px; - } -} - -.parentDocumentSelector-metadata { - pointer-events: auto; - padding-right: 5px; - width: 25px; - display: inline-block; -} - -.buttonSelector { - div { - overflow: visible !important; - } - - display: inline-block; - width:100%; - height:100%; }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionDockingViewMenu.tsx b/src/client/views/collections/CollectionDockingViewMenu.tsx new file mode 100644 index 000000000..1cab293a8 --- /dev/null +++ b/src/client/views/collections/CollectionDockingViewMenu.tsx @@ -0,0 +1,48 @@ +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Tooltip } from "@material-ui/core"; +import { action, computed, observable } from "mobx"; +import { observer } from "mobx-react"; +import * as React from "react"; +import { DocumentButtonBar } from "../DocumentButtonBar"; +import { DocumentView } from "../nodes/DocumentView"; +const higflyout = require("@hig/flyout"); +export const { anchorPoints } = higflyout; +export const Flyout = higflyout.default; + +@observer +export class CollectionDockingViewMenu extends React.Component<{ views: () => DocumentView[], Stack: any }> { + customStylesheet(styles: any) { + return { + ...styles, + panel: { + ...styles.panel, + minWidth: "100px" + }, + }; + } + _ref = React.createRef<HTMLDivElement>(); + + @computed get flyout() { + return ( + <div className="dockingViewButtonSelector-flyout" title=" " ref={this._ref}> + <DocumentButtonBar views={this.props.views} stack={this.props.Stack} /> + </div> + ); + } + + @observable _tooltipOpen: boolean = false; + render() { + return <Tooltip open={this._tooltipOpen} onClose={action(() => this._tooltipOpen = false)} title={<><div className="dash-tooltip">Tap for toolbar, drag to create alias in another pane</div></>} placement="bottom"> + <span className="dockingViewButtonSelector" + onPointerEnter={action(() => !this._ref.current?.getBoundingClientRect().width && (this._tooltipOpen = true))} + onPointerDown={action(e => { + this.props.views()[0]?.select(false); + this._tooltipOpen = false; + })} > + <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={this.flyout} stylesheet={this.customStylesheet}> + <FontAwesomeIcon icon={"arrows-alt"} size={"sm"} /> + </Flyout> + </span> + </Tooltip >; + } +} diff --git a/src/client/views/collections/CollectionLinearView.scss b/src/client/views/collections/CollectionLinearView.scss index f5c4299a9..ca72b98a5 100644 --- a/src/client/views/collections/CollectionLinearView.scss +++ b/src/client/views/collections/CollectionLinearView.scss @@ -4,10 +4,12 @@ .collectionLinearView-outer { overflow: visible; height: 100%; + pointer-events: none; .collectionLinearView { display: flex; height: 100%; + align-items: center; >span { background: $dark-color; diff --git a/src/client/views/collections/CollectionLinearView.tsx b/src/client/views/collections/CollectionLinearView.tsx index e1b07077e..859ee9362 100644 --- a/src/client/views/collections/CollectionLinearView.tsx +++ b/src/client/views/collections/CollectionLinearView.tsx @@ -1,22 +1,21 @@ +import { Tooltip } from '@material-ui/core'; import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, HeightSym, WidthSym } from '../../../fields/Doc'; +import { documentSchema } from '../../../fields/documentSchemas'; +import { Id } from '../../../fields/FieldSymbols'; import { makeInterface } from '../../../fields/Schema'; -import { BoolCast, NumCast, StrCast, Cast, ScriptCast } from '../../../fields/Types'; -import { emptyFunction, returnEmptyString, returnOne, returnTrue, Utils, returnFalse, returnZero } from '../../../Utils'; +import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; +import { emptyFunction, returnOne, returnTrue, Utils } from '../../../Utils'; import { DragManager } from '../../util/DragManager'; import { Transform } from '../../util/Transform'; -import "./CollectionLinearView.scss"; -import { CollectionViewType } from './CollectionView'; -import { CollectionSubView } from './CollectionSubView'; -import { DocumentView } from '../nodes/DocumentView'; -import { documentSchema } from '../../../fields/documentSchemas'; -import { Id } from '../../../fields/FieldSymbols'; +import { ContentFittingDocumentView } from '../nodes/ContentFittingDocumentView'; import { DocumentLinksButton } from '../nodes/DocumentLinksButton'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { LinkDescriptionPopup } from '../nodes/LinkDescriptionPopup'; -import { Tooltip } from '@material-ui/core'; +import "./CollectionLinearView.scss"; +import { CollectionSubView } from './CollectionSubView'; +import { CollectionViewType } from './CollectionView'; type LinearDocument = makeInterface<[typeof documentSchema,]>; @@ -89,6 +88,7 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { } } DocumentLinksButton.StartLink = undefined; + DocumentLinksButton.StartLinkView = undefined; } @action @@ -112,12 +112,7 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { const backgroundColor = StrCast(this.props.Document.backgroundColor, "black"); const color = StrCast(this.props.Document.color, "white"); - const menuOpener = <label htmlFor={`${guid}`} style={{ - background: backgroundColor === color ? "black" : backgroundColor, - // width: "18px", height: "18px", fontSize: "12.5px", - // transition: this.props.Document.linearViewIsExpanded ? "transform 0.2s" : "transform 0.5s", - // transform: this.props.Document.linearViewIsExpanded ? "" : "rotate(45deg)" - }} + const menuOpener = <label htmlFor={`${guid}`} style={{ pointerEvents: "all", cursor: "pointer", background: backgroundColor === color ? "black" : backgroundColor, }} onPointerDown={e => e.stopPropagation()} > <p>{BoolCast(this.props.Document.linearViewIsExpanded) ? "–" : "+"}</p> </label>; @@ -128,21 +123,22 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { {menuOpener} </Tooltip> <input id={`${guid}`} type="checkbox" checked={BoolCast(this.props.Document.linearViewIsExpanded)} ref={this.addMenuToggle} - onChange={action((e: any) => this.props.Document.linearViewIsExpanded = this.addMenuToggle.current!.checked)} /> + onChange={action(() => this.props.Document.linearViewIsExpanded = this.addMenuToggle.current!.checked)} /> <div className="collectionLinearView-content" style={{ height: this.dimension(), flexDirection: flexDir }}> {this.childLayoutPairs.map((pair, ind) => { const nested = pair.layout._viewType === CollectionViewType.Linear; const dref = React.createRef<HTMLDivElement>(); const nativeWidth = NumCast(pair.layout._nativeWidth, this.dimension()); - const deltaSize = nativeWidth * .15 / 2; const scalable = pair.layout.onClick || pair.layout.onDragStart; return <div className={`collectionLinearView-docBtn` + (scalable ? "-scalable" : "")} key={pair.layout[Id]} ref={dref} style={{ - width: scalable ? (nested ? pair.layout[WidthSym]() : this.dimension() - deltaSize) : undefined, - height: nested && pair.layout.linearViewIsExpanded ? pair.layout[HeightSym]() : this.dimension() - deltaSize, + pointerEvents: "all", + minWidth: 30, + width: nested ? pair.layout[WidthSym]() : this.dimension(), + height: nested && pair.layout.linearViewIsExpanded ? pair.layout[HeightSym]() : this.dimension(), }} > - <DocumentView + <ContentFittingDocumentView Document={pair.layout} DataDoc={pair.data} LibraryPath={this.props.LibraryPath} @@ -155,10 +151,8 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { onClick={undefined} ScreenToLocalTransform={this.getTransform(dref)} ContentScaling={returnOne} - NativeHeight={returnZero} - NativeWidth={returnZero} - PanelWidth={nested ? pair.layout[WidthSym] : () => this.dimension()}// ugh - need to get rid of this inline function to avoid recomputing - PanelHeight={nested ? pair.layout[HeightSym] : () => this.dimension()} + PanelWidth={nested ? pair.layout[WidthSym] : this.dimension} + PanelHeight={nested ? pair.layout[HeightSym] : this.dimension} renderDepth={this.props.renderDepth + 1} focus={emptyFunction} backgroundColor={this.props.backgroundColor} @@ -166,13 +160,16 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { whenActiveChanged={emptyFunction} bringToFront={emptyFunction} docFilters={this.props.docFilters} + docRangeFilters={this.props.docRangeFilters} + searchFilterDocs={this.props.searchFilterDocs} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} /> </div>; })} </div> {DocumentLinksButton.StartLink ? <span className="bottomPopup-background" style={{ - background: backgroundColor === color ? "black" : backgroundColor + background: backgroundColor === color ? "black" : backgroundColor, + pointerEvents: "all" }} onPointerDown={e => e.stopPropagation()} > <span className="bottomPopup-text" > @@ -192,9 +189,6 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { </span> </Tooltip> - {/* <FontAwesomeIcon icon="times-circle" size="lg" style={{ color: "red" }} - onClick={this.exitLongLinks} /> */} - </span> : null} </div> </div>; diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx index c772dcfe7..7014966c7 100644 --- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx +++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx @@ -2,7 +2,7 @@ import React = require("react"); import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { Doc } from "../../../fields/Doc"; +import { Doc, DocListCast, DataSym } from "../../../fields/Doc"; import { PastelSchemaPalette, SchemaHeaderField } from "../../../fields/SchemaHeaderField"; import { ScriptField } from "../../../fields/ScriptField"; import { StrCast, NumCast } from "../../../fields/Types"; @@ -90,7 +90,8 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr this.props.parent.Document.dropConverter.script.run({ dragData: de.complete.docDragData }); const key = StrCast(this.props.parent.props.Document._pivotField); const castedValue = this.getValue(this.heading); - de.complete.docDragData.droppedDocuments.forEach(d => d[key] = castedValue); + const onLayoutDoc = this.onLayoutDoc(key); + de.complete.docDragData.droppedDocuments.forEach(d => Doc.SetInPlace(d, key, castedValue, !onLayoutDoc)); this.props.parent.onInternalDrop(e, de); e.stopPropagation(); } @@ -115,7 +116,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr return false; } } - this.props.docList.forEach(d => d[key] = castedValue); + this.props.docList.forEach(d => Doc.SetInPlace(d, key, castedValue, true)); this._heading = castedValue.toString(); return true; } @@ -141,7 +142,8 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr this._createAliasSelected = false; const key = StrCast(this.props.parent.props.Document._pivotField); const newDoc = Docs.Create.TextDocument(value, { _autoHeight: true, _width: 200, title: value }); - newDoc[key] = this.getValue(this.props.heading); + const onLayoutDoc = this.onLayoutDoc(key); + (onLayoutDoc ? newDoc : newDoc[DataSym])[key] = this.getValue(this.props.heading); const docs = this.props.parent.childDocList; return docs ? (docs.splice(0, 0, newDoc) ? true : false) : this.props.parent.props.addDocument(newDoc); } @@ -149,7 +151,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr deleteRow = undoBatch(action(() => { this._createAliasSelected = false; const key = StrCast(this.props.parent.props.Document._pivotField); - this.props.docList.forEach(d => d[key] = undefined); + this.props.docList.forEach(d => Doc.SetInPlace(d, key, undefined, true)); if (this.props.parent.columnHeaders && this.props.headingObject) { const index = this.props.parent.columnHeaders.indexOf(this.props.headingObject); this.props.parent.columnHeaders.splice(index, 1); @@ -180,11 +182,21 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr @action headerDown = (e: React.PointerEvent<HTMLDivElement>) => { if (e.button === 0 && !e.ctrlKey) { - setupMoveUpEvents(this, e, this.headerMove, emptyFunction, () => (this.props.parent.props.Document._chromeStatus === "disabled") && this.collapseSection(e)); + setupMoveUpEvents(this, e, this.headerMove, emptyFunction, e => (this.props.parent.props.Document._chromeStatus === "disabled") && this.collapseSection(e)); this._createAliasSelected = false; } } + /** + * Returns true if a key is on the layout doc of the documents in the collection. + */ + onLayoutDoc = (key: string): boolean => { + DocListCast(this.props.parent.Document.data).forEach(doc => { + if (Doc.Get(doc, key, true)) return true; + }); + return false; + } + renderColorPicker = () => { const selected = this.color; diff --git a/src/client/views/collections/CollectionMenu.scss b/src/client/views/collections/CollectionMenu.scss index 2f6d97f99..e36e5caa7 100644 --- a/src/client/views/collections/CollectionMenu.scss +++ b/src/client/views/collections/CollectionMenu.scss @@ -229,18 +229,18 @@ .flexLabel { margin-bottom: 0; } - } - .collectionGridViewChrome-entryBox { - width: 50%; - } + .collectionGridViewChrome-entryBox { + width: 50%; + color: black; + } - .collectionGridViewChrome-columnButton { - color: black; + .collectionGridViewChrome-columnButton { + color: black; + } } } - .collectionStackingViewChrome-sort, .collectionTreeViewChrome-sort { display: flex; @@ -315,6 +315,59 @@ } } +.webBox-urlEditor { + position: relative; + opacity: 0.9; + z-index: 9001; + transition: top .5s; + + .urlEditor { + display: grid; + grid-template-columns: 1fr auto; + padding-bottom: 10px; + overflow: hidden; + margin-top: 5px; + height: 35px; + + .editorBase { + display: flex; + + .editor-collapse { + transition: all .5s, opacity 0.3s; + position: absolute; + width: 40px; + transform-origin: top left; + } + + .switchToText { + color: $main-accent; + } + + .switchToText:hover { + color: $dark-color; + } + } + + button:hover { + transform: scale(1); + } + } +} + +.webpage-urlInput { + padding: 12px 10px 11px 10px; + border: 0px; + color: black; + font-size: 10px; + letter-spacing: 2px; + outline-color: black; + background: rgb(238, 238, 238); + width: 100%; + min-width: 350px; + margin-right: 10px; + height: 100%; +} + .collectionFreeFormMenu-cont { display: inline-flex; position: relative; @@ -324,6 +377,7 @@ .antimodeMenu-button { text-align: center; display: block; + position: relative; } .color-previewI { @@ -331,12 +385,15 @@ height: 20%; bottom: 0; position: absolute; + margin-left: 2px; } .color-previewII { width: 80%; height: 80%; margin-left: 10%; + position: absolute; + bottom: 5; } .btn-group { diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx index 25a2fee04..51d1d5559 100644 --- a/src/client/views/collections/CollectionMenu.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -5,35 +5,39 @@ import { Tooltip } from "@material-ui/core"; import { action, computed, Lambda, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import { ColorState } from "react-color"; -import { Doc, DocListCast, Opt } from "../../../fields/Doc"; +import { Doc, DocListCast, Field, Opt } from "../../../fields/Doc"; import { Document } from "../../../fields/documentSchemas"; import { Id } from "../../../fields/FieldSymbols"; import { InkTool } from "../../../fields/InkField"; import { List } from "../../../fields/List"; import { ObjectField } from "../../../fields/ObjectField"; -import { RichTextField } from "../../../fields/RichTextField"; import { listSpec } from "../../../fields/Schema"; import { ScriptField } from "../../../fields/ScriptField"; import { BoolCast, Cast, NumCast, StrCast } from "../../../fields/Types"; +import { WebField } from "../../../fields/URLField"; import { emptyFunction, setupMoveUpEvents, Utils } from "../../../Utils"; import { DocumentType } from "../../documents/DocumentTypes"; import { CurrentUserUtils } from "../../util/CurrentUserUtils"; import { DragManager } from "../../util/DragManager"; +import { Scripting } from "../../util/Scripting"; import { SelectionManager } from "../../util/SelectionManager"; import { undoBatch } from "../../util/UndoManager"; -import AntimodeMenu, { AntimodeMenuProps } from "../AntimodeMenu"; +import { AntimodeMenu, AntimodeMenuProps } from "../AntimodeMenu"; import { EditableView } from "../EditableView"; -import GestureOverlay from "../GestureOverlay"; +import { GestureOverlay } from "../GestureOverlay"; import { ActiveFillColor, ActiveInkColor, SetActiveArrowEnd, SetActiveArrowStart, SetActiveBezierApprox, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth } from "../InkingStroke"; import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; import { DocumentView } from "../nodes/DocumentView"; -import RichTextMenu from "../nodes/formattedText/RichTextMenu"; +import { RichTextMenu } from "../nodes/formattedText/RichTextMenu"; +import { PresBox } from "../nodes/PresBox"; import "./CollectionMenu.scss"; import { CollectionViewType, COLLECTION_BORDER_WIDTH } from "./CollectionView"; +import { TabDocView } from "./TabDocView"; +import { RichTextField } from "../../../fields/RichTextField"; @observer -export default class CollectionMenu extends AntimodeMenu<AntimodeMenuProps> { - static Instance: CollectionMenu; +export class CollectionMenu extends AntimodeMenu<AntimodeMenuProps> { + @observable static Instance: CollectionMenu; @observable SelectedCollection: DocumentView | undefined; @observable FieldKey: string; @@ -41,9 +45,9 @@ export default class CollectionMenu extends AntimodeMenu<AntimodeMenuProps> { constructor(props: any) { super(props); this.FieldKey = ""; - CollectionMenu.Instance = this; + runInAction(() => CollectionMenu.Instance = this); this._canFade = false; // don't let the inking menu fade away - this.Pinned = Cast(Doc.UserDoc()["menuCollections-pinned"], "boolean", true); + runInAction(() => this.Pinned = Cast(Doc.UserDoc()["menuCollections-pinned"], "boolean", true)); this.jumpTo(300, 300); } @@ -91,13 +95,13 @@ export default class CollectionMenu extends AntimodeMenu<AntimodeMenuProps> { </button> </Tooltip>; - return this.getElement(!this.SelectedCollection ? [button] : + return this.getElement(!this.SelectedCollection ? [/*button*/] : [<CollectionViewBaseChrome key="chrome" docView={this.SelectedCollection} - fieldKey={Doc.LayoutFieldKey(this.SelectedCollection?.props.Document)} + fieldKey={this.SelectedCollection.LayoutFieldKey} type={StrCast(this.SelectedCollection?.props.Document._viewType, CollectionViewType.Invalid) as CollectionViewType} />, prop, - button]); + /*button*/]); } } @@ -118,7 +122,18 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp _templateCommand = { params: ["target", "source"], title: "item view", script: "self.target.childLayoutTemplate = getDocTemplate(self.source?.[0])", - immediate: undoBatch((source: Doc[]) => source.length && (this.target.childLayoutTemplate = Doc.getDocTemplate(source?.[0]))), + immediate: undoBatch((source: Doc[]) => { + let formatStr = source.length && Cast(source[0].text, RichTextField, null)?.Text; + try { formatStr && JSON.parse(formatStr); } catch (e) { formatStr = ""; } + if (source.length === 1 && formatStr) { + Doc.SetInPlace(this.target, "childLayoutString", formatStr, false); + } else if (source.length) { + this.target.childLayoutTemplate = Doc.getDocTemplate(source?.[0]); + } else { + Doc.SetInPlace(this.target, "childLayoutString", undefined, true); + Doc.SetInPlace(this.target, "childLayoutTemplate", undefined, true); + } + }), initialize: emptyFunction, }; _narrativeCommand = { @@ -130,7 +145,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp _contentCommand = { params: ["target", "source"], title: "set content", script: "getProto(self.target).data = copyField(self.source);", - immediate: undoBatch((source: Doc[]) => Doc.GetProto(this.target).data = new List<Doc>(source)), // Doc.aliasDocs(source), + immediate: undoBatch((source: Doc[]) => Doc.GetProto(this.target).data = new List<Doc>(source)), initialize: emptyFunction, }; _onClickCommand = { @@ -161,9 +176,9 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp }; _viewCommand = { params: ["target"], title: "bookmark view", - script: "self.target._panX = self['target-panX']; self.target._panY = self['target-panY']; self.target._viewScale = self['target-viewScale'];", - immediate: undoBatch((source: Doc[]) => { this.target._panX = 0; this.target._panY = 0; this.target._viewScale = 1; }), - initialize: (button: Doc) => { button['target-panX'] = this.target._panX; button['target-panY'] = this.target._panY; button['target-viewScale'] = this.target._viewScale; }, + script: "self.target._panX = self['target-panX']; self.target._panY = self['target-panY']; self.target._viewScale = self['target-viewScale']; gotoFrame(self.target, self['target-currentFrame']);", + immediate: undoBatch((source: Doc[]) => { this.target._panX = 0; this.target._panY = 0; this.target._viewScale = 1; this.target._currentFrame = 0; }), + initialize: (button: Doc) => { button['target-panX'] = this.target._panX; button['target-panY'] = this.target._panY; button['target-viewScale'] = this.target._viewScale; button['target-currentFrame'] = this.target._currentFrame; }, }; _clusterCommand = { params: ["target"], title: "fit content", @@ -173,18 +188,23 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp }; _fitContentCommand = { params: ["target"], title: "toggle clusters", - script: "self.target.useClusters = !self.target.useClusters;", - immediate: undoBatch((source: Doc[]) => this.target.useClusters = !this.target.useClusters), + script: "self.target._useClusters = !self.target._useClusters;", + immediate: undoBatch((source: Doc[]) => this.target._useClusters = !this.target._useClusters), initialize: emptyFunction }; _saveFilterCommand = { params: ["target"], title: "save filter", - script: "self.target._docFilters = copyField(self['target-docFilters']);", - immediate: undoBatch((source: Doc[]) => this.target._docFilters = undefined), - initialize: (button: Doc) => { button['target-docFilters'] = this.target._docFilters instanceof ObjectField ? ObjectField.MakeCopy(this.target._docFilters as any as ObjectField) : ""; }, + script: `self.target._docFilters = compareLists(self['target-docFilters'],self.target._docFilters) ? undefined : copyField(self['target-docFilters']); + self.target._searchFilterDocs = compareLists(self['target-searchFilterDocs'],self.target._searchFilterDocs) ? undefined: copyField(self['target-searchFilterDocs']);`, + immediate: undoBatch((source: Doc[]) => { this.target._docFilters = undefined; this.target._searchFilterDocs = undefined; }), + initialize: (button: Doc) => { + button['target-docFilters'] = (Cast(Doc.UserDoc().mySearchPanelDoc, Doc, null)._docFilters || Cast(Doc.UserDoc().activeDashboard, Doc, null)._docFilters) instanceof ObjectField ? + ObjectField.MakeCopy((Cast(Doc.UserDoc().mySearchPanelDoc, Doc, null)._docFilters || Cast(Doc.UserDoc().activeDashboard, Doc, null)._docFilters) as any as ObjectField) : undefined; + button['target-searchFilterDocs'] = CurrentUserUtils.ActiveDashboard._searchFilterDocs instanceof ObjectField ? ObjectField.MakeCopy(CurrentUserUtils.ActiveDashboard._searchFilterDocs as any as ObjectField) : undefined; + }, }; - @computed get _freeform_commands() { return Doc.UserDoc().noviceMode ? [this._viewCommand] : [this._viewCommand, this._saveFilterCommand, this._contentCommand, this._templateCommand, this._narrativeCommand]; } + @computed get _freeform_commands() { return Doc.UserDoc().noviceMode ? [this._viewCommand, this._saveFilterCommand] : [this._viewCommand, this._saveFilterCommand, this._contentCommand, this._templateCommand, this._narrativeCommand]; } @computed get _stacking_commands() { return Doc.UserDoc().noviceMode ? undefined : [this._contentCommand, this._templateCommand]; } @computed get _masonry_commands() { return Doc.UserDoc().noviceMode ? undefined : [this._contentCommand, this._templateCommand]; } @computed get _schema_commands() { return Doc.UserDoc().noviceMode ? undefined : [this._templateCommand, this._narrativeCommand]; } @@ -223,18 +243,13 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp runInAction(() => this._currentKey = e.target.selectedOptions[0].value); } - @action - toggleViewSpecs = (e: React.SyntheticEvent) => { - this.document._facetWidth = this.document._facetWidth ? 0 : 200; - e.stopPropagation(); - } @action closeViewSpecs = () => { this.document._facetWidth = 0; } @computed get subChrome() { - switch (this.props.type) { + switch (this.props.docView.props.LayoutTemplateString ? CollectionViewType.Freeform : this.props.type) { // bcz: ARgh! hack to get menu for tree view outline items default: return this.otherSubChrome; case CollectionViewType.Invalid: case CollectionViewType.Freeform: return (<CollectionFreeFormViewChrome key="collchrome" {...this.props} isOverlay={this.props.type === CollectionViewType.Invalid} />); @@ -326,6 +341,8 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp } @computed get viewModes() { + const excludedViewTypes = Doc.UserDoc().noviceMode ? [CollectionViewType.Invalid, CollectionViewType.Docking, CollectionViewType.Pile, CollectionViewType.Map, CollectionViewType.Linear, CollectionViewType.Time] : + [CollectionViewType.Invalid, CollectionViewType.Docking, CollectionViewType.Pile, CollectionViewType.Linear]; return <div className="collectionViewBaseChrome-viewModes" > <Tooltip title={<div className="dash-tooltip">drop document to apply or drag to create button</div>} placement="bottom"> <div className="commandEntry-outerDiv" ref={this._viewRef} onPointerDown={this.dragViewDown}> @@ -337,7 +354,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp onPointerDown={stopPropagation} onChange={this.viewChanged} value={StrCast(this.props.type)}> - {Object.values(CollectionViewType).map(type => [CollectionViewType.Invalid, CollectionViewType.Docking].includes(type) ? (null) : ( + {Object.values(CollectionViewType).filter(type => !excludedViewTypes.includes(type)).map(type => ( <option key={Utils.GenerateGuid()} className="collectionViewBaseChrome-viewOption" @@ -353,9 +370,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp } @computed get selectedDocumentView() { - if (SelectionManager.SelectedDocuments().length) { - return SelectionManager.SelectedDocuments()[0]; - } else { return undefined; } + return SelectionManager.SelectedDocuments().length ? SelectionManager.SelectedDocuments()[0] : undefined; } @computed get selectedDoc() { return this.selectedDocumentView?.rootDoc; } @computed get notACollection() { @@ -366,6 +381,98 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp } else return false; } + @computed + get pinButton() { + const targetDoc = this.selectedDoc; + const isPinned = targetDoc && Doc.isDocPinned(targetDoc); + return !targetDoc ? (null) : <Tooltip key="pin" title={<div className="dash-tooltip">{Doc.isDocPinned(targetDoc) ? "Unpin from presentation" : "Pin to presentation"}</div>} placement="top"> + <button className="antimodeMenu-button" style={{ backgroundColor: isPinned ? "121212" : undefined, borderLeft: "1px solid gray" }} + onClick={e => TabDocView.PinDoc(targetDoc, isPinned)}> + <FontAwesomeIcon className="documentdecorations-icon" size="lg" icon="map-pin" /> + </button> + </Tooltip>; + } + + @computed + get pinWithViewButton() { + const presPinWithViewIcon = <img src={`/assets/pinWithView.png`} style={{ margin: "auto", width: 19 }} />; + const targetDoc = this.selectedDoc; + return (!targetDoc || (targetDoc._viewType !== CollectionViewType.Freeform && targetDoc.type !== DocumentType.IMG)) ? (null) : <Tooltip title={<><div className="dash-tooltip">{"Pin to presentation trail with current view"}</div></>} placement="top"> + <button className="antimodeMenu-button" style={{ borderRight: "1px solid gray", borderLeft: "1px solid gray", justifyContent: 'center' }} + onClick={e => { + if (targetDoc) { + TabDocView.PinDoc(targetDoc, false); + const activeDoc = PresBox.Instance.childDocs[PresBox.Instance.childDocs.length - 1]; + const x = targetDoc._panX; + const y = targetDoc._panY; + const scale = targetDoc._viewScale; + activeDoc.presPinView = true; + activeDoc.presPinViewX = x; + activeDoc.presPinViewY = y; + activeDoc.presPinViewScale = scale; + } + }}> + {presPinWithViewIcon} + </button> + </Tooltip>; + } + + + @undoBatch + onAlias = () => { + if (this.selectedDoc && this.selectedDocumentView) { + // const copy = Doc.MakeCopy(this.selectedDocumentView.props.Document, true); + // copy.x = NumCast(this.selectedDoc.x) + NumCast(this.selectedDoc._width); + // copy.y = NumCast(this.selectedDoc.y) + 30; + // this.selectedDocumentView.props.addDocument?.(copy); + const alias = Doc.MakeAlias(this.selectedDoc); + alias.x = NumCast(this.selectedDoc.x) + NumCast(this.selectedDoc._width); + alias.y = NumCast(this.selectedDoc.y) + 30; + this.selectedDocumentView.props.addDocument?.(alias); + } + } + onAliasButtonDown = (e: React.PointerEvent): void => { + setupMoveUpEvents(this, e, this.onAliasButtonMoved, emptyFunction, emptyFunction); + } + + @undoBatch + onAliasButtonMoved = (e: PointerEvent) => { + const contentDiv = this.selectedDocumentView?.ContentDiv; + if (contentDiv) { + const dragData = new DragManager.DocumentDragData([this.selectedDocumentView!.props.Document]); + const offset = [e.clientX - contentDiv.getBoundingClientRect().x, e.clientY - contentDiv.getBoundingClientRect().y]; + dragData.defaultDropAction = "alias"; + dragData.canEmbed = true; + DragManager.StartDocumentDrag([contentDiv], dragData, e.clientX, e.clientY, { + offsetX: offset[0], + offsetY: offset[1], + hideSource: false + }); + return true; + } + return false; + } + + @computed + get aliasButton() { + const targetDoc = this.selectedDoc; + return !targetDoc ? (null) : <Tooltip title={<div className="dash-tooltip">{"Tap or Drag to create an alias"}</div>} placement="top"> + <button className="antimodeMenu-button" onPointerDown={this.onAliasButtonDown} onClick={this.onAlias} style={{ cursor: "drag" }}> + <FontAwesomeIcon className="documentdecorations-icon" icon="copy" size="lg" /> + </button> + </Tooltip>; + } + + @computed get lightboxButton() { + const targetDoc = this.selectedDoc; + return !targetDoc ? (null) : <Tooltip title={<div className="dash-tooltip">{"Show Lightbox of Images"}</div>} placement="top"> + <button className="antimodeMenu-button" onPointerDown={action(() => targetDoc._isLightboxOpen = true)} onClick={this.onAlias}> + <FontAwesomeIcon className="documentdecorations-icon" icon="desktop" size="lg" /> + </button> + </Tooltip>; + } + + render() { return ( @@ -374,15 +481,6 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp <div className="collectionViewBaseChrome"> {this.notACollection || this.props.type === CollectionViewType.Invalid ? (null) : this.viewModes} {!this._buttonizableCommands ? (null) : this.templateChrome} - {Doc.UserDoc().noviceMode ? (null) : - <Tooltip title={<div className="dash-tooltip">filter documents to show</div>} placement="bottom"> - <div className="collectionViewBaseChrome-viewSpecs" style={{ display: "grid" }}> - <button className={"antimodeMenu-button"} onClick={this.toggleViewSpecs} > - <FontAwesomeIcon icon="filter" size="lg" /> - </button> - </div> - </Tooltip>} - {this.props.docView.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Freeform ? (null) : <Tooltip title={<div className="dash-tooltip">Toggle Overlay Layer</div>} placement="bottom"> <button className={"antimodeMenu-button"} key="float" @@ -391,6 +489,10 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp <FontAwesomeIcon icon={["fab", "buffer"]} size={"lg"} /> </button> </Tooltip>} + {this.notACollection ? (null) : this.lightboxButton} + {this.aliasButton} + {/* {this.pinButton} */} + {this.pinWithViewButton} </div> {this.subChrome} </div> @@ -415,23 +517,21 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu } get document() { return this.props.docView.props.Document; } @computed get dataField() { - return this.document[Doc.LayoutFieldKey(this.document)]; + return this.document[this.props.docView.LayoutFieldKey]; } @computed get childDocs() { return DocListCast(this.dataField); } @computed get selectedDocumentView() { - if (SelectionManager.SelectedDocuments().length) { - return SelectionManager.SelectedDocuments()[0]; - } else { return undefined; } + return SelectionManager.SelectedDocuments().length ? SelectionManager.SelectedDocuments()[0] : undefined; } @computed get selectedDoc() { return this.selectedDocumentView?.rootDoc; } @computed get isText() { if (this.selectedDoc) { const layoutField = Doc.LayoutField(this.selectedDoc); - return StrCast(layoutField).includes("FormattedText") || - (layoutField instanceof Doc && StrCast(layoutField.layout).includes("FormattedText")); + const layoutStr = this.selectedDocumentView?.props.LayoutTemplateString || StrCast(layoutField); + return (document.activeElement as any)?.className.includes("ProseMirror") || layoutStr.includes("FormattedText") || StrCast((layoutField as Doc)?.layout).includes("FormattedText"); } else return false; } @@ -439,30 +539,25 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu @undoBatch @action nextKeyframe = (): void => { - const currentFrame = Cast(this.document.currentFrame, "number", null); + const currentFrame = Cast(this.document._currentFrame, "number", null); if (currentFrame === undefined) { - this.document.currentFrame = 0; + this.document._currentFrame = 0; CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0); } CollectionFreeFormDocumentView.updateKeyframe(this.childDocs, currentFrame || 0); - this.document.currentFrame = Math.max(0, (currentFrame || 0) + 1); - this.document.lastFrame = Math.max(NumCast(this.document.currentFrame), NumCast(this.document.lastFrame)); + this.document._currentFrame = Math.max(0, (currentFrame || 0) + 1); + this.document.lastFrame = Math.max(NumCast(this.document._currentFrame), NumCast(this.document.lastFrame)); } @undoBatch @action prevKeyframe = (): void => { - const currentFrame = Cast(this.document.currentFrame, "number", null); + const currentFrame = Cast(this.document._currentFrame, "number", null); if (currentFrame === undefined) { - this.document.currentFrame = 0; + this.document._currentFrame = 0; CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0); } CollectionFreeFormDocumentView.gotoKeyframe(this.childDocs.slice()); - this.document.currentFrame = Math.max(0, (currentFrame || 0) - 1); - } - @undoBatch - @action - miniMap = (): void => { - this.document.hideMinimap = !this.document.hideMinimap; + this.document._currentFrame = Math.max(0, (currentFrame || 0) - 1); } private _palette = ["#D0021B", "#F5A623", "#F8E71C", "#8B572A", "#7ED321", "#417505", "#9013FE", "#4A90E2", "#50E3C2", "#B8E986", "#000000", "#4A4A4A", "#9B9B9B", "#FFFFFF", ""]; @@ -584,10 +679,10 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu style={{ backgroundColor: this._colorBtn ? "121212" : "", zIndex: 1001 }}> {/* <FontAwesomeIcon icon="pen-nib" size="lg" /> */} <div className="color-previewII" style={{ backgroundColor: color }}> - {color === "" ? <p style={{ fontSize: 45, color: "red", marginTop: -16, marginLeft: -5, position: "fixed" }}>☒</p> : ""} + {color === "" ? <p style={{ fontSize: 40, color: "red", marginTop: -10, marginLeft: -5, position: "fixed" }}>☒</p> : ""} </div> - </button>)} - </div>; + </button >)} + </div >; } @computed get fillPicker() { const fillPicker = this.toggleButton("shape fill color", this._fillBtn, () => this._fillBtn = !this._fillBtn, "fill-drip", @@ -600,53 +695,172 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu onPointerDown={action(() => { this.changeColor(color, "fill"); this._fillBtn = false; this.editProperties(color, "fill"); })} style={{ backgroundColor: this._fillBtn ? "121212" : "", zIndex: 1001 }}> <div className="color-previewII" style={{ backgroundColor: color }}> - {color === "" ? <p style={{ fontSize: 45, color: "red", marginTop: -16, marginLeft: -5, position: "fixed" }}>☒</p> : ""} + {color === "" ? <p style={{ fontSize: 40, color: "red", marginTop: -10, marginLeft: -5, position: "fixed" }}>☒</p> : ""} </div> </button>)} </div>; } + onUrlDrop = (e: React.DragEvent) => { + const { dataTransfer } = e; + const html = dataTransfer.getData("text/html"); + const uri = dataTransfer.getData("text/uri-list"); + const url = uri || html || this._url; + const newurl = url.startsWith(window.location.origin) ? + url.replace(window.location.origin, this._url.match(/http[s]?:\/\/[^\/]*/)?.[0] || "") : url; + this.submitURL(newurl); + e.stopPropagation(); + } + onUrlDragover = (e: React.DragEvent) => { + e.preventDefault(); + } + + @computed get _url() { + return this.selectedDoc?.data instanceof WebField ? Cast(this.selectedDoc.data, WebField, null)?.url.toString() : Field.toString(this.selectedDoc?.data as Field); + } + + set _url(value) { + if (this.selectedDoc) { + Doc.GetProto(this.selectedDoc).data = new WebField(value); + Doc.SetInPlace(this.selectedDoc, "title", value, true); + const annots = Doc.GetProto(this.selectedDoc)["data-annotations-" + this.urlHash(value)]; + Doc.GetProto(this.selectedDoc)["data-annotations"] = annots instanceof ObjectField ? ObjectField.MakeCopy(annots) : new List<Doc>([]); + } + } + + @action + submitURL = (url: string) => { + if (!url.startsWith("http")) url = "http://" + url; + try { + const selectedDoc = this.selectedDoc; + if (selectedDoc) { + const URLy = new URL(url); + const future = Cast(selectedDoc["data-future"], listSpec("string"), null); + const history = Cast(selectedDoc["data-history"], listSpec("string"), null); + const annos = DocListCast(selectedDoc["data-annotations"]); + if (Field.toString(selectedDoc.data as Field) === Field.toString(new WebField(URLy))) { + Doc.GetProto(selectedDoc).data = new WebField(new URL("http://cs.brown.edu/~avd")); + setTimeout(action(() => Doc.GetProto(selectedDoc).data = new WebField(URLy)), 100); + } else { + if (url) { + Doc.GetProto(selectedDoc)["data-annotations-" + this.urlHash(this._url)] = new List<Doc>(annos); + if (history === undefined) { + selectedDoc["data-history"] = new List<string>([this._url]); + } else { + history.push(this._url); + } + future && (future.length = 0); + } + this._url = url; + } + } + } catch (e) { + console.log("WebBox URL error:" + url); + } + } + + urlHash(s: string) { + return s.split('').reduce((a: any, b: any) => { a = ((a << 5) - a) + b.charCodeAt(0); return a & a; }, 0); + } + + onValueKeyDown = async (e: React.KeyboardEvent) => { + e.key === "Enter" && this.submitURL(this._keyInput.current!.value); + e.stopPropagation(); + } + + @action + forward = () => { + const selectedDoc = this.selectedDoc; + if (selectedDoc) { + const future = Cast(selectedDoc["data-future"], listSpec("string"), null); + const history = Cast(selectedDoc["data-history"], listSpec("string"), null); + if (future?.length) { + history?.push(this._url); + Doc.GetProto(selectedDoc)["data-annotations-" + this.urlHash(this._url)] = new List<Doc>(DocListCast(selectedDoc["data-annotations"])); + const newurl = future.pop()!; + Doc.GetProto(selectedDoc).data = new WebField(new URL(this._url = newurl)); + Doc.GetProto(selectedDoc)["data-annotations"] = new List<Doc>(DocListCast(selectedDoc["data-annotations-" + this.urlHash(newurl)])); + } + } + } + + @action + back = () => { + const selectedDoc = this.selectedDoc; + if (selectedDoc) { + const future = Cast(selectedDoc["data-future"], listSpec("string"), null); + const history = Cast(selectedDoc["data-history"], listSpec("string"), null); + if (history?.length) { + if (future === undefined) selectedDoc["data-future"] = new List<string>([this._url]); + else future.push(this._url); + Doc.GetProto(selectedDoc)["data-annotations-" + this.urlHash(this._url)] = new List<Doc>(DocListCast(selectedDoc["data-annotations"])); + const newurl = history.pop()!; + Doc.GetProto(selectedDoc).data = new WebField(new URL(this._url = newurl)); + Doc.GetProto(selectedDoc)["data-annotations"] = new List<Doc>(DocListCast(selectedDoc["data-annotations-" + this.urlHash(newurl)])); + } + } + } + + private _keyInput = React.createRef<HTMLInputElement>(); + + @computed get urlEditor() { + return ( + <div className="webBox-buttons" + onDrop={this.onUrlDrop} + onDragOver={this.onUrlDragover} style={{ display: "flex" }}> + <input className="webpage-urlInput" key={this._url} + placeholder="ENTER URL" + defaultValue={this._url} + onDrop={this.onUrlDrop} + onDragOver={this.onUrlDragover} + onKeyDown={this.onValueKeyDown} + onClick={(e) => { + this._keyInput.current!.select(); + e.stopPropagation(); + }} + ref={this._keyInput} + /> + <div style={{ display: "flex", flexDirection: "row", justifyContent: "space-between", maxWidth: "250px", }}> + <button className="submitUrl" onClick={() => this.submitURL(this._keyInput.current!.value)} onDragOver={this.onUrlDragover} onDrop={this.onUrlDrop}> + GO + </button> + <button className="submitUrl" onClick={this.back}> + <FontAwesomeIcon icon="caret-left" size="lg"></FontAwesomeIcon> + </button> + <button className="submitUrl" onClick={this.forward}> + <FontAwesomeIcon icon="caret-right" size="lg"></FontAwesomeIcon> + </button> + </div> + </div> + ); + } + + @observable viewType = this.selectedDoc?._viewType; render() { return !this.props.docView.layoutDoc ? (null) : <div className="collectionFreeFormMenu-cont"> - {this.props.docView.props.renderDepth !== 0 || this.isText || this.props.isDoc ? (null) : - <Tooltip key="map" title={<div className="dash-tooltip">Toggle Mini Map</div>} placement="bottom"> - <div className="backKeyframe" onClick={this.miniMap} style={{ marginRight: "5px" }}> - <FontAwesomeIcon icon={"map"} size={"lg"} /> - </div> - </Tooltip> - } - {!this.isText && !this.props.isDoc ? <Tooltip key="back" title={<div className="dash-tooltip">Back Frame</div>} placement="bottom"> + {!Doc.UserDoc().noviceMode && !this.isText && !this.props.isDoc ? <Tooltip key="back" title={<div className="dash-tooltip">Back Frame</div>} placement="bottom"> <div className="backKeyframe" onClick={this.prevKeyframe}> <FontAwesomeIcon icon={"caret-left"} size={"lg"} /> </div> </Tooltip> : null} - {!this.isText && !this.props.isDoc ? <Tooltip key="num" title={<div className="dash-tooltip">Toggle View All</div>} placement="bottom"> + {!Doc.UserDoc().noviceMode && !this.isText && !this.props.isDoc ? <Tooltip key="num" title={<div className="dash-tooltip">Toggle View All</div>} placement="bottom"> <div className="numKeyframe" style={{ color: this.document.editing ? "white" : "black", backgroundColor: this.document.editing ? "#5B9FDD" : "#AEDDF8" }} onClick={action(() => this.document.editing = !this.document.editing)} > - {NumCast(this.document.currentFrame)} + {NumCast(this.document._currentFrame)} </div> </Tooltip> : null} - {!this.isText && !this.props.isDoc ? <Tooltip key="fwd" title={<div className="dash-tooltip">Forward Frame</div>} placement="bottom"> + {!Doc.UserDoc().noviceMode && !this.isText && !this.props.isDoc ? <Tooltip key="fwd" title={<div className="dash-tooltip">Forward Frame</div>} placement="bottom"> <div className="fwdKeyframe" onClick={this.nextKeyframe}> <FontAwesomeIcon icon={"caret-right"} size={"lg"} /> </div> </Tooltip> : null} {!this.props.isOverlay || this.document.type !== DocumentType.WEB || this.isText || this.props.isDoc ? (null) : - <Tooltip key="hypothesis" title={<div className="dash-tooltip">Use Hypothesis</div>} placement="bottom"> - <button className={"antimodeMenu-button"} key="hypothesis" - style={{ - backgroundColor: !this.props.docView.layoutDoc.isAnnotating ? "121212" : undefined, - borderRight: "1px solid gray" - }} - onClick={() => this.props.docView.layoutDoc.isAnnotating = !this.props.docView.layoutDoc.isAnnotating}> - <FontAwesomeIcon icon={["fab", "hire-a-helper"]} size={"lg"} /> - </button> - </Tooltip> + this.urlEditor } {!this.isText ? <> @@ -679,15 +893,14 @@ export class CollectionStackingViewChrome extends React.Component<CollectionMenu if (docs instanceof Doc) { const keys = Object.keys(docs).filter(key => key.indexOf("title") >= 0 || key.indexOf("author") >= 0 || key.indexOf("creationDate") >= 0 || key.indexOf("lastModified") >= 0 || - (key[0].toUpperCase() === key[0] && key.substring(0, 3) !== "ACL" && key !== "UseCors" && key[0] !== "_")); + (key[0].toUpperCase() === key[0] && key.substring(0, 3) !== "acl" && key[0] !== "_")); return keys.filter(key => key.toLowerCase().indexOf(val) > -1); } else { const keys = new Set<string>(); docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key))); - const noviceKeys = Array.from(keys).filter(key => key.indexOf("title") >= 0 || - key.indexOf("author") >= 0 || key.indexOf("creationDate") >= 0 || - key.indexOf("lastModified") >= 0 || (key[0]?.toUpperCase() === key[0] && - key.substring(0, 3) !== "ACL" && key !== "UseCors" && key[0] !== "_")); + const noviceKeys = Array.from(keys).filter(key => key.indexOf("title") >= 0 || key.indexOf("author") >= 0 || + key.indexOf("creationDate") >= 0 || key.indexOf("lastModified") >= 0 || + (key[0]?.toUpperCase() === key[0] && key.substring(0, 3) !== "acl" && key[0] !== "_")); return noviceKeys.filter(key => key.toLowerCase().indexOf(val) > -1); } } @@ -935,16 +1148,10 @@ export class CollectionGridViewChrome extends React.Component<CollectionMenuProp get numCols() { return NumCast(this.document.gridNumCols, 10); } /** - * Sets the value of `numCols` on the grid's Document to the value entered. - */ - @undoBatch - onNumColsEnter = (e: React.KeyboardEvent<HTMLInputElement>) => { - if (e.key === "Enter" || e.key === "Tab") { - if (e.currentTarget.valueAsNumber > 0) { - this.document.gridNumCols = e.currentTarget.valueAsNumber; - } - - } + * Sets the value of `numCols` on the grid's Document to the value entered. + */ + onNumColsChange = (e: React.ChangeEvent<HTMLInputElement>) => { + if (e.currentTarget.valueAsNumber > 0) undoBatch(() => this.document.gridNumCols = e.currentTarget.valueAsNumber)(); } /** @@ -985,6 +1192,7 @@ export class CollectionGridViewChrome extends React.Component<CollectionMenuProp if (this.numCols > 1 && !this.decrementLimitReached) { this.entered && (this.document.gridNumCols as number)++; undoBatch(() => this.document.gridNumCols = this.numCols - 1)(); + if (this.numCols === 1) this.decrementLimitReached = true; } this.entered = false; } @@ -1040,7 +1248,7 @@ export class CollectionGridViewChrome extends React.Component<CollectionMenuProp <span className="grid-icon"> <FontAwesomeIcon icon="columns" size="1x" /> </span> - <input className="collectionGridViewChrome-entryBox" type="number" placeholder={this.numCols.toString()} onKeyDown={this.onNumColsEnter} onClick={(e: React.MouseEvent<HTMLInputElement, MouseEvent>) => { e.stopPropagation(); e.preventDefault(); e.currentTarget.focus(); }} /> + <input className="collectionGridViewChrome-entryBox" type="number" value={this.numCols} onChange={this.onNumColsChange} onClick={(e: React.MouseEvent<HTMLInputElement, MouseEvent>) => { e.stopPropagation(); e.preventDefault(); e.currentTarget.focus(); }} /> <input className="collectionGridViewChrome-columnButton" onClick={this.onIncrementButtonClick} onMouseEnter={this.incrementValue} onMouseLeave={this.decrementValue} type="button" value="↑" /> <input className="collectionGridViewChrome-columnButton" style={{ marginRight: 5 }} onClick={this.onDecrementButtonClick} onMouseEnter={this.decrementValue} onMouseLeave={this.incrementValue} type="button" value="↓" /> </span> @@ -1083,4 +1291,15 @@ export class CollectionGridViewChrome extends React.Component<CollectionMenuProp </div> ); } -}
\ No newline at end of file +} +Scripting.addGlobal(function gotoFrame(doc: any, newFrame: any) { + const dataField = doc[Doc.LayoutFieldKey(doc)]; + const childDocs = DocListCast(dataField); + const currentFrame = Cast(doc._currentFrame, "number", null); + if (currentFrame === undefined) { + doc._currentFrame = 0; + CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0); + } + CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0); + doc._currentFrame = Math.max(0, newFrame); +}); diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx index 2e4055256..5b4730848 100644 --- a/src/client/views/collections/CollectionPileView.tsx +++ b/src/client/views/collections/CollectionPileView.tsx @@ -1,44 +1,38 @@ -import { action, computed, observable, runInAction } from "mobx"; +import { action, computed } from "mobx"; import { observer } from "mobx-react"; -import { HeightSym, Opt, WidthSym, Doc } from "../../../fields/Doc"; -import { ScriptField } from "../../../fields/ScriptField"; -import { BoolCast, NumCast, StrCast } from "../../../fields/Types"; +import { Doc, HeightSym, WidthSym } from "../../../fields/Doc"; +import { NumCast, StrCast } from "../../../fields/Types"; +import { emptyFunction, setupMoveUpEvents } from "../../../Utils"; +import { DocUtils } from "../../documents/Documents"; +import { SelectionManager } from "../../util/SelectionManager"; +import { SnappingManager } from "../../util/SnappingManager"; +import { UndoManager } from "../../util/UndoManager"; import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView"; -import { CollectionSubView } from "./CollectionSubView"; import "./CollectionPileView.scss"; +import { CollectionSubView } from "./CollectionSubView"; import React = require("react"); -import { setupMoveUpEvents, emptyFunction, returnFalse } from "../../../Utils"; -import { SelectionManager } from "../../util/SelectionManager"; -import { UndoManager, undoBatch } from "../../util/UndoManager"; -import { SnappingManager } from "../../util/SnappingManager"; -import { DragManager } from "../../util/DragManager"; -import { DocUtils } from "../../documents/Documents"; @observer export class CollectionPileView extends CollectionSubView(doc => doc) { - _lastTap = 0; - _doubleTap: boolean | undefined = false; _originalChrome: string = ""; - @observable _contentsActive = true; - @observable _collapsed: boolean = false; - @observable _childClickedScript: Opt<ScriptField>; + componentDidMount() { if (this.layoutEngine() !== "pass" && this.layoutEngine() !== "starburst") { this.Document._pileLayoutEngine = "pass"; } this._originalChrome = StrCast(this.layoutDoc._chromeStatus); this.layoutDoc._chromeStatus = "disabled"; - this.layoutDoc.hideFilterView = true; } componentWillUnmount() { - this.layoutDoc.hideFilterView = false; this.layoutDoc._chromeStatus = this._originalChrome; } layoutEngine = () => StrCast(this.Document._pileLayoutEngine); + // returns the contents of the pileup in a CollectionFreeFormView @computed get contents() { - return <div className="collectionPileView-innards" style={{ pointerEvents: this.layoutEngine() === "starburst" ? undefined : "none" }} > + const draggingSelf = this.props.isSelected(); + return <div className="collectionPileView-innards" style={{ pointerEvents: this.layoutEngine() === "starburst" || (SnappingManager.GetIsDragging() && !draggingSelf) ? undefined : "none", zIndex: this.layoutEngine() === "starburst" && !SnappingManager.GetIsDragging() ? -10 : "auto" }} > <CollectionFreeFormView {...this.props} layoutEngine={this.layoutEngine} addDocument={(doc: Doc | Doc[]) => { (doc instanceof Doc ? [doc] : doc).map((d) => DocUtils.iconify(d)); @@ -50,6 +44,8 @@ export class CollectionPileView extends CollectionSubView(doc => doc) { }} /> </div>; } + + // toggles the pileup between starburst to compact toggleStarburst = action(() => { if (this.layoutEngine() === 'starburst') { const defaultSize = 110; @@ -80,11 +76,11 @@ export class CollectionPileView extends CollectionSubView(doc => doc) { } }); + // for dragging documents out of the pileup view _undoBatch: UndoManager.Batch | undefined; pointerDown = (e: React.PointerEvent) => { let dist = 0; SnappingManager.SetIsDragging(true); - // this._lastTap should be set to 0, and this._doubleTap should be set to false in the class header setupMoveUpEvents(this, e, (e: PointerEvent, down: number[], delta: number[]) => { if (this.layoutEngine() === "pass" && this.childDocs.length && e.shiftKey) { dist += Math.sqrt(delta[0] * delta[0] + delta[1] * delta[1]); @@ -110,8 +106,9 @@ export class CollectionPileView extends CollectionSubView(doc => doc) { }, emptyFunction, e.shiftKey && this.layoutEngine() === "pass", this.layoutEngine() === "pass" && e.shiftKey); // this sets _doubleTap } + // onClick for toggling the pileup view onClick = (e: React.MouseEvent) => { - if (e.button === 0) {//} && this._doubleTap) { + if (e.button === 0) { SelectionManager.DeselectAll(); this.toggleStarburst(); e.stopPropagation(); @@ -119,8 +116,7 @@ export class CollectionPileView extends CollectionSubView(doc => doc) { } render() { - - return <div className={"collectionPileView"} onClick={this.onClick} onPointerDown={this.pointerDown} + return <div className={`collectionPileView`} onClick={this.onClick} onPointerDown={this.pointerDown} style={{ width: this.props.PanelWidth(), height: `calc(100% - ${this.props.Document._chromeStatus === "enabled" ? 51 : 0}px)` }}> {this.contents} </div>; diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx index c566be7de..2896a8243 100644 --- a/src/client/views/collections/CollectionSchemaCells.tsx +++ b/src/client/views/collections/CollectionSchemaCells.tsx @@ -1,44 +1,38 @@ import React = require("react"); -import { action, observable, trace, computed, runInAction } from "mobx"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; +import DatePicker from "react-datepicker"; +import "react-datepicker/dist/react-datepicker.css"; import { CellInfo } from "react-table"; import "react-table/react-table.css"; -import { emptyFunction, returnFalse, returnZero, returnOne, returnEmptyFilter, Utils, emptyPath } from "../../../Utils"; +import { DateField } from "../../../fields/DateField"; import { Doc, DocListCast, Field, Opt } from "../../../fields/Doc"; import { Id } from "../../../fields/FieldSymbols"; -import { KeyCodes } from "../../util/KeyCodes"; -import { SetupDrag, DragManager } from "../../util/DragManager"; -import { CompileScript } from "../../util/Scripting"; -import { Transform } from "../../util/Transform"; -import { MAX_ROW_HEIGHT, COLLECTION_BORDER_WIDTH } from '../globalCssVariables.scss'; -import '../DocumentDecorations.scss'; -import { EditableView } from "../EditableView"; -import { FieldView, FieldViewProps } from "../nodes/FieldView"; -import "./CollectionSchemaView.scss"; -import { CollectionView, Flyout } from "./CollectionView"; -import { NumCast, StrCast, BoolCast, FieldValue, Cast, DateCast } from "../../../fields/Types"; -import { Docs } from "../../documents/Documents"; -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faExpand } from '@fortawesome/free-solid-svg-icons'; +import { List } from "../../../fields/List"; import { SchemaHeaderField } from "../../../fields/SchemaHeaderField"; -import { undoBatch } from "../../util/UndoManager"; -import { SnappingManager } from "../../util/SnappingManager"; import { ComputedField } from "../../../fields/ScriptField"; +import { BoolCast, Cast, DateCast, FieldValue, NumCast, StrCast } from "../../../fields/Types"; import { ImageField } from "../../../fields/URLField"; -import { List } from "../../../fields/List"; -import { OverlayView } from "../OverlayView"; -import { DocumentIconContainer } from "../nodes/DocumentIcon"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import DatePicker from "react-datepicker"; -import "react-datepicker/dist/react-datepicker.css"; -import { DateField } from "../../../fields/DateField"; -import { RichTextField } from "../../../fields/RichTextField"; +import { Utils, emptyFunction } from "../../../Utils"; +import { Docs } from "../../documents/Documents"; +import { DocumentType } from "../../documents/DocumentTypes"; import { DocumentManager } from "../../util/DocumentManager"; +import { DragManager } from "../../util/DragManager"; +import { KeyCodes } from "../../util/KeyCodes"; +import { CompileScript } from "../../util/Scripting"; import { SearchUtil } from "../../util/SearchUtil"; +import { SnappingManager } from "../../util/SnappingManager"; +import { undoBatch } from "../../util/UndoManager"; +import '../DocumentDecorations.scss'; +import { EditableView } from "../EditableView"; +import { MAX_ROW_HEIGHT } from '../globalCssVariables.scss'; +import { DocumentIconContainer } from "../nodes/DocumentIcon"; +import { OverlayView } from "../OverlayView"; +import "./CollectionSchemaView.scss"; +import { CollectionView } from "./CollectionView"; const path = require('path'); -library.add(faExpand); - export interface CellProps { row: number; col: number; @@ -64,20 +58,25 @@ export interface CellProps { @observer export class CollectionSchemaCell extends React.Component<CellProps> { + public static resolvedFieldKey(column: string, rowDoc: Doc) { + const fieldKey = column; + if (fieldKey.startsWith("*")) { + const rootKey = fieldKey.substring(1); + const allKeys = [...Array.from(Object.keys(rowDoc)), ...Array.from(Object.keys(Doc.GetProto(rowDoc)))]; + const matchedKeys = allKeys.filter(key => key.includes(rootKey)); + if (matchedKeys.length) return matchedKeys[0]; + } + return fieldKey; + } @observable protected _isEditing: boolean = false; protected _focusRef = React.createRef<HTMLDivElement>(); - protected _document = this.props.rowProps.original; + protected _rowDoc = this.props.rowProps.original; + protected _rowDataDoc = Doc.GetProto(this.props.rowProps.original); protected _dropDisposer?: DragManager.DragDropDisposer; - - async componentDidMount() { - document.addEventListener("keydown", this.onKeyDown); - } - @observable contents: string = ""; - componentWillUnmount() { - document.removeEventListener("keydown", this.onKeyDown); - } + componentDidMount() { document.addEventListener("keydown", this.onKeyDown); } + componentWillUnmount() { document.removeEventListener("keydown", this.onKeyDown); } @action onKeyDown = (e: KeyboardEvent): void => { @@ -99,7 +98,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> { @action onPointerDown = async (e: React.PointerEvent): Promise<void> => { - + this.onItemDown(e); this.props.changeFocusedCellByIndex(this.props.row, this.props.col); this.props.setPreviewDoc(this.props.rowProps.original); @@ -113,61 +112,39 @@ export class CollectionSchemaCell extends React.Component<CellProps> { } catch { } } - // this._isEditing = true; - // this.props.setIsEditing(true); - - const field = this.props.rowProps.original[this.props.rowProps.column.id!]; - const doc = FieldValue(Cast(field, Doc)); - if (typeof field === "object" && doc) this.props.setPreviewDoc(doc); + const doc = Cast(this._rowDoc[this.renderFieldKey], Doc, null); + doc && this.props.setPreviewDoc(doc); } @undoBatch applyToDoc = (doc: Doc, row: number, col: number, run: (args?: { [name: string]: any }) => any) => { const res = run({ this: doc, $r: row, $c: col, $: (r: number = 0, c: number = 0) => this.props.getField(r + row, c + col) }); if (!res.success) return false; - // doc[this.props.fieldKey] = res.result; - // return true; - doc[this.props.rowProps.column.id as string] = res.result; + doc[this.renderFieldKey] = res.result; return true; } private drop = (e: Event, de: DragManager.DropEvent) => { if (de.complete.docDragData) { - const fieldKey = this.props.rowProps.column.id as string; if (de.complete.docDragData.draggedDocuments.length === 1) { - this._document[fieldKey] = de.complete.docDragData.draggedDocuments[0]; + this._rowDataDoc[this.renderFieldKey] = de.complete.docDragData.draggedDocuments[0]; } else { const coll = Docs.Create.SchemaDocument([new SchemaHeaderField("title", "#f1efeb")], de.complete.docDragData.draggedDocuments, {}); - this._document[fieldKey] = coll; + this._rowDataDoc[this.renderFieldKey] = coll; } e.stopPropagation(); } } protected dropRef = (ele: HTMLElement | null) => { - this._dropDisposer && this._dropDisposer(); - if (ele) { - this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this)); - } + this._dropDisposer?.(); + ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this))); } - // expandDoc = (e: React.PointerEvent) => { - // let field = this.props.rowProps.original[this.props.rowProps.column.id as string]; - // let doc = FieldValue(Cast(field, Doc)); - - // this.props.setPreviewDoc(doc!); - - // // this.props.changeFocusedCellByIndex(this.props.row, this.props.col); - - // e.stopPropagation(); - // } - - returnHighlights(bing: (() => string), positions?: number[]) { - const results = []; - const contents = bing(); - - if (positions !== undefined) { + returnHighlights(contents: string, positions?: number[]) { + if (positions) { + const results = []; StrCast(this.props.Document._searchString); const length = StrCast(this.props.Document._searchString).length; const color = contents ? "black" : "grey"; @@ -182,71 +159,24 @@ export class CollectionSchemaCell extends React.Component<CellProps> { ); return results; } - else { - return <span style={{ color: contents ? "black" : "grey" }}>{contents ? contents?.valueOf() : "undefined"}</span>; - } + return <span style={{ color: contents ? "black" : "grey" }}>{contents ? contents?.valueOf() : "undefined"}</span>; } - type: string = ""; + @computed get renderFieldKey() { return CollectionSchemaCell.resolvedFieldKey(this.props.rowProps.column.id!, this.props.rowProps.original); } + onItemDown = async (e: React.PointerEvent) => { + if (this.props.Document._searchDoc) { + const aliasdoc = await SearchUtil.GetAliasesOfDocument(this._rowDataDoc); + const targetContext = aliasdoc.length <= 0 ? undefined : Cast(aliasdoc[0].context, Doc, null); + DocumentManager.Instance.jumpToDocument(this._rowDoc, false, emptyFunction, targetContext, + undefined, undefined, undefined, () => this.props.setPreviewDoc(this._rowDoc)); + } + } renderCellWithType(type: string | undefined) { const dragRef: React.RefObject<HTMLDivElement> = React.createRef(); - const props: FieldViewProps = { - Document: this.props.rowProps.original, - DataDoc: this.props.rowProps.original, - LibraryPath: [], - dropAction: "alias", - bringToFront: emptyFunction, - rootSelected: returnFalse, - fieldKey: this.props.rowProps.column.id as string, - docFilters: returnEmptyFilter, - ContainingCollectionView: this.props.CollectionView, - ContainingCollectionDoc: this.props.CollectionView && this.props.CollectionView.props.Document, - isSelected: returnFalse, - select: emptyFunction, - renderDepth: this.props.renderDepth + 1, - ScreenToLocalTransform: Transform.Identity, - focus: emptyFunction, - active: returnFalse, - whenActiveChanged: emptyFunction, - PanelHeight: returnZero, - PanelWidth: returnZero, - NativeHeight: returnZero, - NativeWidth: returnZero, - addDocTab: this.props.addDocTab, - pinToPres: this.props.pinToPres, - ContentScaling: returnOne - }; + const fieldKey = this.renderFieldKey; + const field = this._rowDoc[fieldKey]; - let matchedKeys = [props.fieldKey]; - if (props.fieldKey.startsWith("*")) { - const allKeys = Array.from(Object.keys(props.Document)); - allKeys.push(...Array.from(Object.keys(Doc.GetProto(props.Document)))); - matchedKeys = allKeys.filter(key => key.includes(props.fieldKey.substring(1))); - } - const fieldKey = matchedKeys.length ? matchedKeys[0] : props.fieldKey; - const field = props.Document[fieldKey]; - const doc = FieldValue(Cast(field, Doc)); - const fieldIsDoc = (type === "document" && typeof field === "object") || (typeof field === "object" && doc); - - const onItemDown = async (e: React.PointerEvent) => { - if (this.props.Document._searchDoc !== undefined) { - const doc = Doc.GetProto(this.props.rowProps.original); - const aliasdoc = await SearchUtil.GetAliasesOfDocument(doc); - let targetContext = undefined; - if (aliasdoc.length > 0) { - targetContext = Cast(aliasdoc[0].context, Doc) as Doc; - } - DocumentManager.Instance.jumpToDocument(this.props.rowProps.original, false, undefined, targetContext); - } - else { - fieldIsDoc && - SetupDrag(this._focusRef, - () => this._document[props.fieldKey] instanceof Doc ? this._document[props.fieldKey] : this._document, - this._document[props.fieldKey] instanceof Doc ? (doc: Doc | Doc[], target: Doc | undefined, addDoc: (newDoc: Doc | Doc[]) => any) => addDoc(doc) : this.props.moveDocument, - this._document[props.fieldKey] instanceof Doc ? "alias" : this.props.Document.schemaDoc ? "copy" : undefined)(e); - } - }; const onPointerEnter = (e: React.PointerEvent): void => { if (e.buttons === 1 && SnappingManager.GetIsDragging() && (type === "document" || type === undefined)) { dragRef.current!.className = "collectionSchemaView-cellContainer doc-drag-over"; @@ -256,49 +186,17 @@ export class CollectionSchemaCell extends React.Component<CellProps> { dragRef.current!.className = "collectionSchemaView-cellContainer"; }; - let contents: any = "incorrect type"; - if (type === undefined) contents = field === undefined ? undefined : Field.toString(field as Field);//StrCast(field) === "" ? "--" : <FieldView {...props} fieldKey={fieldKey} />; - if (type === "number") contents = typeof field === "number" ? NumCast(field) : "--" + typeof field + "--"; - if (type === "string") { - fieldKey === "text" ? - contents = Cast(field, RichTextField)?.Text : - contents = typeof field === "string" ? (StrCast(field) === "" ? "--" : StrCast(field)) : "--" + typeof field + "--"; - } - if (type === "boolean") contents = typeof field === "boolean" ? (BoolCast(field) ? "true" : "false") : "--" + typeof field + "--"; - if (type === "document") { - const doc = FieldValue(Cast(field, Doc)); - contents = typeof field === "object" ? doc ? StrCast(doc.title) === "" ? "--" : StrCast(doc.title) : `--${typeof field}--` : `--${typeof field}--`; - } - if (type === "image") { - const image = FieldValue(Cast(field, ImageField)); - const doc = FieldValue(Cast(field, Doc)); - contents = typeof field === "object" ? doc ? StrCast(doc.title) === "" ? "--" : StrCast(doc.title) : `--${typeof field}--` : `--${typeof field}--`; - } - if (type === "list") { - contents = typeof field === "object" ? doc ? StrCast(field) === "" ? "--" : StrCast(field) : `--${typeof field}--` : `--${typeof field}--`; - } - if (type === "date") { - contents = typeof field === "object" ? doc ? StrCast(field) === "" ? "--" : StrCast(field) : `--${typeof field}--` : `--${typeof field}--`; - } - + let contents = Field.toString(field as Field); + contents = contents === "" ? "--" : contents; let className = "collectionSchemaView-cellWrapper"; if (this._isEditing) className += " editing"; if (this.props.isFocused && this.props.isEditable) className += " focused"; if (this.props.isFocused && !this.props.isEditable) className += " inactive"; - - // let docExpander = ( - // <div className="collectionSchemaView-cellContents-docExpander" onPointerDown={this.expandDoc} > - // <FontAwesomeIcon icon="expand" size="sm" /> - // </div> - // ); const positions = []; - let cfield = props.Document[props.fieldKey]; - this.type = props.fieldKey; if (StrCast(this.props.Document._searchString).toLowerCase() !== "") { - let term = (cfield instanceof Promise) ? "...promise pending..." : Field.toString(cfield as Field); - term = term.toLowerCase(); + let term = (field instanceof Promise) ? "...promise pending..." : contents.toLowerCase(); const search = StrCast(this.props.Document._searchString).toLowerCase(); let start = term.indexOf(search); let tally = 0; @@ -315,178 +213,95 @@ export class CollectionSchemaCell extends React.Component<CellProps> { positions.pop(); } } - let search = false; - if (this.props.Document._searchDoc !== undefined) { - search = true; - } - const placeholder = type === "number" ? "0" : contents === "" ? "--" : "undefined"; return ( - <div className="collectionSchemaView-cellContainer" style={{ cursor: fieldIsDoc ? "grab" : "auto" }} + <div className="collectionSchemaView-cellContainer" style={{ cursor: field instanceof Doc ? "grab" : "auto" }} ref={dragRef} onPointerDown={this.onPointerDown} onPointerEnter={onPointerEnter} onPointerLeave={onPointerLeave}> - <div className={className} ref={this._focusRef} onPointerDown={onItemDown} tabIndex={-1}> - <div className="collectionSchemaView-cellContents" - ref={type === undefined || type === "document" ? this.dropRef : null} key={props.Document[Id]}> - {!search ? + <div className={className} ref={this._focusRef} tabIndex={-1}> + <div className="collectionSchemaView-cellContents" ref={type === undefined || type === "document" ? this.dropRef : null}> + {!this.props.Document._searchDoc ? <EditableView - positions={positions.length > 0 ? positions : undefined} - search={Cast(this.props.Document._searchString, "string", null)} editing={this._isEditing} isEditingCallback={this.isEditingCallback} display={"inline"} contents={contents} - highlight={positions.length > 0 ? true : undefined} - //contents={StrCast(contents)} height={"auto"} maxHeight={Number(MAX_ROW_HEIGHT)} placeholder={placeholder} - bing={() => { - const cfield = ComputedField.WithoutComputed(() => FieldValue(props.Document[props.fieldKey])); - if (cfield !== undefined) { - // if (typeof(cfield)===RichTextField) - const a = cfield as RichTextField; - const b = cfield as DateField; - console.log(b); - if (a.Text !== undefined) { - return (a.Text); - } - else if (b.toString() !== undefined) { - return b.toString(); - } - else if (StrCast(cfield)) { - return StrCast(cfield); - } - else { - return String(NumCast(cfield)); - } - } - }} GetValue={() => { - if (type === "number" && (contents === 0 || contents === "0")) { - return "0"; - } else { - const cfield = ComputedField.WithoutComputed(() => FieldValue(props.Document[props.fieldKey])); - if (type === "number") { - return StrCast(cfield); - } - const cscript = cfield instanceof ComputedField ? cfield.script.originalScript : undefined; - const cfinalScript = cscript?.split("return")[cscript.split("return").length - 1]; - const val = cscript !== undefined ? (cfinalScript?.endsWith(";") ? `:=${cfinalScript?.substring(0, cfinalScript.length - 2)}` : cfinalScript) : - Field.IsField(cfield) ? Field.toScriptString(cfield) : ""; - return val; - - } - + const cfield = ComputedField.WithoutComputed(() => FieldValue(field)); + const cscript = cfield instanceof ComputedField ? cfield.script.originalScript : undefined; + const cfinalScript = cscript?.split("return")[cscript.split("return").length - 1]; + return cscript ? (cfinalScript?.endsWith(";") ? `:=${cfinalScript?.substring(0, cfinalScript.length - 2)}` : cfinalScript) : + Field.IsField(cfield) ? Field.toScriptString(cfield) : ""; }} SetValue={action((value: string) => { let retVal = false; - - if (value.startsWith(":=")) { - retVal = this.props.setComputed(value.substring(2), props.Document, this.props.rowProps.column.id!, this.props.row, this.props.col); + if (value.startsWith(":=") || value.startsWith("=:=")) { + const script = value.substring(value.startsWith("=:=") ? 3 : 2); + retVal = this.props.setComputed(script, value.startsWith(":=") ? this._rowDataDoc : this._rowDoc, this.renderFieldKey, this.props.row, this.props.col); } else { - const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); - if (script.compiled) { - retVal = this.applyToDoc(props.Document, this.props.row, this.props.col, script.run); - } - + const inputscript = value.substring(value.startsWith("=") ? 1 : 0); + const script = CompileScript(inputscript, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); + script.compiled && (retVal = this.applyToDoc(inputscript.length !== value.length ? this._rowDoc : this._rowDataDoc, this.props.row, this.props.col, script.run)); } if (retVal) { this._isEditing = false; // need to set this here. otherwise, the assignment of the field will invalidate & cause render() to be called with the wrong value for 'editing' this.props.setIsEditing(false); } return retVal; - - //return true; })} OnFillDown={async (value: string) => { const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); - if (script.compiled) { - DocListCast(this.props.Document[this.props.fieldKey]). - forEach((doc, i) => value.startsWith(":=") ? - this.props.setComputed(value.substring(2), doc, this.props.rowProps.column.id!, i, this.props.col) : - this.applyToDoc(doc, i, this.props.col, script.run)); - } + script.compiled && DocListCast(this.props.Document[this.props.fieldKey]). + forEach((doc, i) => value.startsWith(":=") ? + this.props.setComputed(value.substring(2), Doc.GetProto(doc), this.renderFieldKey, i, this.props.col) : + this.applyToDoc(Doc.GetProto(doc), i, this.props.col, script.run)); }} /> : - this.returnHighlights(() => { - const dateCheck: Date | undefined = this.props.rowProps.original[this.props.rowProps.column.id as string] instanceof DateField ? DateCast(this.props.rowProps.original[this.props.rowProps.column.id as string]).date : undefined; - if (dateCheck !== undefined) { - cfield = dateCheck.toLocaleString(); - } - if (props.fieldKey === "context") { - cfield = this.contents; - } - if (props.fieldKey === "*lastModified") { - if (FieldValue(props.Document["data-lastModified"]) !== undefined) { - const d = ComputedField.WithoutComputed(() => FieldValue(props.Document["data-lastModified"])) as DateField; - cfield = d.date.toLocaleString(); - } - - else if (FieldValue(props.Document["text-lastModified"]) !== undefined) { - const d = ComputedField.WithoutComputed(() => FieldValue(props.Document["text-lastModified"])) as DateField; - cfield = d.date.toLocaleString(); - } - } - return Field.toString(cfield as Field); - }, positions) + this.returnHighlights(contents, positions) } </div > - {/* {fieldIsDoc ? docExpander : null} */} </div> </div> ); } - render() { - return this.renderCellWithType(undefined); - } + render() { return this.renderCellWithType(undefined); } } @observer -export class CollectionSchemaNumberCell extends CollectionSchemaCell { - render() { - return this.renderCellWithType("number"); - } -} +export class CollectionSchemaNumberCell extends CollectionSchemaCell { render() { return this.renderCellWithType("number"); } } @observer -export class CollectionSchemaBooleanCell extends CollectionSchemaCell { - render() { - return this.renderCellWithType("boolean"); - } -} +export class CollectionSchemaBooleanCell extends CollectionSchemaCell { render() { return this.renderCellWithType("boolean"); } } @observer -export class CollectionSchemaStringCell extends CollectionSchemaCell { - render() { - return this.renderCellWithType("string"); - } -} +export class CollectionSchemaStringCell extends CollectionSchemaCell { render() { return this.renderCellWithType("string"); } } @observer export class CollectionSchemaDateCell extends CollectionSchemaCell { - @observable private _date: Date = this.props.rowProps.original[this.props.rowProps.column.id as string] instanceof DateField ? DateCast(this.props.rowProps.original[this.props.rowProps.column.id as string]).date : - this.props.rowProps.original[this.props.rowProps.column.id as string] instanceof Date ? this.props.rowProps.original[this.props.rowProps.column.id as string] : new Date(); + @computed get _date(): Opt<DateField> { return this._rowDoc[this.renderFieldKey] instanceof DateField ? DateCast(this._rowDoc[this.renderFieldKey]) : undefined; } @action handleChange = (date: any) => { - this._date = date; // const script = CompileScript(date.toString(), { requiredType: "Date", addReturn: true, params: { this: Doc.name } }); // if (script.compiled) { // this.applyToDoc(this._document, this.props.row, this.props.col, script.run); // } else { // ^ DateCast is always undefined for some reason, but that is what the field should be set to - this._document[this.props.rowProps.column.id as string] = date as Date; + this._rowDoc[this.renderFieldKey] = new DateField(date as Date); //} } render() { - return <DatePicker - selected={this._date} - onSelect={date => this.handleChange(date)} - onChange={date => this.handleChange(date)} - />; + return !this.props.isFocused ? <span onPointerDown={this.onPointerDown}>{this._date ? Field.toString(this._date as Field) : "--"}</span> : + <DatePicker + selected={this._date?.date || new Date} + onSelect={date => this.handleChange(date)} + onChange={date => this.handleChange(date)} + />; } } @@ -495,44 +310,11 @@ export class CollectionSchemaDocCell extends CollectionSchemaCell { _overlayDisposer?: () => void; - private prop: FieldViewProps = { - Document: this.props.rowProps.original, - DataDoc: this.props.rowProps.original, - LibraryPath: [], - dropAction: "alias", - bringToFront: emptyFunction, - rootSelected: returnFalse, - fieldKey: this.props.rowProps.column.id as string, - ContainingCollectionView: this.props.CollectionView, - ContainingCollectionDoc: this.props.CollectionView && this.props.CollectionView.props.Document, - isSelected: returnFalse, - select: emptyFunction, - renderDepth: this.props.renderDepth + 1, - ScreenToLocalTransform: Transform.Identity, - focus: emptyFunction, - active: returnFalse, - whenActiveChanged: emptyFunction, - PanelHeight: returnZero, - PanelWidth: returnZero, - NativeHeight: returnZero, - NativeWidth: returnZero, - addDocTab: this.props.addDocTab, - pinToPres: this.props.pinToPres, - ContentScaling: returnOne, - docFilters: returnEmptyFilter - }; - @observable private _field = this.prop.Document[this.prop.fieldKey]; - @observable private _doc = FieldValue(Cast(this._field, Doc)); - @observable private _docTitle = this._doc?.title; - @observable private _preview = false; - @computed get previewWidth() { return () => NumCast(this.props.Document.schemaPreviewWidth); } - @computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); } - @computed get tableWidth() { return this.prop.PanelWidth() - 2 * this.borderWidth - 4 - this.previewWidth(); } + @computed get _doc() { return FieldValue(Cast(this._rowDoc[this.renderFieldKey], Doc)); } @action onSetValue = (value: string) => { - this._docTitle = value; - //this.prop.Document[this.prop.fieldKey] = this._text; + this._doc && (Doc.GetProto(this._doc).title = value); const script = CompileScript(value, { addReturn: true, @@ -542,46 +324,22 @@ export class CollectionSchemaDocCell extends CollectionSchemaCell { const results = script.compiled && script.run(); if (results && results.success) { - this._doc = results.result; - this._document[this.prop.fieldKey] = results.result; - this._docTitle = this._doc?.title; - + this._rowDoc[this.renderFieldKey] = results.result; return true; } return false; } + componentWillUnmount() { this.onBlur(); } + + onBlur = () => { this._overlayDisposer?.(); }; onFocus = () => { - this._overlayDisposer?.(); + this.onBlur(); this._overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 }); } @action - onOpenClick = () => { - this._preview = false; - if (this._doc) { - this.props.addDocTab(this._doc, "onRight"); - return true; - } - return false; - } - - @action - showPreview = (bool: boolean, e: any) => { - if (this._isEditing) { - this._preview = false; - } else { - if (bool) { - this.props.showDoc(this._doc, this.prop.DataDoc, e.clientX, e.clientY); - } else { - this.props.showDoc(undefined); - } - } - } - - @action - isEditingCalling = (isEditing: boolean): void => { - this.showPreview(false, ""); + isEditingCallback = (isEditing: boolean): void => { document.removeEventListener("keydown", this.onKeyDown); isEditing && document.addEventListener("keydown", this.onKeyDown); this._isEditing = isEditing; @@ -589,227 +347,90 @@ export class CollectionSchemaDocCell extends CollectionSchemaCell { this.props.changeFocusedCellByIndex(this.props.row, this.props.col); } - onDown = (e: any) => { - this.props.changeFocusedCellByIndex(this.props.row, this.props.col); - this.props.setPreviewDoc(this.props.rowProps.original); - - let url: string; - if (url = StrCast(this.props.rowProps.row.href)) { - try { - new URL(url); - const temp = window.open(url)!; - temp.blur(); - window.focus(); - } catch { } - } - - const field = this.props.rowProps.original[this.props.rowProps.column.id!]; - const doc = FieldValue(Cast(field, Doc)); - if (typeof field === "object" && doc) this.props.setPreviewDoc(doc); - - this.showPreview(true, e); - - } - render() { - if (typeof this._field === "object" && this._doc && this._docTitle) { - return ( - <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} - onPointerDown={this.onDown} - onPointerEnter={(e) => { this.showPreview(true, e); }} - onPointerLeave={(e) => { this.showPreview(false, e); }} + return !this._doc ? this.renderCellWithType("document") : + <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} + onPointerDown={this.onPointerDown} + > + <div className="collectionSchemaView-cellContents-document" + style={{ padding: "5.9px" }} + ref={this.dropRef} + onFocus={this.onFocus} + onBlur={this.onBlur} > - - <div className="collectionSchemaView-cellContents-document" - style={{ padding: "5.9px" }} - ref={this.dropRef} - onFocus={this.onFocus} - onBlur={() => this._overlayDisposer?.()} - > - - <EditableView - editing={this._isEditing} - isEditingCallback={this.isEditingCalling} - display={"inline"} - contents={this._docTitle} - height={"auto"} - maxHeight={Number(MAX_ROW_HEIGHT)} - GetValue={() => { - return StrCast(this._docTitle); - }} - SetValue={action((value: string) => { - this.onSetValue(value); - this.showPreview(false, ""); - return true; - })} - /> - </div > - <div onClick={this.onOpenClick} className="collectionSchemaView-cellContents-docButton"> - <FontAwesomeIcon icon="external-link-alt" size="lg" ></FontAwesomeIcon> </div> + <EditableView + editing={this._isEditing} + isEditingCallback={this.isEditingCallback} + display={"inline"} + contents={this._doc.title || "--"} + height={"auto"} + maxHeight={Number(MAX_ROW_HEIGHT)} + GetValue={() => StrCast(this._doc?.title)} + SetValue={action((value: string) => { + this.onSetValue(value); + return true; + })} + /> + </div > + <div onClick={() => this._doc && this.props.addDocTab(this._doc, "add:right")} className="collectionSchemaView-cellContents-docButton"> + <FontAwesomeIcon icon="external-link-alt" size="lg" /> </div> - ); - } else { - return this.renderCellWithType("document"); - } + </div>; } } @observer export class CollectionSchemaImageCell extends CollectionSchemaCell { - // render() { - // return this.renderCellWithType("image"); - // } - - choosePath(url: URL, dataDoc: any) { - const lower = url.href.toLowerCase(); - if (url.protocol === "data") { - return url.href; - } else if (url.href.indexOf(window.location.origin) === -1) { - return Utils.CorsProxy(url.href); - } else if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower)) { - return url.href;//Why is this here - } + + choosePath(url: URL) { + if (url.protocol === "data") return url.href; + if (url.href.indexOf(window.location.origin) === -1) return Utils.CorsProxy(url.href); + if (!/\.(png|jpg|jpeg|gif|webp)$/.test(url.href.toLowerCase())) return url.href;//Why is this here + const ext = path.extname(url.href); - const _curSuffix = "_o"; - return url.href.replace(ext, _curSuffix + ext); + return url.href.replace(ext, "_o" + path.extname(url.href)); } render() { - const props: FieldViewProps = { - Document: this.props.rowProps.original, - DataDoc: this.props.rowProps.original, - LibraryPath: [], - dropAction: "alias", - bringToFront: emptyFunction, - rootSelected: returnFalse, - fieldKey: this.props.rowProps.column.id as string, - ContainingCollectionView: this.props.CollectionView, - ContainingCollectionDoc: this.props.CollectionView && this.props.CollectionView.props.Document, - isSelected: returnFalse, - select: emptyFunction, - renderDepth: this.props.renderDepth + 1, - ScreenToLocalTransform: Transform.Identity, - focus: emptyFunction, - active: returnFalse, - whenActiveChanged: emptyFunction, - PanelHeight: returnZero, - PanelWidth: returnZero, - NativeHeight: returnZero, - NativeWidth: returnZero, - addDocTab: this.props.addDocTab, - pinToPres: this.props.pinToPres, - ContentScaling: returnOne, - docFilters: returnEmptyFilter - }; + const field = Cast(this._rowDoc[this.renderFieldKey], ImageField, null); // retrieve the primary image URL that is being rendered from the data doc + const alts = DocListCast(this._rowDoc[this.renderFieldKey + "-alternates"]); // retrieve alternate documents that may be rendered as alternate images + const altpaths = alts.map(doc => Cast(doc[Doc.LayoutFieldKey(doc)], ImageField, null)?.url).filter(url => url).map(url => this.choosePath(url)); // access the primary layout data of the alternate documents + const paths = field ? [this.choosePath(field.url), ...altpaths] : altpaths; + const url = paths.length ? paths : [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")]; - let image = true; - let url = []; - if (props.DataDoc) { - const field = Cast(props.DataDoc[props.fieldKey], ImageField, null); // retrieve the primary image URL that is being rendered from the data doc - const alts = DocListCast(props.DataDoc[props.fieldKey + "-alternates"]); // retrieve alternate documents that may be rendered as alternate images - const altpaths = alts.map(doc => Cast(doc[Doc.LayoutFieldKey(doc)], ImageField, null)?.url).filter(url => url).map(url => this.choosePath(url, props.DataDoc)); // access the primary layout data of the alternate documents - const paths = field ? [this.choosePath(field.url, props.DataDoc), ...altpaths] : altpaths; - if (paths.length) { - url = paths; - } else { - url = [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")]; - image = false; - } - //url = paths.length ? paths : [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")]; - } else { - url = [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")]; - image = false; - } + const heightToWidth = NumCast(this._rowDoc._nativeHeight) / NumCast(this._rowDoc._nativeWidth); + let width = Math.min(75, this.props.rowProps.width); + const height = Math.min(75, width * heightToWidth); + width = height / heightToWidth; - const heightToWidth = NumCast(props.DataDoc?._nativeHeight) / NumCast(props.DataDoc?._nativeWidth); - const height = this.props.rowProps.width * heightToWidth; - - if (props.fieldKey === "data") { - if (url !== []) { - const reference = React.createRef<HTMLDivElement>(); - return ( - <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}> - <div className="collectionSchemaView-cellContents" key={this._document[Id]} ref={reference}> - <img src={url[0]} width={image ? this.props.rowProps.width : "30px"} - height={image ? height : "30px"} /> - </div > - </div> - ); - - } else { - return this.renderCellWithType("image"); - } - } else { - return this.renderCellWithType("image"); - } + const reference = React.createRef<HTMLDivElement>(); + return <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}> + <div className="collectionSchemaView-cellContents" key={this._rowDoc[Id]} ref={reference}> + <img src={url[0]} + width={paths.length ? width : "20px"} + height={paths.length ? height : "20px"} /> + </div > + </div>; } } - - - @observer export class CollectionSchemaListCell extends CollectionSchemaCell { - _overlayDisposer?: () => void; - private prop: FieldViewProps = { - Document: this.props.rowProps.original, - DataDoc: this.props.rowProps.original, - LibraryPath: [], - dropAction: "alias", - bringToFront: emptyFunction, - rootSelected: returnFalse, - fieldKey: this.props.rowProps.column.id as string, - ContainingCollectionView: this.props.CollectionView, - ContainingCollectionDoc: this.props.CollectionView && this.props.CollectionView.props.Document, - isSelected: returnFalse, - select: emptyFunction, - renderDepth: this.props.renderDepth + 1, - ScreenToLocalTransform: Transform.Identity, - focus: emptyFunction, - active: returnFalse, - whenActiveChanged: emptyFunction, - PanelHeight: returnZero, - PanelWidth: returnZero, - NativeHeight: returnZero, - NativeWidth: returnZero, - addDocTab: this.props.addDocTab, - pinToPres: this.props.pinToPres, - ContentScaling: returnOne, - docFilters: returnEmptyFilter - }; - @observable private _field = this.prop.Document[this.prop.fieldKey]; - @observable private _optionsList = this._field as List<any>; + @computed get _field() { return this._rowDoc[this.renderFieldKey]; } + @computed get _optionsList() { return this._field as List<any>; } @observable private _opened = false; @observable private _text = "select an item"; @observable private _selectedNum = 0; @action - toggleOpened(open: boolean) { - this._opened = open; - } - - // @action - // onChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { - // this._text = e.target.value; - - // // change if its a document - // this._optionsList[this._selectedNum] = this._text; - // } - - @action onSetValue = (value: string) => { - - - this._text = value; - // change if its a document - this._optionsList[this._selectedNum] = this._text; - - (this.prop.Document[this.prop.fieldKey] as List<any>).splice(this._selectedNum, 1, value); + this._optionsList[this._selectedNum] = this._text = value; + (this._field as List<any>).splice(this._selectedNum, 1, value); } @action @@ -823,55 +444,23 @@ export class CollectionSchemaListCell extends CollectionSchemaCell { this._overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 }); } - render() { - - const dragRef: React.RefObject<HTMLDivElement> = React.createRef(); - - let type = "list"; - - let link = false; - let doc = false; + const link = false; const reference = React.createRef<HTMLDivElement>(); - if (typeof this._field === "object" && this._optionsList[0]) { - - const options = this._optionsList.map((element, index) => { - - if (element instanceof Doc) { - doc = true; - type = "document"; - if (this.prop.fieldKey.toLowerCase() === "links") { - link = true; - type = "link"; - } - const document = FieldValue(Cast(element, Doc)); - const title = element.title; - return <div - className="collectionSchemaView-dropdownOption" - onPointerDown={(e) => { this.onSelected(StrCast(element.title), index); }} - style={{ padding: "6px" }}> - {title} - </div>; - - } else { - return <div - className="collectionSchemaView-dropdownOption" - onPointerDown={(e) => { this.onSelected(StrCast(element), index); }} - style={{ padding: "6px" }}>{element}</div>; - } - }); + if (this._optionsList?.length) { + const options = !this._opened ? (null) : + <div> + {this._optionsList.map((element, index) => { + const val = Field.toString(element); + return <div className="collectionSchemaView-dropdownOption" key={index} style={{ padding: "6px" }} onPointerDown={(e) => this.onSelected(StrCast(element), index)} > + {val} + </div>; + })} + </div>; const plainText = <div style={{ padding: "5.9px" }}>{this._text}</div>; - // const textarea = <textarea onChange={this.onChange} value={this._text} - // onFocus={doc ? this.onFocus : undefined} - // onBlur={doc ? e => this._overlayDisposer?.() : undefined} - // style={{ resize: "none" }} - // placeholder={"select an item"}></textarea>; - - const textarea = <div className="collectionSchemaView-cellContents" - style={{ padding: "5.9px" }} - ref={type === undefined || type === "document" ? this.dropRef : null} key={this.prop.Document[Id]}> + const textarea = <div className="collectionSchemaView-cellContents" key={this._rowDoc[Id]} style={{ padding: "5.9px" }} ref={this.dropRef} > <EditableView editing={this._isEditing} isEditingCallback={this.isEditingCallback} @@ -879,11 +468,8 @@ export class CollectionSchemaListCell extends CollectionSchemaCell { contents={this._text} height={"auto"} maxHeight={Number(MAX_ROW_HEIGHT)} - GetValue={() => { - return this._text; - }} + GetValue={() => this._text} SetValue={action((value: string) => { - // add special for params this.onSetValue(value); return true; @@ -892,61 +478,35 @@ export class CollectionSchemaListCell extends CollectionSchemaCell { </div >; //☰ - - const dropdown = <div> - {options} - </div>; - return ( <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}> - <div className="collectionSchemaView-cellContents" key={this._document[Id]} ref={reference}> + <div className="collectionSchemaView-cellContents" key={this._rowDoc[Id]} ref={reference}> <div className="collectionSchemaView-dropDownWrapper"> - <button type="button" className="collectionSchemaView-dropdownButton" onClick={(e) => { this.toggleOpened(!this._opened); }} - style={{ right: "length", position: "relative" }}> - {this._opened ? <FontAwesomeIcon icon="caret-up" size="lg" ></FontAwesomeIcon> - : <FontAwesomeIcon icon="caret-down" size="lg" ></FontAwesomeIcon>} + <button type="button" className="collectionSchemaView-dropdownButton" style={{ right: "length", position: "relative" }} + onClick={action(e => this._opened = !this._opened)} > + <FontAwesomeIcon icon={this._opened ? "caret-up" : "caret-down"} size="sm" /> </button> <div className="collectionSchemaView-dropdownText"> {link ? plainText : textarea} </div> </div> - - {this._opened ? dropdown : null} + {options} </div > </div> ); - } else { - return this.renderCellWithType("list"); } + return this.renderCellWithType("list"); } } - - - @observer export class CollectionSchemaCheckboxCell extends CollectionSchemaCell { - @observable private _isChecked: boolean = typeof this.props.rowProps.original[this.props.rowProps.column.id as string] === "boolean" ? BoolCast(this.props.rowProps.original[this.props.rowProps.column.id as string]) : false; - - @action - toggleChecked = (e: React.ChangeEvent<HTMLInputElement>) => { - this._isChecked = e.target.checked; - const script = CompileScript(e.target.checked.toString(), { requiredType: "boolean", addReturn: true, params: { this: Doc.name } }); - if (script.compiled) { - this.applyToDoc(this._document, this.props.row, this.props.col, script.run); - } - } + @computed get _isChecked() { return BoolCast(this._rowDoc[this.renderFieldKey]); } render() { const reference = React.createRef<HTMLDivElement>(); - const onItemDown = (e: React.PointerEvent) => { - (!this.props.CollectionView || !this.props.CollectionView.props.isSelected() ? undefined : - SetupDrag(reference, () => this._document, this.props.moveDocument, this.props.Document.schemaDoc ? "copy" : undefined)(e)); - }; return ( <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}> - <div className="collectionSchemaView-cellContents" onPointerDown={onItemDown} key={this._document[Id]} ref={reference}> - <input type="checkbox" checked={this._isChecked} onChange={this.toggleChecked} /> - </div > + <input type="checkbox" checked={this._isChecked} onChange={e => this._rowDoc[this.renderFieldKey] = e.target.checked} /> </div> ); } @@ -955,62 +515,15 @@ export class CollectionSchemaCheckboxCell extends CollectionSchemaCell { @observer export class CollectionSchemaButtons extends CollectionSchemaCell { - render() { - // const reference = React.createRef<HTMLDivElement>(); - // const onItemDown = (e: React.PointerEvent) => { - // (!this.props.CollectionView || !this.props.CollectionView.props.isSelected() ? undefined : - // SetupDrag(reference, () => this._document, this.props.moveDocument, this.props.Document.schemaDoc ? "copy" : undefined)(e)); - // }; - const doc = this.props.rowProps.original; - let buttons: JSX.Element | undefined = undefined; - buttons = <div style={{ - paddingTop: 8, - paddingLeft: 3, - }}><button onClick={() => { - doc.searchMatch = false; - setTimeout(() => doc.searchMatch = true, 0); - }} style={{ padding: 2, left: 77 }}> - <FontAwesomeIcon icon="arrow-up" size="sm" /> - </button> - <button onClick={() => { - { - doc.searchMatchAlt = false; - setTimeout(() => doc.searchMatchAlt = true, 0); - } - }} style={{ padding: 2 }}> - <FontAwesomeIcon icon="arrow-down" size="sm" /> - </button></div>; - const type = StrCast(doc.type); - if (type === "pdf") { - buttons = <div><button - style={{ - position: "relative", - height: 30, - width: 28, - left: 1, - }} - - onClick={() => { - doc.searchMatch = false; - setTimeout(() => doc.searchMatch = true, 0); - }}> - <FontAwesomeIcon icon="arrow-down" size="sm" /> - </button></div >; - } - else if (type !== "rtf") { - buttons = undefined; - } - - if (BoolCast(this.props.Document._searchDoc) === true) { - - } - else { - buttons = undefined; - } - return ( - <div> {buttons}</div> - ); + return !this.props.Document._searchDoc || ![DocumentType.PDF, DocumentType.RTF].includes(StrCast(this._rowDoc.type) as DocumentType) ? <></> : + <div style={{ paddingTop: 8, paddingLeft: 3 }} > + <button style={{ padding: 2, left: 77 }} onClick={() => Doc.SearchMatchNext(this._rowDoc, true)}> + <FontAwesomeIcon icon="arrow-up" size="sm" /> + </button> + <button style={{ padding: 2 }} onClick={() => Doc.SearchMatchNext(this._rowDoc, false)} > + <FontAwesomeIcon icon="arrow-down" size="sm" /> + </button> + </div>; } -} - +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionSchemaHeaders.tsx b/src/client/views/collections/CollectionSchemaHeaders.tsx index be25bf9de..dbf7488ec 100644 --- a/src/client/views/collections/CollectionSchemaHeaders.tsx +++ b/src/client/views/collections/CollectionSchemaHeaders.tsx @@ -13,57 +13,11 @@ import { SearchBox } from "../search/SearchBox"; import { ColumnType } from "./CollectionSchemaView"; import "./CollectionSchemaView.scss"; import { CollectionView } from "./CollectionView"; -import * as fa from '@fortawesome/free-solid-svg-icons'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; -export interface HeaderProps { - keyValue: SchemaHeaderField; - possibleKeys: string[]; - existingKeys: string[]; - keyType: ColumnType; - typeConst: boolean; - onSelect: (oldKey: string, newKey: string, addnew: boolean) => void; - setIsEditing: (isEditing: boolean) => void; - deleteColumn: (column: string) => void; - setColumnType: (column: SchemaHeaderField, type: ColumnType) => void; - setColumnSort: (column: SchemaHeaderField, desc: boolean | undefined) => void; - setColumnColor: (column: SchemaHeaderField, color: string) => void; - -} - -export class CollectionSchemaHeader extends React.Component<HeaderProps> { - render() { - const icon: IconProp = this.props.keyType === ColumnType.Number ? "hashtag" : this.props.keyType === ColumnType.String ? "font" : - this.props.keyType === ColumnType.Boolean ? "check-square" : this.props.keyType === ColumnType.Doc ? "sort-amount-down" : - this.props.keyType === ColumnType.Image ? "image" : this.props.keyType === ColumnType.List ? "list-ul" : this.props.keyType === ColumnType.Date ? "calendar" : - "align-justify"; - return ( - <div className="collectionSchemaView-header" style={{ background: this.props.keyValue.color }}> - <CollectionSchemaColumnMenu - columnField={this.props.keyValue} - // keyValue={this.props.keyValue.heading} - possibleKeys={this.props.possibleKeys} - existingKeys={this.props.existingKeys} - // keyType={this.props.keyType} - typeConst={this.props.typeConst} - menuButtonContent={<div><FontAwesomeIcon icon={icon} size="sm" />{this.props.keyValue.heading}</div>} - addNew={false} - onSelect={this.props.onSelect} - setIsEditing={this.props.setIsEditing} - deleteColumn={this.props.deleteColumn} - onlyShowOptions={false} - setColumnType={this.props.setColumnType} - setColumnSort={this.props.setColumnSort} - setColumnColor={this.props.setColumnColor} - /> - </div> - ); - } -} - export interface AddColumnHeaderProps { createColumn: () => void; @@ -79,7 +33,6 @@ export class CollectionSchemaAddColumnHeader extends React.Component<AddColumnHe } - export interface ColumnMenuProps { columnField: SchemaHeaderField; // keyValue: string; @@ -103,37 +56,29 @@ export class CollectionSchemaColumnMenu extends React.Component<ColumnMenuProps> @observable private _isOpen: boolean = false; @observable private _node: HTMLDivElement | null = null; - componentDidMount() { - document.addEventListener("pointerdown", this.detectClick); - } + componentDidMount() { document.addEventListener("pointerdown", this.detectClick); } - componentWillUnmount() { - document.removeEventListener("pointerdown", this.detectClick); - } + componentWillUnmount() { document.removeEventListener("pointerdown", this.detectClick); } - detectClick = (e: PointerEvent): void => { - if (this._node && this._node.contains(e.target as Node)) { - } else { - this._isOpen = false; - this.props.setIsEditing(false); - } + @action + detectClick = (e: PointerEvent) => { + !this._node?.contains(e.target as Node) && this.props.setIsEditing(this._isOpen = false); } @action toggleIsOpen = (): void => { - this._isOpen = !this._isOpen; - this.props.setIsEditing(this._isOpen); + this.props.setIsEditing(this._isOpen = !this._isOpen); } - changeColumnType = (type: ColumnType): void => { + changeColumnType = (type: ColumnType) => { this.props.setColumnType(this.props.columnField, type); } - changeColumnSort = (desc: boolean | undefined): void => { + changeColumnSort = (desc: boolean | undefined) => { this.props.setColumnSort(this.props.columnField, desc); } - changeColumnColor = (color: string): void => { + changeColumnColor = (color: string) => { this.props.setColumnColor(this.props.columnField, color); } @@ -145,7 +90,7 @@ export class CollectionSchemaColumnMenu extends React.Component<ColumnMenuProps> } renderTypes = () => { - if (this.props.typeConst) return <></>; + if (this.props.typeConst) return (null); const type = this.props.columnField.type; return ( @@ -291,7 +236,7 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { @observable private _key: string = this.props.keyValue; @observable private _searchTerm: string = this.props.keyValue; @observable private _isOpen: boolean = false; - @observable private _canClose: boolean = true; + @observable private _node: HTMLDivElement | null = null; @observable private _inputRef: React.RefObject<HTMLInputElement> = React.createRef(); @action setSearchTerm = (value: string): void => { this._searchTerm = value; }; @@ -306,6 +251,31 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { this.props.setIsEditing(false); } + @action + setNode = (node: HTMLDivElement): void => { + if (node) { + this._node = node; + } + } + + componentDidMount() { + document.addEventListener("pointerdown", this.detectClick); + const filters = Cast(this.props.Document._docFilters, listSpec("string")); + if (filters?.includes(this._key)) { + runInAction(() => this.closeResultsVisibility = "contents"); + } + } + + @action + detectClick = (e: PointerEvent): void => { + if (this._node && this._node.contains(e.target as Node)) { + } else { + this._isOpen = false; + this.props.setIsEditing(false); + } + } + + private tempfilter: string = ""; @undoBatch onKeyDown = (e: React.KeyboardEvent): void => { if (e.key === "Enter") { @@ -313,23 +283,24 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { const colpos = this._searchTerm.indexOf(":"); const temp = this._searchTerm.slice(colpos + 1, this._searchTerm.length); if (temp === "") { - Doc.setDocFilter(this.props.Document, this._key, temp, undefined); + Doc.setDocFilter(this.props.Document, this._key, this.tempfilter, undefined); + this.updateFilter(); } else { - Doc.setDocFilter(this.props.Document, this._key, temp, "match"); + Doc.setDocFilter(this.props.Document, this._key, this.tempfilter, undefined); + this.tempfilter = temp; + Doc.setDocFilter(this.props.Document, this._key, temp, "check"); this.props.col.setColor("green"); + this.closeResultsVisibility = "contents"; } } else { - let keyOptions = this._searchTerm === "" ? this.props.possibleKeys : this.props.possibleKeys.filter(key => key.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1); - const blockedkeys = ["_scrollTop", "customTitle", "limitHeight", "proto", "x", "y", "_width", "_height", "_autoHeight", "_fontSize", "_fontFamily", "context", "zIndex", "_timeStampOnEnter", "lines", "highlighting", "searchMatch", "creationDate", "isPrototype", "text-annotations", "aliases", "text-lastModified", "text-noTemplate", "layoutKey", "baseProto", "_xMargin", "_yMargin", "layout", "layout_keyValue", "links"]; - keyOptions = keyOptions.filter(n => !blockedkeys.includes(n)); - if (keyOptions.length) { - this.onSelect(keyOptions[0]); - console.log("case1"); + Doc.setDocFilter(this.props.Document, this._key, this.tempfilter, undefined); + this.updateFilter(); + if (this.showKeys.length) { + this.onSelect(this.showKeys[0]); } else if (this._searchTerm !== "" && this.props.canAddNew) { this.setSearchTerm(this._searchTerm || this._key); - console.log("case2"); this.onSelect(this._searchTerm); } } @@ -346,22 +317,14 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { this.props.setIsEditing(true); } - @action - onBlur = (e: React.FocusEvent): void => { - if (this._canClose) { - this._isOpen = false; - this.props.setIsEditing(false); - } - } - - @action - onPointerEnter = (e: React.PointerEvent): void => { - this._canClose = false; - } - - @action - onPointerOut = (e: React.PointerEvent): void => { - this._canClose = true; + @computed get showKeys() { + const whitelistKeys = ["context", "author", "*lastModified", "text", "data", "tags", "creationDate"]; + const keyOptions = this._searchTerm === "" ? this.props.possibleKeys : this.props.possibleKeys.filter(key => key.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1); + const showKeys = new Set<string>(); + [...keyOptions, ...whitelistKeys].forEach(key => (!Doc.UserDoc().noviceMode || + whitelistKeys.includes(key) + || ((!key.startsWith("_") && key[0] === key[0].toUpperCase()) || key[0] === "#")) ? showKeys.add(key) : null); + return Array.from(showKeys.keys()).filter(key => !this._searchTerm || key.includes(this._searchTerm)); } @action renderOptions = (): JSX.Element[] | JSX.Element => { @@ -369,28 +332,24 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { this.defaultMenuHeight = 0; return <></>; } - const searchTerm = this._searchTerm.trim() === "New field" ? "" : this._searchTerm; - - let keyOptions = searchTerm === "" ? this.props.possibleKeys : this.props.possibleKeys.filter(key => key.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1); - const exactFound = keyOptions.findIndex(key => key.toUpperCase() === this._searchTerm.toUpperCase()) > -1 || - this.props.existingKeys.findIndex(key => key.toUpperCase() === this._searchTerm.toUpperCase()) > -1; - - const blockedkeys = ["proto", "x", "y", "_width", "_height", "_autoHeight", "_fontSize", "_fontFamily", "context", "zIndex", "_timeStampOnEnter", "lines", "highlighting", "searchMatch", "creationDate", "isPrototype", "text-annotations", "aliases", "text-lastModified", "text-noTemplate", "layoutKey", "baseProto", "_xMargin", "_yMargin", "layout", "layout_keyValue", "links"]; - keyOptions = keyOptions.filter(n => !blockedkeys.includes(n)); - - const options = keyOptions.map(key => { + const options = this.showKeys.map(key => { return <div key={key} className="key-option" style={{ border: "1px solid lightgray", width: this.props.width, maxWidth: this.props.width, overflowX: "hidden", background: "white", }} - onPointerDown={e => e.stopPropagation()} onClick={() => { this.onSelect(key); this.setSearchTerm(""); }}>{key}</div>; + onPointerDown={e => { + e.stopPropagation(); + }} + onClick={() => { + this.onSelect(key); + this.setSearchTerm(""); + }}>{key}</div>; }); // if search term does not already exist as a group type, give option to create new group type if (this._key !== this._searchTerm.slice(0, this._key.length)) { - console.log("little further"); - if (!exactFound && this._searchTerm !== "" && this.props.canAddNew) { + if (this._searchTerm !== "" && this.props.canAddNew) { options.push(<div key={""} className="key-option" style={{ border: "1px solid lightgray", width: this.props.width, maxWidth: this.props.width, overflowX: "hidden", background: "white", }} @@ -418,55 +377,55 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { @action renderFilterOptions = (): JSX.Element[] | JSX.Element => { - if (!this._isOpen) { + if (!this._isOpen || !this.props.dataDoc) { this.defaultMenuHeight = 0; return <></>; } - const keyOptions: string[] = []; const colpos = this._searchTerm.indexOf(":"); const temp = this._searchTerm.slice(colpos + 1, this._searchTerm.length); if (this.docSafe.length === 0) { - this.docSafe = DocListCast(this.props.dataDoc![this.props.fieldKey]); + this.docSafe = DocListCast(this.props.dataDoc[this.props.fieldKey]); } const docs = this.docSafe; docs.forEach((doc) => { const key = StrCast(doc[this._key]); - if (keyOptions.includes(key) === false && key.includes(temp)) { + if (keyOptions.includes(key) === false && key.includes(temp) && key !== "") { keyOptions.push(key); } }); const filters = Cast(this.props.Document._docFilters, listSpec("string")); + if (filters === undefined || filters.length === 0 || filters.includes(this._key) === false) { + this.props.col.setColor("rgb(241, 239, 235)"); + this.closeResultsVisibility = "none"; + } for (let i = 0; i < (filters?.length ?? 0) - 1; i += 3) { if (filters![i] === this.props.col.heading && keyOptions.includes(filters![i + 1]) === false) { keyOptions.push(filters![i + 1]); } } - - if (filters === undefined || filters.length === 0 || filters.includes(this._key) === false) { - this.props.col.setColor("rgb(241, 239, 235)"); - } - const options = keyOptions.map(key => { - //Doc.setDocFilter(this.props.Document!, this._key, key, undefined); let bool = false; - console.log(filters); if (filters !== undefined) { bool = filters.includes(key) && filters[filters.indexOf(key) + 1] === "check"; - console.log(filters.includes(key)); } return <div key={key} className="key-option" style={{ border: "1px solid lightgray", paddingLeft: 5, textAlign: "left", width: this.props.width, maxWidth: this.props.width, overflowX: "hidden", background: "white", backgroundColor: "white", }} > - <input type="checkbox" onChange={(e) => { - e.target.checked === true ? Doc.setDocFilter(this.props.Document, this._key, key, "check") : Doc.setDocFilter(this.props.Document, this._key, key, undefined); - e.target.checked === true ? this.props.col.setColor("green") : ""; - e.target.checked === true && SearchBox.Instance.filter === true ? Doc.setDocFilter(docs[0], this._key, key, "check") : Doc.setDocFilter(docs[0], this._key, key, undefined); - }} - checked={bool} ></input> + <input type="checkbox" + onPointerDown={e => e.stopPropagation()} + onClick={e => e.stopPropagation()} + onChange={(e) => { + e.target.checked === true ? Doc.setDocFilter(this.props.Document, this._key, key, "check") : Doc.setDocFilter(this.props.Document, this._key, key, undefined); + e.target.checked === true ? this.closeResultsVisibility = "contents" : console.log(""); + e.target.checked === true ? this.props.col.setColor("green") : this.updateFilter(); + e.target.checked === true && SearchBox.Instance.filter === true ? Doc.setDocFilter(docs[0], this._key, key, "check") : Doc.setDocFilter(docs[0], this._key, key, undefined); + }} + checked={bool} + /> <span style={{ paddingLeft: 4 }}> {key} </span> @@ -492,6 +451,14 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { @observable defaultMenuHeight = 0; + updateFilter() { + const filters = Cast(this.props.Document._docFilters, listSpec("string")); + if (filters === undefined || filters.length === 0 || filters.includes(this._key) === false) { + this.props.col.setColor("rgb(241, 239, 235)"); + this.closeResultsVisibility = "none"; + } + } + get ignoreFields() { return ["_docFilters", "_docRangeFilters"]; } @@ -501,29 +468,48 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { return script ? () => script : undefined; } filterBackground = () => "rgba(105, 105, 105, 0.432)"; - @observable filterOpen: boolean | undefined = undefined; + closeResultsVisibility: string = "none"; + + removeFilters = (e: React.PointerEvent): void => { + const keyOptions: string[] = []; + if (this.docSafe.length === 0 && this.props.dataDoc) { + this.docSafe = DocListCast(this.props.dataDoc[this.props.fieldKey]); + } + const docs = this.docSafe; + docs.forEach((doc) => { + const key = StrCast(doc[this._key]); + if (keyOptions.includes(key) === false) { + keyOptions.push(key); + } + }); + + Doc.setDocFilter(this.props.Document, this._key, "", "remove"); + this.props.col.setColor("rgb(241, 239, 235)"); + this.closeResultsVisibility = "none"; + } render() { return ( - <div style={{ display: "flex" }}> + <div style={{ display: "flex" }} ref={this.setNode}> <FontAwesomeIcon onClick={e => { this.props.openHeader(this.props.col, e.clientX, e.clientY); e.stopPropagation(); }} icon={this.props.icon} size="lg" style={{ display: "inline", paddingBottom: "1px", paddingTop: "4px", cursor: "hand" }} /> {/* <FontAwesomeIcon icon={fa.faSearchMinus} size="lg" style={{ display: "inline", paddingBottom: "1px", paddingTop: "4px", cursor: "hand" }} onClick={e => { runInAction(() => { this._isOpen === undefined ? this._isOpen = true : this._isOpen = !this._isOpen }) }} /> */} - <div className="keys-dropdown" style={{ zIndex: 10, width: this.props.width, maxWidth: this.props.width }}> + <div className="keys-dropdown" style={{ zIndex: 1, width: this.props.width, maxWidth: this.props.width }}> <input className="keys-search" style={{ width: "100%" }} ref={this._inputRef} type="text" value={this._searchTerm} placeholder="Column key" onKeyDown={this.onKeyDown} onChange={e => this.onChange(e.target.value)} - onClick={(e) => { - //this._inputRef.current!.select(); - e.stopPropagation(); - }} onFocus={this.onFocus} onBlur={this.onBlur}></input> + onClick={(e) => { e.stopPropagation(); this._inputRef.current?.focus(); }} + onFocus={this.onFocus} ></input> + <div style={{ display: this.closeResultsVisibility }}> + <FontAwesomeIcon onPointerDown={this.removeFilters} icon={"times-circle"} size="lg" + style={{ cursor: "hand", color: "grey", padding: 2, left: -20, top: -1, height: 15, position: "relative" }} /> + </div> {!this._isOpen ? (null) : <div className="keys-options-wrapper" style={{ width: this.props.width, maxWidth: this.props.width, height: "auto", - }} - onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerOut}> + }}> {this._searchTerm.includes(":") ? this.renderFilterOptions() : this.renderOptions()} </div>} </div > diff --git a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx index 37e6c115d..881246bd4 100644 --- a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx +++ b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx @@ -1,21 +1,17 @@ import React = require("react"); -import { ReactTableDefaults, TableCellRenderer, RowInfo } from "react-table"; -import "./CollectionSchemaView.scss"; -import { Transform } from "../../util/Transform"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { action } from "mobx"; +import { ReactTableDefaults, RowInfo, TableCellRenderer } from "react-table"; import { Doc } from "../../../fields/Doc"; -import { DragManager, SetupDrag, dropActionType } from "../../util/DragManager"; +import { SchemaHeaderField } from "../../../fields/SchemaHeaderField"; import { Cast, FieldValue, StrCast } from "../../../fields/Types"; -import { ContextMenu } from "../ContextMenu"; -import { action } from "mobx"; -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faGripVertical, faTrash } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { DocumentManager } from "../../util/DocumentManager"; -import { SchemaHeaderField } from "../../../fields/SchemaHeaderField"; -import { undoBatch } from "../../util/UndoManager"; +import { DragManager, dropActionType, SetupDrag } from "../../util/DragManager"; import { SnappingManager } from "../../util/SnappingManager"; - -library.add(faGripVertical, faTrash); +import { Transform } from "../../util/Transform"; +import { undoBatch } from "../../util/UndoManager"; +import { ContextMenu } from "../ContextMenu"; +import "./CollectionSchemaView.scss"; export interface MovableColumnProps { columnRenderer: TableCellRenderer; @@ -205,6 +201,7 @@ export class MovableRow extends React.Component<MovableRowProps> { } onRowContextMenu = (e: React.MouseEvent): void => { + e.preventDefault(); const description = this.props.rowWrapped ? "Unwrap text on row" : "Text wrap row"; ContextMenu.Instance.addItem({ description: description, event: () => this.props.textWrapRow(this.props.rowInfo.original), icon: "file-pdf" }); } @@ -226,13 +223,15 @@ export class MovableRow extends React.Component<MovableRowProps> { render() { const { children = null, rowInfo } = this.props; + if (!rowInfo) { return <ReactTableDefaults.TrComponent>{children}</ReactTableDefaults.TrComponent>; } const { original } = rowInfo; const doc = FieldValue(Cast(original, Doc)); - if (!doc) return <></>; + + if (!doc) return (null); const reference = React.createRef<HTMLDivElement>(); const onItemDown = SetupDrag(reference, () => doc, this.move, StrCast(this.props.dropAction) as dropActionType); @@ -245,11 +244,11 @@ export class MovableRow extends React.Component<MovableRowProps> { <div className={className} onKeyPress={this.onKeyDown} ref={this.createRowDropTarget} onContextMenu={this.onRowContextMenu}> <div className="collectionSchema-row-wrapper" onKeyPress={this.onKeyDown} ref={this._header} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}> <ReactTableDefaults.TrComponent onKeyPress={this.onKeyDown} > - {/* <div className="row-dragger"> - <div className="row-option" onClick={undoBatch(() => this.props.removeDoc(this.props.rowInfo.original))}><FontAwesomeIcon icon="trash" size="sm" /></div> - <div className="row-option" style={{ cursor: "grab" }} ref={reference} onPointerDown={onItemDown}><FontAwesomeIcon icon="grip-vertical" size="sm" /></div> - <div className="row-option" onClick={() => this.props.addDocTab(this.props.rowInfo.original, "onRight")}><FontAwesomeIcon icon="external-link-alt" size="sm" /></div> - </div> */} + <div className="row-dragger"> + <div className="row-option" style={{ left: 5 }} onClick={undoBatch(() => this.props.removeDoc(this.props.rowInfo.original))}><FontAwesomeIcon icon="trash" size="sm" /></div> + <div className="row-option" style={{ cursor: "grab", left: 25 }} ref={reference} onPointerDown={onItemDown}><FontAwesomeIcon icon="grip-vertical" size="sm" /></div> + <div className="row-option" style={{ left: 40 }} onClick={() => this.props.addDocTab(this.props.rowInfo.original, "add:right")}><FontAwesomeIcon icon="external-link-alt" size="sm" /></div> + </div> {children} </ReactTableDefaults.TrComponent> </div> diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss index c1918aed0..8d2f645d9 100644 --- a/src/client/views/collections/CollectionSchemaView.scss +++ b/src/client/views/collections/CollectionSchemaView.scss @@ -59,6 +59,7 @@ justify-content: space-between; flex-wrap: nowrap; touch-action: none; + padding: 2px; div { touch-action: none; @@ -99,6 +100,9 @@ direction: ltr; overflow: visible; } + .rt-noData { + display: none; + } .rt-thead { width: 100%; @@ -139,6 +143,10 @@ width: 100%; direction: rtl; overflow: visible; + + .rt-td { + border-right: 1px solid rgba(0, 0, 0, 0.2); + } } .rt-tr-group { @@ -158,6 +166,8 @@ font-size: 13px; text-align: center; white-space: nowrap; + display: flex; + align-items: center; .imageBox-cont { position: relative; @@ -176,6 +186,11 @@ height: 100%; } } + .rt-td.rt-expandable { + display: flex; + align-items: center; + height: inherit; + } .rt-resizer { width: 8px; @@ -359,47 +374,6 @@ button.add-column { } } -.altcollectionTimeView-treeView { - display: flex; - flex-direction: column; - width: 175px; - height: auto; - position: fixed; - border-left: solid 1px; - z-index: 1; - - .collectionTimeView-addfacet { - display: inline-block; - width: 200px; - height: 30px; - background: darkGray; - text-align: left; - - .collectionTimeView-button { - align-items: center; - display: flex; - width: 100%; - height: 100%; - - .collectionTimeView-span { - margin: auto; - } - } - - >div, - >div>div { - width: 100%; - height: 100%; - } - } - - .altcollectionTimeView-tree { - display: inline-block; - width: 100%; - height: calc(100% - 30px); - } -} - .collectionSchema-row { height: 100%; background-color: white; @@ -417,11 +391,12 @@ button.add-column { .row-dragger { display: flex; justify-content: space-around; - flex: 50 0 auto; - width: 50px; + //flex: 50 0 auto; + width: 0; max-width: 50px; - height: 100%; + //height: 100%; min-height: 30px; + align-items: center; color: lightgray; background-color: white; transition: color 0.1s ease; @@ -429,10 +404,12 @@ button.add-column { .row-option { // padding: 5px; cursor: pointer; + position: absolute; transition: color 0.1s ease; display: flex; flex-direction: column; justify-content: center; + z-index: 2; &:hover { color: gray; @@ -462,7 +439,7 @@ button.add-column { .collectionSchemaView-cellContainer { width: 100%; - height: 100%; + height: unset; } .collectionSchemaView-cellWrapper { @@ -599,10 +576,17 @@ button.add-column { .collectionSchemaView-table { width: 100%; height: 100%; + overflow: auto; + padding: 3px; } +.rt-td.rt-expandable { + overflow: visible; + position: relative; + height:100%; + z-index: 1; +} .reactTable-sub { - padding: 10px 30px; background-color: rgb(252, 252, 252); width: 100%; @@ -610,12 +594,6 @@ button.add-column { display: none; } - .collectionSchemaView-table { - border: solid 1px; - overflow: hidden; - } - - .row-dragger { background-color: rgb(252, 252, 252); } @@ -625,20 +603,26 @@ button.add-column { } .collectionSchemaView-table { - width: 100%; + width: 100%; + border: solid 1px; + overflow: visible; + padding: 0px; } } .collectionSchemaView-expander { height: 100%; min-height: 30px; - position: relative; + position: absolute; color: gray; + width: 20; + height: auto; + left: 55; svg { position: absolute; top: 50%; - left: 50%; + left: 10; transform: translate(-50%, -50%); } } diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index a72b349ec..27575374a 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -1,32 +1,29 @@ import React = require("react"); -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faCog, faPlus, faSortDown, faSortUp, faTable } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, observable, untracked } from "mobx"; import { observer } from "mobx-react"; import Measure from "react-measure"; import { Resize } from "react-table"; import "react-table/react-table.css"; -import { Doc } from "../../../fields/Doc"; +import { Doc, Opt } from "../../../fields/Doc"; import { List } from "../../../fields/List"; import { listSpec } from "../../../fields/Schema"; import { PastelSchemaPalette, SchemaHeaderField } from "../../../fields/SchemaHeaderField"; -import { Cast, NumCast, BoolCast } from "../../../fields/Types"; +import { Cast, NumCast } from "../../../fields/Types"; import { TraceMobx } from "../../../fields/util"; import { emptyFunction, returnFalse, returnOne, returnZero, setupMoveUpEvents } from "../../../Utils"; +import { SelectionManager } from "../../util/SelectionManager"; import { SnappingManager } from "../../util/SnappingManager"; import { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; import { COLLECTION_BORDER_WIDTH } from '../../views/globalCssVariables.scss'; +import { ContextMenu } from "../ContextMenu"; +import { ContextMenuProps } from "../ContextMenuItem"; import '../DocumentDecorations.scss'; import { ContentFittingDocumentView } from "../nodes/ContentFittingDocumentView"; -import { KeysDropdown } from "./CollectionSchemaHeaders"; import "./CollectionSchemaView.scss"; import { CollectionSubView } from "./CollectionSubView"; import { SchemaTable } from "./SchemaTable"; - -library.add(faCog, faPlus, faSortUp, faSortDown); -library.add(faTable); // bcz: need to add drag and drop of rows and columns. This seems like it might work for rows: https://codesandbox.io/s/l94mn1q657 export enum ColumnType { @@ -44,7 +41,7 @@ const columnTypes: Map<string, ColumnType> = new Map([ ["title", ColumnType.String], ["x", ColumnType.Number], ["y", ColumnType.Number], ["_width", ColumnType.Number], ["_height", ColumnType.Number], ["_nativeWidth", ColumnType.Number], ["_nativeHeight", ColumnType.Number], ["isPrototype", ColumnType.Boolean], - ["page", ColumnType.Number], ["curPage", ColumnType.Number], ["currentTimecode", ColumnType.Number], ["zIndex", ColumnType.Number] + ["_curPage", ColumnType.Number], ["_currentTimecode", ColumnType.Number], ["zIndex", ColumnType.Number] ]); @observer @@ -71,7 +68,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @computed get menuCoordinates() { let searchx = 0; let searchy = 0; - if (this.props.Document._searchDoc !== undefined) { + if (this.props.Document._searchDoc) { const el = document.getElementsByClassName("collectionSchemaView-searchContainer")[0]; if (el !== undefined) { const rect = el.getBoundingClientRect(); @@ -303,17 +300,9 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { this.columns = columns; if (filter) { Doc.setDocFilter(this.props.Document, newKey, filter, "match"); - if (this.props.Document.selectedDoc !== undefined) { - const doc = Cast(this.props.Document.selectedDoc, Doc) as Doc; - Doc.setDocFilter(doc, newKey, filter, "match"); - } } else { this.props.Document._docFilters = undefined; - if (this.props.Document.selectedDoc !== undefined) { - const doc = Cast(this.props.Document.selectedDoc, Doc) as Doc; - doc._docFilters = undefined; - } } } } @@ -388,7 +377,10 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @action setFocused = (doc: Doc) => this._focusedTable = doc; - @action setPreviewDoc = (doc: Doc) => this.previewDoc = doc; + @action setPreviewDoc = (doc: Opt<Doc>) => { + SelectionManager.SelectSchemaDoc(this, doc); + this.previewDoc = doc; + } //toggles preview side-panel of schema @action @@ -437,8 +429,6 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { <ContentFittingDocumentView Document={this.previewDocument} DataDoc={undefined} - NativeHeight={returnZero} - NativeWidth={returnZero} fitToBox={true} FreezeDimensions={true} focus={emptyFunction} @@ -449,6 +439,8 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { PanelHeight={this.previewHeight} ScreenToLocalTransform={this.getPreviewTransform} docFilters={this.docFilters} + docRangeFilters={this.docRangeFilters} + searchFilterDocs={this.searchFilterDocs} ContainingCollectionDoc={this.props.CollectionView?.props.Document} ContainingCollectionView={this.props.CollectionView} moveDocument={this.props.moveDocument} @@ -493,8 +485,8 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { documentKeys={this.documentKeys} headerIsEditing={this._headerIsEditing} openHeader={this.openHeader} - onClick={e => { e.stopPropagation(); this.closeHeader(); }} - onPointerDown={this.onTablePointerDown} + onClick={this.onTableClick} + onPointerDown={emptyFunction} onResizedChange={this.onResizedChange} setColumns={this.setColumns} reorderColumns={this.reorderColumns} @@ -515,14 +507,28 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { </div> </div>; } + onSpecificMenu = (e: React.MouseEvent) => { + if ((e.target as any)?.className?.includes?.("collectionSchemaView-cell") || (e.target instanceof HTMLSpanElement)) { + const cm = ContextMenu.Instance; + const options = cm.findByDescription("Options..."); + const optionItems: ContextMenuProps[] = options && "subitems" in options ? options.subitems : []; + optionItems.push({ description: "remove", event: () => this.previewDoc && this.props.removeDocument(this.previewDoc), icon: "trash" }); + !options && cm.addItem({ description: "Options...", subitems: optionItems, icon: "compass" }); + cm.displayMenu(e.clientX, e.clientY); + (e.nativeEvent as any).SchemaHandled = true; // not sure why this is needed, but if you right-click quickly on a cell, the Document/Collection contextMenu handlers still fire without this. + e.stopPropagation(); + } + } @action - onTablePointerDown = (e: React.PointerEvent): void => { - this.setFocused(this.props.Document); - if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey && this.props.isSelected(true)) { + onTableClick = (e: React.MouseEvent): void => { + if (!(e.target as any)?.className?.includes?.("collectionSchemaView-cell") && !(e.target instanceof HTMLSpanElement)) { + this.setPreviewDoc(undefined); + } else { e.stopPropagation(); } - // this.closeHeader(); + this.setFocused(this.props.Document); + this.closeHeader(); } onResizedChange = (newResized: Resize[], event: any) => { @@ -572,7 +578,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { } render() { let name = "collectionSchemaView-container"; - if (this.props.Document._searchDoc !== undefined) { + if (this.props.Document._searchDoc) { name = "collectionSchemaView-searchContainer"; } if (!this.props.active()) setTimeout(() => this.closeHeader(), 0); @@ -594,13 +600,14 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { </div>; return <div className={name} style={{ - overflow: this.props.overflow === true ? "scroll" : undefined, - pointerEvents: !this.props.active() && !SnappingManager.GetIsDragging() ? "none" : undefined, + overflow: this.props.overflow === true ? "scroll" : undefined, backgroundColor: "white", + pointerEvents: this.props.Document._searchDoc !== undefined && !this.props.active() && !SnappingManager.GetIsDragging() ? "none" : undefined, width: name === "collectionSchemaView-searchContainer" ? "auto" : this.props.PanelWidth() || "100%", height: this.props.PanelHeight() || "100%", position: "relative", }} > <div className="collectionSchemaView-tableContainer" - style={{ backgroundColor: "white", width: `calc(100% - ${this.previewWidth()}px)` }} + style={{ width: `calc(100% - ${this.previewWidth()}px)` }} onKeyPress={this.onKeyPress} + onContextMenu={this.onSpecificMenu} onPointerDown={this.onPointerDown} onWheel={e => this.props.active(true) && e.stopPropagation()} onDrop={e => this.onExternalDrop(e, {})} diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss index 8fc74a9c6..9f56a0c0e 100644 --- a/src/client/views/collections/CollectionStackingView.scss +++ b/src/client/views/collections/CollectionStackingView.scss @@ -205,7 +205,7 @@ display: flex; align-items: center; justify-content: center; - color: lightGray; + color: black; .editableView-container-editing-oneLine, .editableView-container-editing { diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 241c64f9a..daaee67dc 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -12,7 +12,7 @@ import { listSpec, makeInterface } from "../../../fields/Schema"; import { SchemaHeaderField } from "../../../fields/SchemaHeaderField"; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from "../../../fields/Types"; import { TraceMobx } from "../../../fields/util"; -import { emptyFunction, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils, smoothScroll } from "../../../Utils"; +import { emptyFunction, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils, smoothScroll, returnVal } from "../../../Utils"; import { DragManager, dropActionType } from "../../util/DragManager"; import { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; @@ -33,25 +33,31 @@ const _global = (window /* browser */ || global /* node */) as any; type StackingDocument = makeInterface<[typeof collectionSchema, typeof documentSchema]>; const StackingDocument = makeInterface(collectionSchema, documentSchema); +export type collectionStackingViewProps = { + chromeStatus?: string; +}; + @observer -export class CollectionStackingView extends CollectionSubView(StackingDocument) { +export class CollectionStackingView extends CollectionSubView<StackingDocument, Partial<collectionStackingViewProps>>(StackingDocument) { _masonryGridRef: HTMLDivElement | null = null; _draggerRef = React.createRef<HTMLDivElement>(); _pivotFieldDisposer?: IReactionDisposer; + _autoHeightDisposer?: IReactionDisposer; _docXfs: any[] = []; _columnStart: number = 0; @observable _heightMap = new Map<string, number>(); @observable _cursor: CursorProperty = "grab"; @observable _scroll = 0; // used to force the document decoration to update when scrolling + @computed get chromeStatus() { return this.props.chromeStatus || StrCast(this.layoutDoc._chromeStatus); } @computed get columnHeaders() { return Cast(this.layoutDoc._columnHeaders, listSpec(SchemaHeaderField)); } @computed get pivotField() { return StrCast(this.layoutDoc._pivotField); } @computed get filteredChildren() { return this.childLayoutPairs.filter(pair => pair.layout instanceof Doc && !pair.layout.hidden).map(pair => pair.layout); } @computed get xMargin() { return NumCast(this.layoutDoc._xMargin, 2 * Math.min(this.gridGap, .05 * this.props.PanelWidth())); } - @computed get yMargin() { return Math.max(this.layoutDoc._showTitle && !this.layoutDoc._showTitleHover ? 30 : 0, NumCast(this.layoutDoc._yMargin, 5)); } // 2 * this.gridGap)); } + @computed get yMargin() { return Math.max(this.layoutDoc._showTitle && !this.layoutDoc._showTitleHover ? 30 : 0, this.props.yMargin || NumCast(this.layoutDoc._yMargin, 5)); } // 2 * this.gridGap)); } @computed get gridGap() { return NumCast(this.layoutDoc._gridGap, 10); } @computed get isStackingView() { return BoolCast(this.layoutDoc._columnsStack, true); } @computed get numGroupColumns() { return this.isStackingView ? Math.max(1, this.Sections.size + (this.showAddAGroup ? 1 : 0)) : 1; } - @computed get showAddAGroup() { return (this.pivotField && (this.layoutDoc._chromeStatus !== 'view-mode' && this.layoutDoc._chromeStatus !== 'disabled')); } + @computed get showAddAGroup() { return (this.pivotField && (this.chromeStatus !== 'view-mode' && this.chromeStatus !== 'disabled')); } @computed get columnWidth() { return Math.min(this.props.PanelWidth() / this.props.ContentScaling() - 2 * this.xMargin, this.isStackingView ? Number.MAX_VALUE : this.layoutDoc._columnWidth === -1 ? this.props.PanelWidth() - 2 * this.xMargin : NumCast(this.layoutDoc._columnWidth, 250)); @@ -78,7 +84,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) const rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap); const style = this.isStackingView ? { width: width(), marginTop: i ? this.gridGap : 0, height: height() } : { gridRowEnd: `span ${rowSpan}` }; return <div className={`collectionStackingView-${this.isStackingView ? "columnDoc" : "masonryDoc"}`} key={d[Id]} ref={dref} style={style} > - {this.getDisplayDoc(d, (!d.isTemplateDoc && !d.isTemplateForField && !d.PARAMS) ? undefined : this.props.DataDoc, dxf, width)} + {this.getDisplayDoc(d, dxf, width)} </div>; }); } @@ -127,19 +133,6 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) return fields; } - getSimpleDocHeight(d?: Doc) { - if (!d) return 0; - const layoutDoc = Doc.Layout(d, this.props.ChildLayoutTemplate?.()); - const nw = NumCast(layoutDoc._nativeWidth); - const nh = NumCast(layoutDoc._nativeHeight); - let wid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1); - if (!layoutDoc._fitWidth && nw && nh) { - const aspect = nw && nh ? nh / nw : 1; - if (!(this.layoutDoc._columnsFill)) wid = Math.min(layoutDoc[WidthSym](), wid); - return wid * aspect; - } - return layoutDoc._fitWidth ? wid * NumCast(layoutDoc.scrollHeight, nh) / (nw || 1) : layoutDoc[HeightSym](); - } componentDidMount() { super.componentDidMount?.(); @@ -148,10 +141,12 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) () => this.pivotField, () => this.layoutDoc._columnHeaders = new List() ); + this._autoHeightDisposer = reaction(() => this.layoutDoc._autoHeight, this.forceAutoHeight); } componentWillUnmount() { super.componentWillUnmount(); this._pivotFieldDisposer?.(); + this._autoHeightDisposer?.(); } @action @@ -186,17 +181,16 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) const localTop = this.props.ScreenToLocalTransform().transformPoint(0, top); smoothScroll(doc.presTransition || doc.presTransition === 0 ? NumCast(doc.presTransition) : 500, this._mainCont!, localTop[1] + this._mainCont!.scrollTop); } - afterFocus && setTimeout(() => { - if (afterFocus?.()) { } - }, 500); + afterFocus && setTimeout(afterFocus, 500); } - getDisplayDoc(doc: Doc, dataDoc: Doc | undefined, dxf: () => Transform, width: () => number) { + getDisplayDoc(doc: Doc, dxf: () => Transform, width: () => number) { + const dataDoc = (!doc.isTemplateDoc && !doc.isTemplateForField && !doc.PARAMS) ? undefined : this.props.DataDoc; const height = () => this.getDocHeight(doc); - const opacity = () => this.Document.currentFrame === undefined ? this.props.childOpacity?.() : CollectionFreeFormDocumentView.getValues(doc, NumCast(this.Document.currentFrame))?.opacity; + const opacity = () => this.Document._currentFrame === undefined ? this.props.childOpacity?.() : CollectionFreeFormDocumentView.getValues(doc, NumCast(this.Document._currentFrame))?.opacity; return <ContentFittingDocumentView Document={doc} - DataDoc={dataDoc || (doc[DataSym] !== doc && doc[DataSym])} + DataDoc={dataDoc || (!Doc.AreProtosEqual(doc[DataSym], doc) && doc[DataSym])} backgroundColor={this.props.backgroundColor} LayoutTemplate={this.props.ChildLayoutTemplate} LayoutTemplateString={this.props.ChildLayoutString} @@ -205,10 +199,11 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) renderDepth={this.props.renderDepth + 1} PanelWidth={width} PanelHeight={height} - NativeHeight={returnZero} - NativeWidth={returnZero} + NativeWidth={this.props.childIgnoreNativeSize ? returnZero : undefined} // explicitly ignore nativeWidth/height if childIgnoreNativeSize is set- used by PresBox + NativeHeight={this.props.childIgnoreNativeSize ? returnZero : undefined} + dontCenter={this.props.childIgnoreNativeSize ? true : false} fitToBox={false} - dontRegisterView={BoolCast(this.layoutDoc.dontRegisterChildViews, this.props.dontRegisterView)} + dontRegisterView={dataDoc ? true : BoolCast(this.layoutDoc.dontRegisterChildViews, this.props.dontRegisterView)} rootSelected={this.rootSelected} dropAction={StrCast(this.layoutDoc.childDropAction) as dropActionType} onClick={this.onChildClickHandler} @@ -217,11 +212,14 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) opacity={opacity} focus={this.focusDocument} docFilters={this.docFilters} + docRangeFilters={this.docRangeFilters} + searchFilterDocs={this.searchFilterDocs} ContainingCollectionDoc={this.props.CollectionView?.props.Document} ContainingCollectionView={this.props.CollectionView} addDocument={this.props.addDocument} moveDocument={this.props.moveDocument} removeDocument={this.props.removeDocument} + contentsPointerEvents={StrCast(this.layoutDoc.contentPointerEvents)} parentActive={this.props.active} whenActiveChanged={this.props.whenActiveChanged} addDocTab={this.addDocTab} @@ -240,17 +238,22 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) } getDocHeight(d?: Doc) { if (!d) return 0; + const dataDoc = (!d.isTemplateDoc && !d.isTemplateForField && !d.PARAMS) ? undefined : this.props.DataDoc; const layoutDoc = Doc.Layout(d, this.props.ChildLayoutTemplate?.()); - const nw = NumCast(layoutDoc._nativeWidth); - const nh = NumCast(layoutDoc._nativeHeight); + const layoutField = Doc.LayoutFieldKey(layoutDoc); + const nw = NumCast(layoutDoc._nativeWidth) || NumCast(dataDoc?.[`${layoutField}-nativeWidth`]); + const nh = NumCast(layoutDoc._nativeHeight) || NumCast(dataDoc?.[`${layoutField}-nativeHeight`]); let wid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1); + const hllimit = NumCast(this.layoutDoc.childLimitHeight, -1); if (!layoutDoc._fitWidth && nw && nh) { const aspect = nw && nh ? nh / nw : 1; - if (!(this.layoutDoc._columnsFill)) wid = Math.min(layoutDoc[WidthSym](), wid); - return wid * aspect; + if (!(this.layoutDoc._columnsFill)) wid = Math.min(this.getDocWidth(d), wid); + return Math.min(hllimit === 0 ? this.props.PanelWidth() : hllimit === -1 ? 10000 : hllimit, wid * aspect); } - return layoutDoc._fitWidth ? !nh ? this.props.PanelHeight() - 2 * this.yMargin : - Math.min(wid * NumCast(layoutDoc.scrollHeight, nh) / (nw || 1), this.props.PanelHeight() - 2 * this.yMargin) : Math.max(20, layoutDoc[HeightSym]()); + return layoutDoc._fitWidth ? + (!nh ? this.props.PanelHeight() - 2 * this.yMargin : + Math.min(wid * nh / (nw || 1), this.layoutDoc._autoHeight ? 100000 : this.props.PanelHeight() - 2 * this.yMargin)) : + Math.min(hllimit === 0 ? this.props.PanelWidth() : hllimit === -1 ? 10000 : hllimit, Math.max(20, layoutDoc[HeightSym]())); } columnDividerDown = (e: React.PointerEvent) => { @@ -340,9 +343,11 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) sectionStacking = (heading: SchemaHeaderField | undefined, docList: Doc[]) => { const key = this.pivotField; let type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined = undefined; - const types = docList.length ? docList.map(d => typeof d[key]) : this.filteredChildren.map(d => typeof d[key]); - if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) { - type = types[0]; + if (this.pivotField) { + const types = docList.length ? docList.map(d => typeof d[key]) : this.filteredChildren.map(d => typeof d[key]); + if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) { + type = types[0]; + } } const cols = () => this.isStackingView ? 1 : Math.max(1, Math.min(this.filteredChildren.length, Math.floor((this.props.PanelWidth() - 2 * this.xMargin) / (this.columnWidth + this.gridGap)))); @@ -354,16 +359,17 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) const doc = this.props.DataDoc && this.props.DataDoc.layout === this.layoutDoc ? this.props.DataDoc : this.layoutDoc; this.observer = new _global.ResizeObserver(action((entries: any) => { if (this.layoutDoc._autoHeight && ref && this.refList.length && !SnappingManager.GetIsDragging()) { - Doc.Layout(doc)._height = Math.min(1200, Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace("px", ""))))); + const height = Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace("px", ""))))); + Doc.Layout(doc)._height = Math.max(height, NumCast(doc[this.props.fieldKey + "-height"])); } })); this.observer.observe(ref); } }} - key={heading ? heading.heading : ""} + key={heading?.heading ?? ""} cols={cols} headings={this.headings} - heading={heading ? heading.heading : ""} + heading={heading?.heading ?? ""} headingObject={heading} docList={docList} parent={this} @@ -383,6 +389,11 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) return this.props.ScreenToLocalTransform().translate(offset[0], offset[1] + offsety); } + forceAutoHeight = () => { + const doc = this.props.DataDoc && this.props.DataDoc.layout === this.layoutDoc ? this.props.DataDoc : this.layoutDoc; + Doc.Layout(doc)._height = this.refList.reduce((p, r) => p + Number(getComputedStyle(r).height.replace("px", "")), 0); + } + sectionMasonry = (heading: SchemaHeaderField | undefined, docList: Doc[], first: boolean) => { const key = this.pivotField; let type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined = undefined; @@ -401,7 +412,8 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) const doc = this.props.DataDoc && this.props.DataDoc.layout === this.layoutDoc ? this.props.DataDoc : this.layoutDoc; this.observer = new _global.ResizeObserver(action((entries: any) => { if (this.layoutDoc._autoHeight && ref && this.refList.length && !SnappingManager.GetIsDragging()) { - Doc.Layout(doc)._height = this.refList.reduce((p, r) => p + Number(getComputedStyle(r).height.replace("px", "")), 0); + const height = this.refList.reduce((p, r) => p + Number(getComputedStyle(r).height.replace("px", "")), 0); + Doc.Layout(doc)._height = Math.max(height, NumCast(doc[this.props.fieldKey + "-height"])); } })); this.observer.observe(ref); @@ -439,16 +451,13 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) return firstEntry[0].heading > secondEntry[0].heading ? 1 : -1; } - onToggle = (checked: Boolean) => { - this.layoutDoc._chromeStatus = checked ? "collapsed" : "view-mode"; - } - onContextMenu = (e: React.MouseEvent): void => { // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout if (!e.isPropagationStopped()) { const subItems: ContextMenuProps[] = []; subItems.push({ description: `${this.layoutDoc._columnsFill ? "Variable Size" : "Autosize"} Column`, event: () => this.layoutDoc._columnsFill = !this.layoutDoc._columnsFill, icon: "plus" }); subItems.push({ description: `${this.layoutDoc._autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight, icon: "plus" }); + subItems.push({ description: "Clear All", event: () => this.dataDoc.data = new List([]), icon: "times" }); ContextMenu.Instance.addItem({ description: "Options...", subitems: subItems, icon: "eye" }); } } @@ -464,8 +473,8 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) } - @computed get nativeWidth() { return NumCast(this.layoutDoc._nativeWidth) || this.props.NativeWidth() || 0; } - @computed get nativeHeight() { return NumCast(this.layoutDoc._nativeHeight) || this.props.NativeHeight() || 0; } + @computed get nativeWidth() { return returnVal(this.props.NativeWidth?.(), NumCast(this.layoutDoc._nativeWidth)); } + @computed get nativeHeight() { return returnVal(this.props.NativeHeight?.(), NumCast(this.layoutDoc._nativeHeight)); } @computed get scaling() { return !this.nativeWidth ? 1 : this.props.PanelHeight() / this.nativeHeight; } @@ -488,10 +497,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) width: `${1 / this.scaling * 100}%`, transformOrigin: "top left", }} - onScroll={action(e => { - if (!this.props.isSelected() && this.props.renderDepth) e.currentTarget.scrollTop = this._scroll; - else this._scroll = e.currentTarget.scrollTop; - })} + onScroll={action(e => this._scroll = e.currentTarget.scrollTop)} onDrop={this.onExternalDrop.bind(this)} onContextMenu={this.onContextMenu} onWheel={e => this.props.active(true) && e.stopPropagation()} > @@ -501,13 +507,14 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) style={{ width: !this.isStackingView ? "100%" : this.columnWidth / this.numGroupColumns - 10, marginTop: 10 }}> <EditableView {...editableViewProps} /> </div>} - {this.layoutDoc._chromeStatus !== 'disabled' && this.props.isSelected() ? <Switch - onChange={this.onToggle} - onClick={this.onToggle} - defaultChecked={this.layoutDoc._chromeStatus !== 'view-mode'} - checkedChildren="edit" - unCheckedChildren="view" - /> : null} + {/* {this.chromeStatus === 'disabled' || !this.props.isSelected() ? (null) : + <Switch + onChange={this.onToggle} + onClick={this.onToggle} + defaultChecked={this.chromeStatus !== 'view-mode'} + checkedChildren="edit" + unCheckedChildren="view" + />} */} </div> </div> ); } diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index ede75fba8..1bc989e83 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -1,35 +1,33 @@ import React = require("react"); -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faPalette } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, observable, runInAction, computed } from "mobx"; +import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; import { Doc, DocListCast } from "../../../fields/Doc"; import { RichTextField } from "../../../fields/RichTextField"; +import { listSpec } from "../../../fields/Schema"; import { PastelSchemaPalette, SchemaHeaderField } from "../../../fields/SchemaHeaderField"; import { ScriptField } from "../../../fields/ScriptField"; -import { NumCast, StrCast, Cast } from "../../../fields/Types"; +import { Cast, NumCast, StrCast } from "../../../fields/Types"; import { ImageField } from "../../../fields/URLField"; import { TraceMobx } from "../../../fields/util"; +import { emptyFunction, setupMoveUpEvents } from "../../../Utils"; import { Docs, DocUtils } from "../../documents/Documents"; import { DocumentType } from "../../documents/DocumentTypes"; import { DragManager } from "../../util/DragManager"; +import { SnappingManager } from "../../util/SnappingManager"; import { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; import { EditableView } from "../EditableView"; import { CollectionStackingView } from "./CollectionStackingView"; -import { setupMoveUpEvents, emptyFunction } from "../../../Utils"; import "./CollectionStackingView.scss"; -import { listSpec } from "../../../fields/Schema"; -import { SnappingManager } from "../../util/SnappingManager"; +import { FormattedTextBox } from "../nodes/formattedText/FormattedTextBox"; +import { Id } from "../../../fields/FieldSymbols"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; -library.add(faPalette); - interface CSVFieldColumnProps { cols: () => number; headings: () => object[]; @@ -141,8 +139,9 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC const maxHeading = this.props.docList.reduce((maxHeading, doc) => NumCast(doc.heading) > maxHeading ? NumCast(doc.heading) : maxHeading, 0); const heading = maxHeading === 0 || this.props.docList.length === 0 ? 1 : maxHeading === 1 ? 2 : 3; newDoc.heading = heading; - this.props.parent.props.addDocument(newDoc); - return false; + FormattedTextBox.SelectOnLoad = newDoc[Id]; + FormattedTextBox.SelectOnLoadChar = " "; + return this.props.parent.props.addDocument(newDoc); } @action @@ -232,7 +231,10 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC const docItems: ContextMenuProps[] = []; const dataDoc = this.props.parent.props.DataDoc || this.props.parent.Document; - DocUtils.addDocumentCreatorMenuItems(this.props.parent.props.addDocument, this.props.parent.props.addDocument, x, y); + DocUtils.addDocumentCreatorMenuItems((doc) => { + FormattedTextBox.SelectOnLoad = doc[Id]; + return this.props.parent.props.addDocument(doc); + }, this.props.parent.props.addDocument, x, y, true); Array.from(Object.keys(Doc.GetProto(dataDoc))).filter(fieldKey => dataDoc[fieldKey] instanceof RichTextField || dataDoc[fieldKey] instanceof ImageField || typeof (dataDoc[fieldKey]) === "string").map(fieldKey => docItems.push({ @@ -260,13 +262,8 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC } }, icon: "compress-arrows-alt" })); - layoutItems.push({ description: ":freeform", event: () => this.props.parent.props.addDocument(Docs.Create.FreeformDocument([], { _width: 200, _height: 200 })), icon: "compress-arrows-alt" }); - layoutItems.push({ description: ":carousel", event: () => this.props.parent.props.addDocument(Docs.Create.CarouselDocument([], { _width: 400, _height: 200 })), icon: "compress-arrows-alt" }); - layoutItems.push({ description: ":columns", event: () => this.props.parent.props.addDocument(Docs.Create.MulticolumnDocument([], { _width: 200, _height: 200 })), icon: "compress-arrows-alt" }); - layoutItems.push({ description: ":image", event: () => this.props.parent.props.addDocument(Docs.Create.ImageDocument("http://www.cs.brown.edu/~bcz/face.gif", { _width: 200, _height: 200 })), icon: "compress-arrows-alt" }); - - ContextMenu.Instance.addItem({ description: "Doc Fields ...", subitems: docItems, icon: "eye" }); - ContextMenu.Instance.addItem({ description: "Containers ...", subitems: layoutItems, icon: "eye" }); + !Doc.UserDoc().noviceMode && ContextMenu.Instance.addItem({ description: "Doc Fields ...", subitems: docItems, icon: "eye" }); + !Doc.UserDoc().noviceMode && ContextMenu.Instance.addItem({ description: "Containers ...", subitems: layoutItems, icon: "eye" }); ContextMenu.Instance.setDefaultItem("::", (name: string): void => { Doc.GetProto(this.props.parent.props.Document)[name] = ""; const created = Docs.Create.TextDocument("", { title: name, _width: 250, _autoHeight: true }); @@ -289,9 +286,9 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC const heading = this._heading; const style = this.props.parent; const singleColumn = style.isStackingView; - const columnYMargin = this.props.headingObject ? 0 : NumCast(this.props.parent.props.Document._yMargin, 5); + const columnYMargin = this.props.headingObject ? 0 : NumCast(this.props.parent.yMargin, 5); const uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx); - const evContents = heading ? heading : this.props.type && this.props.type === "number" ? "0" : `NO ${key.toUpperCase()} VALUE`; + const evContents = heading ? heading : this.props?.type === "number" ? "0" : `NO ${key.toUpperCase()} VALUE`; const headerEditableViewProps = { GetValue: () => evContents, SetValue: this.headingChanged, @@ -310,10 +307,10 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC const headingView = this.props.headingObject ? <div key={heading} className="collectionStackingView-sectionHeader" ref={this._headerRef} style={{ - marginTop: NumCast(this.props.parent.props.Document._yMargin, 5), + marginTop: NumCast(this.props.parent.yMargin, 5), width: (style.columnWidth) / ((uniqueHeadings.length + - ((this.props.parent.props.Document._chromeStatus !== 'view-mode' && this.props.parent.props.Document._chromeStatus !== 'disabled') ? 1 : 0)) || 1) + ((this.props.parent.chromeStatus !== 'view-mode' && this.props.parent.chromeStatus !== 'disabled') ? 1 : 0)) || 1) }}> <div className={"collectionStackingView-collapseBar" + (this.props.headingObject.collapsed === true ? " active" : "")} onClick={this.collapseSection}></div> {/* the default bucket (no key value) has a tooltip that describes what it is. @@ -346,7 +343,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC </div> </div> : (null); for (let i = 0; i < cols; i++) templatecols += `${style.columnWidth / style.numGroupColumns}px `; - const chromeStatus = this.props.parent.props.Document._chromeStatus; + const chromeStatus = this.props.parent.chromeStatus; const type = this.props.parent.props.Document.type; return <> {this.props.parent.Document._columnsHideIfEmpty ? (null) : headingView} @@ -369,7 +366,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC </div> {(chromeStatus !== 'view-mode' && chromeStatus !== 'disabled' && type !== DocumentType.PRES) ? <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton" - style={{ width: style.columnWidth / style.numGroupColumns, marginBottom: 70 }}> + style={{ width: style.columnWidth / style.numGroupColumns, marginBottom: 10 }}> <EditableView {...newEditableViewProps} menuCallback={this.menuCallback} /> </div> : null} </div> diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 075be41fd..649b8c175 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -1,8 +1,7 @@ import { action, computed, IReactionDisposer, reaction, observable, runInAction } from "mobx"; -import { basename } from 'path'; import CursorField from "../../../fields/CursorField"; import { Doc, Opt, Field, DocListCast } from "../../../fields/Doc"; -import { Id } from "../../../fields/FieldSymbols"; +import { Id, ToString } from "../../../fields/FieldSymbols"; import { List } from "../../../fields/List"; import { listSpec } from "../../../fields/Schema"; import { ScriptField } from "../../../fields/ScriptField"; @@ -31,8 +30,8 @@ export interface CollectionViewProps extends FieldViewProps { setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void; rootSelected: (outsideReaction?: boolean) => boolean; fieldKey: string; - NativeWidth: () => number; - NativeHeight: () => number; + NativeWidth?: () => number; + NativeHeight?: () => number; } export interface SubCollectionViewProps extends CollectionViewProps { @@ -40,6 +39,7 @@ export interface SubCollectionViewProps extends CollectionViewProps { children?: never | (() => JSX.Element[]) | React.ReactNode; ChildLayoutTemplate?: () => Doc; childOpacity?: () => number; + childIgnoreNativeSize?: boolean; ChildLayoutString?: string; childClickScript?: ScriptField; childDoubleClickScript?: ScriptField; @@ -111,6 +111,13 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: return this.props.ignoreFields?.includes("_docFilters") ? [] : [...this.props.docFilters(), ...Cast(this.props.Document._docFilters, listSpec("string"), [])]; } + docRangeFilters = () => { + return this.props.ignoreFields?.includes("_docRangeFilters") ? [] : + [...this.props.docRangeFilters(), ...Cast(this.props.Document._docRangeFilters, listSpec("string"), [])]; + } + searchFilterDocs = () => { + return [...this.props.searchFilterDocs(), ...DocListCast(this.props.Document._searchFilterDocs)]; + } @computed get childDocs() { let rawdocs: (Doc | Promise<Doc>)[] = []; if (this.dataField instanceof Doc) { // if collection data is just a document, then promote it to a singleton list; @@ -128,52 +135,38 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField); const childDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs; - let searchDocs = DocListCast(this.props.Document._searchDocs); - - - let docsforFilter: Doc[] = childDocs; + const docFilters = this.docFilters(); + const docRangeFilters = this.docRangeFilters(); + const searchDocs = this.searchFilterDocs(); + if (this.props.Document.dontRegisterView || (!docFilters.length && !docRangeFilters.length && !searchDocs.length)) return childDocs; - if (searchDocs !== undefined && searchDocs.length > 0) { - docsforFilter = []; - const docRangeFilters = this.props.ignoreFields?.includes("_docRangeFilters") ? [] : Cast(this.props.Document._docRangeFilters, listSpec("string"), []); - console.log(searchDocs); - searchDocs = DocUtils.FilterDocs(searchDocs, this.docFilters(), docRangeFilters, viewSpecScript); - console.log(this.docFilters()); - console.log(searchDocs); - childDocs.forEach((d) => { - if (d.data !== undefined) { - let newdocs = DocListCast(d.data); - if (newdocs.length > 0) { - let displaycheck: boolean | undefined = undefined; - let newarray: Doc[] = []; - while (newdocs.length > 0) { - newarray = []; - newdocs.forEach((t) => { - if (d.data !== undefined) { - const newdocs = DocListCast(t.data); - newdocs.forEach((newdoc) => { - newarray.push(newdoc); - }); - } - if (searchDocs.includes(t)) { - displaycheck = true; - } - }); - newdocs = newarray; - } - if (displaycheck === true) { - docsforFilter.push(d); - } + const docsforFilter: Doc[] = []; + childDocs.forEach((d) => { + if (DocUtils.Excluded(d, docFilters)) return; + let notFiltered = d.z || ((!searchDocs.length || searchDocs.includes(d)) && ((!docFilters.length && !docRangeFilters.length) || DocUtils.FilterDocs([d], docFilters, docRangeFilters, viewSpecScript).length > 0)); + const fieldKey = Doc.LayoutFieldKey(d); + const annos = !Field.toString(Doc.LayoutField(d) as Field).includes("CollectionView"); + const data = d[annos ? fieldKey + "-annotations" : fieldKey]; + if (data !== undefined) { + let subDocs = DocListCast(data); + if (subDocs.length > 0) { + let newarray: Doc[] = []; + notFiltered = notFiltered || (!searchDocs.length && (!docFilters.length && !docRangeFilters.length) && DocUtils.FilterDocs(subDocs, docFilters, docRangeFilters, viewSpecScript).length); + while (subDocs.length > 0 && !notFiltered) { + newarray = []; + subDocs.forEach((t) => { + const fieldKey = Doc.LayoutFieldKey(t); + const annos = !Field.toString(Doc.LayoutField(t) as Field).includes("CollectionView"); + notFiltered = notFiltered || ((!searchDocs.length || searchDocs.includes(t)) && ((!docFilters.length && !docRangeFilters.length) || DocUtils.FilterDocs([t], docFilters, docRangeFilters, viewSpecScript).length)); + DocListCast(t[annos ? fieldKey + "-annotations" : fieldKey]).forEach((newdoc) => newarray.push(newdoc)); + }); + subDocs = newarray; } } - if (searchDocs.includes(d)) { - docsforFilter.push(d); - } - }); - return docsforFilter; - } - const docRangeFilters = this.props.ignoreFields?.includes("_docRangeFilters") ? [] : Cast(this.props.Document._docRangeFilters, listSpec("string"), []); - return this.props.Document.dontRegisterView ? childDocs : DocUtils.FilterDocs(childDocs, this.docFilters(), docRangeFilters, viewSpecScript); + } + notFiltered && docsforFilter.push(d); + }); + return docsforFilter; } @action @@ -231,16 +224,22 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: if (docDragData) { let added = false; const dropAction = docDragData.dropAction || docDragData.userDropAction; - if ((!dropAction || dropAction === "move") && docDragData.moveDocument) { + const targetDocments = DocListCast(this.dataDoc[this.props.fieldKey]); + const someMoved = !docDragData.userDropAction && docDragData.draggedDocuments.some(drag => targetDocments.includes(drag)); + if (someMoved) docDragData.droppedDocuments = docDragData.droppedDocuments.map((drop, i) => targetDocments.includes(docDragData.draggedDocuments[i]) ? docDragData.draggedDocuments[i] : drop); + if ((!dropAction || dropAction === "move" || someMoved) && docDragData.moveDocument) { const movedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] === d); const addedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] !== d); - const res = addedDocs.length ? this.addDocument(addedDocs) : true; if (movedDocs.length) { const canAdd = this.props.Document._viewType === CollectionViewType.Pile || de.embedKey || !this.props.isAnnotationOverlay || Doc.AreProtosEqual(Cast(movedDocs[0].annotationOn, Doc, null), this.props.Document); added = docDragData.moveDocument(movedDocs, this.props.Document, canAdd ? this.addDocument : returnFalse); - } else added = res; - e.stopPropagation(); + } else { + ScriptCast(this.props.Document.dropConverter)?.script.run({ dragData: docDragData }); + added = addedDocs.length ? this.addDocument(addedDocs) : true; + } + added && e.stopPropagation(); + return added; } else { ScriptCast(this.props.Document.dropConverter)?.script.run({ dragData: docDragData }); added = this.addDocument(docDragData.droppedDocuments); @@ -249,7 +248,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: e.stopPropagation(); return added; } - else if (de.complete.annoDragData) { + else if (de.complete.annoDragData && (!this.props.isAnnotationOverlay || de.complete.annoDragData.dragDocument === this.props.Document)) { e.stopPropagation(); return this.addDocument(de.complete.annoDragData.dropDocument); } @@ -293,10 +292,10 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: } }); } else { - this.addDocument(Docs.Create.WebDocument(href, { ...options, title: href })); + this.addDocument(Docs.Create.WebDocument(href, { ...options, _fitWidth: true, title: href })); } } else if (text) { - this.addDocument(Docs.Create.TextDocument(text, { ...options, _width: 100, _height: 25 })); + this.addDocument(Docs.Create.TextDocument(text, { ...options, _showTitle: StrCast(Doc.UserDoc().showTitle), _width: 100, _height: 25 })); } return; } @@ -342,7 +341,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: Doc.GetProto(htmlDoc)["data-text"] = Doc.GetProto(htmlDoc).text = text; this.props.addDocument(htmlDoc); if (srcWeb) { - const focusNode = (SelectionManager.SelectedDocuments()[0].ContentDiv?.getElementsByTagName("iframe")?.[0].contentDocument?.getSelection()?.focusNode as any); + const focusNode = (SelectionManager.SelectedDocuments()[0].ContentDiv?.getElementsByTagName("iframe")?.[0]?.contentDocument?.getSelection()?.focusNode as any); if (focusNode) { const rect = "getBoundingClientRect" in focusNode ? focusNode.getBoundingClientRect() : focusNode?.parentElement.getBoundingClientRect(); const x = (rect?.x || 0); @@ -363,14 +362,15 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: if (uriList || text) { if ((uriList || text).includes("www.youtube.com/watch") || text.includes("www.youtube.com/embed")) { const url = (uriList || text).replace("youtube.com/watch?v=", "youtube.com/embed/").split("&")[0]; - this.addDocument(Docs.Create.VideoDocument(url, { + console.log("Video URI = ", uriList); + console.log("Add:" + this.addDocument(Docs.Create.VideoDocument(url, { ...options, title: url, _width: 400, _height: 315, _nativeWidth: 600, _nativeHeight: 472.5 - })); + }))); return; } // let matches: RegExpExecArray | null; @@ -391,27 +391,30 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: // } } if (uriList) { + console.log("Web URI = ", uriList); const existingWebDoc = await Hypothesis.findWebDoc(uriList); if (existingWebDoc) { const alias = Doc.MakeAlias(existingWebDoc); alias.x = options.x; alias.y = options.y; alias._nativeWidth = 850; - alias._nativeHeight = 962; + alias._height = 512; alias._width = 400; this.addDocument(alias); } else { + console.log("Adding ..."); const newDoc = Docs.Create.WebDocument(uriList, { ...options, + _fitWidth: true, title: uriList.split("#annotations:")[0], _width: 400, - _height: 315, + _height: 512, _nativeWidth: 850, - _nativeHeight: 962, - UseCors: true + useCors: true }); + console.log(" ... " + newDoc.title); newDoc.data = new WebField(uriList.split("#annotations:")[0]); // clean hypothes.is URLs that reference a specific annotation (eg. https://en.wikipedia.org/wiki/Cartoon#annotations:t7qAeNbCEeqfG5972KR2Ig) - this.addDocument(newDoc); + console.log(" ... " + this.addDocument(newDoc) + " " + newDoc.title); } return; } diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index c9bf82406..c5add7cfb 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -28,16 +28,7 @@ .no-indent { padding-left: 0; - } - - .bullet { - position: relative; - width: 15px; - color: $intermediate-color; - margin-top: 3px; - transform: scale(1.3, 1.3); - border: #80808030 1px solid; - border-radius: 4px; + width: max-content; } .editableView-container { @@ -85,73 +76,12 @@ white-space: pre-wrap; min-width: 10px; } - -.treeViewItem-openRight { - display: none; - height: 17px; - width: 15px; -} - -.treeViewItem-openRight:hover { - background: #797777; - cursor: pointer; -} - -.treeViewItem-border { - display: inherit; - border-left: dashed 1px #00000042; -} - -.treeViewItem-header-editing, -.treeViewItem-header { - border: transparent 1px solid; - display: flex; - - .editableView-container-editing-oneLine { - min-width: 15px; - } - - .documentView-node-topmost { - width: unset; - } - - >svg { - display: none; - } - -} - -.treeViewItem-header:hover { - .collectionTreeView-keyHeader { - display: inherit; - } - - >svg { - display: inherit; - } - - .treeViewItem-openRight { - display: inline-block; - height: 17px; - width: 15px; - - // display: inline; - svg { - display: block; - padding: 0px; - margin-left: 3px; - } - } -} - -.treeViewItem-header-above { - border-top: black 1px solid; -} - -.treeViewItem-header-below { - border-bottom: black 1px solid; -} - -.treeViewItem-header-inside { - border: black 1px solid; +.docContainer-system { + font-variant: all-small-caps; + border-radius: 5px; + background: grey; + color: white; + padding-left: 3px; + padding-right: 3px; + padding-bottom: 2px; }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index f23fa8eb6..a7c8a7cdc 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -1,667 +1,31 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable } from "mobx"; +import { action, computed } from "mobx"; import { observer } from "mobx-react"; -import { DataSym, Doc, DocListCast, Field, HeightSym, Opt, WidthSym } from '../../../fields/Doc'; +import { DataSym, Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; -import { PrefetchProxy } from '../../../fields/Proxy'; -import { Document, listSpec } from '../../../fields/Schema'; -import { ComputedField, ScriptField } from '../../../fields/ScriptField'; -import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; -import { emptyFunction, emptyPath, returnFalse, returnOne, returnTrue, returnZero, simulateMouseClick, Utils, returnEmptyFilter } from '../../../Utils'; -import { Docs, DocUtils } from '../../documents/Documents'; -import { DocumentType } from "../../documents/DocumentTypes"; -import { DocumentManager } from '../../util/DocumentManager'; -import { SnappingManager } from '../../util/SnappingManager'; +import { Document } from '../../../fields/Schema'; +import { ScriptField } from '../../../fields/ScriptField'; +import { BoolCast, NumCast, ScriptCast, StrCast, Cast } from '../../../fields/Types'; +import { TraceMobx } from '../../../fields/util'; +import { emptyPath, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, returnTrue, Utils } from '../../../Utils'; +import { DocUtils } from '../../documents/Documents'; import { DragManager, dropActionType } from "../../util/DragManager"; -import { Scripting } from '../../util/Scripting'; import { SelectionManager } from '../../util/SelectionManager'; -import { Transform } from '../../util/Transform'; +import { SnappingManager } from '../../util/SnappingManager'; import { undoBatch, UndoManager } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { EditableView } from "../EditableView"; -import { MainView } from '../MainView'; import { ContentFittingDocumentView } from '../nodes/ContentFittingDocumentView'; -import { DocumentView } from '../nodes/DocumentView'; -import { ImageBox } from '../nodes/ImageBox'; -import { KeyValueBox } from '../nodes/KeyValueBox'; -import { Templates } from '../Templates'; +import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; +import { RichTextMenu } from '../nodes/formattedText/RichTextMenu'; import { CollectionSubView } from "./CollectionSubView"; import "./CollectionTreeView.scss"; -import { CollectionViewType } from './CollectionView'; +import { TreeView } from "./TreeView"; import React = require("react"); -import { makeTemplate } from '../../util/DropConverter'; -import { TraceMobx } from '../../../fields/util'; - -export interface TreeViewProps { - document: Doc; - dataDoc?: Doc; - libraryPath: Doc[] | undefined; - containingCollection: Doc; - prevSibling?: Doc; - renderDepth: number; - deleteDoc: (doc: Doc | Doc[]) => boolean; - moveDocument: DragManager.MoveFunction; - dropAction: dropActionType; - addDocTab: (doc: Doc, where: string, libraryPath?: Doc[]) => boolean; - pinToPres: (document: Doc) => void; - panelWidth: () => number; - panelHeight: () => number; - ChromeHeight: undefined | (() => number); - addDocument: (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => boolean; - indentDocument?: () => void; - outdentDocument?: () => void; - ScreenToLocalTransform: () => Transform; - backgroundColor?: (doc: Doc, renderDepth: number) => string | undefined; - outerXf: () => { translateX: number, translateY: number }; - treeViewDoc: Doc; - parentKey: string; - active: (outsideReaction?: boolean) => boolean; - treeViewHideHeaderFields: () => boolean; - treeViewPreventOpen: boolean; - renderedIds: string[]; // list of document ids rendered used to avoid unending expansion of items in a cycle - onCheckedClick?: () => ScriptField; - onChildClick?: () => ScriptField; - ignoreFields?: string[]; -} - -@observer -/** - * Renders a treeView of a collection of documents - * - * special fields: - * treeViewOpen : flag denoting whether the documents sub-tree (contents) is visible or hidden - * treeViewPreventOpen : ignores the treeViewOpen flag (for allowing a view to not be slaved to other views of the document) - * treeViewExpandedView : name of field whose contents are being displayed as the document's subtree - */ -class TreeView extends React.Component<TreeViewProps> { - private _editTitleScript: (() => ScriptField) | undefined; - private _header?: React.RefObject<HTMLDivElement> = React.createRef(); - private _treedropDisposer?: DragManager.DragDropDisposer; - private _dref = React.createRef<HTMLDivElement>(); - private _tref = React.createRef<HTMLDivElement>(); - private _docRef = React.createRef<DocumentView>(); - private _uniqueId = Utils.GenerateGuid(); - private _editMaxWidth: number | string = 0; - - get doc() { return this.props.document; } - get noviceMode() { return BoolCast(Doc.UserDoc().noviceMode, false); } - get displayName() { return "TreeView(" + this.doc.title + ")"; } // this makes mobx trace() statements more descriptive - get defaultExpandedView() { return this.childDocs.length ? this.fieldKey : StrCast(this.doc.defaultExpandedView, this.noviceMode ? "layout" : "fields"); } - @observable _overrideTreeViewOpen = false; // override of the treeViewOpen field allowing the display state to be independent of the document's state - set treeViewOpen(c: boolean) { - if (this.props.treeViewPreventOpen) this._overrideTreeViewOpen = c; - else this.doc.treeViewOpen = this._overrideTreeViewOpen = c; - } - @computed get treeViewOpen() { return (!this.props.treeViewPreventOpen && !this.doc.treeViewPreventOpen && BoolCast(this.doc.treeViewOpen)) || this._overrideTreeViewOpen; } - @computed get treeViewExpandedView() { return StrCast(this.doc.treeViewExpandedView, this.defaultExpandedView); } - @computed get MAX_EMBED_HEIGHT() { return NumCast(this.props.containingCollection.maxEmbedHeight, 200); } - @computed get dataDoc() { return this.doc[DataSym]; } - @computed get layoutDoc() { return Doc.Layout(this.doc); } - @computed get fieldKey() { const splits = StrCast(Doc.LayoutField(this.doc)).split("fieldKey={\'"); return splits.length > 1 ? splits[1].split("\'")[0] : "data"; } - childDocList(field: string) { - const layout = Doc.LayoutField(this.doc) instanceof Doc ? Doc.LayoutField(this.doc) as Doc : undefined; - return ((this.props.dataDoc ? DocListCast(this.props.dataDoc[field]) : undefined) || // if there's a data doc for an expanded template, use it's data field - (layout ? DocListCast(layout[field]) : undefined) || // else if there's a layout doc, display it's fields - DocListCast(this.doc[field])); // otherwise use the document's data field - } - @computed get childDocs() { return this.childDocList(this.fieldKey); } - @computed get childLinks() { return this.childDocList("links"); } - @computed get childAnnos() { return this.childDocList(this.fieldKey + "-annotations"); } - @computed get boundsOfCollectionDocument() { - return StrCast(this.props.document.type).indexOf(DocumentType.COL) === -1 || !DocListCast(this.props.document[this.fieldKey]).length ? undefined : - Doc.ComputeContentBounds(DocListCast(this.props.document[this.fieldKey])); - } - - @undoBatch openRight = () => this.props.addDocTab(this.doc, "onRight", this.props.libraryPath); - @undoBatch move = (doc: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => { - return this.doc !== target && this.props.deleteDoc(doc) && addDoc(doc); - } - @undoBatch @action remove = (doc: Doc | Doc[], key: string) => { - return (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && Doc.RemoveDocFromList(this.dataDoc, key, doc), true); - } - @undoBatch @action removeDoc = (doc: Doc | Doc[]) => { - return (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => - flg && Doc.RemoveDocFromList(this.props.containingCollection, Doc.LayoutFieldKey(this.props.containingCollection), doc), true); - } - - constructor(props: any) { - super(props); - const script = ScriptField.MakeScript(`{setInPlace(self, 'editTitle', '${this._uniqueId}'); documentView.select();} `, { documentView: "any" }); - this._editTitleScript = script && (() => script); - if (Doc.GetT(this.doc, "editTitle", "string", true) === "*") Doc.SetInPlace(this.doc, "editTitle", this._uniqueId, false); - } - - protected createTreeDropTarget = (ele: HTMLDivElement) => { - this._treedropDisposer?.(); - ele && (this._treedropDisposer = DragManager.MakeDropTarget(ele, this.treeDrop.bind(this), undefined, this.preTreeDrop.bind(this)), this.doc); - } - - onPointerEnter = (e: React.PointerEvent): void => { - this.props.active(true) && Doc.BrushDoc(this.dataDoc); - if (e.buttons === 1 && SnappingManager.GetIsDragging()) { - this._header!.current!.className = "treeViewItem-header"; - document.addEventListener("pointermove", this.onDragMove, true); - } - } - onPointerLeave = (e: React.PointerEvent): void => { - Doc.UnBrushDoc(this.dataDoc); - if (this._header?.current?.className !== "treeViewItem-header-editing") { - this._header!.current!.className = "treeViewItem-header"; - } - document.removeEventListener("pointermove", this.onDragMove, true); - } - onDragMove = (e: PointerEvent): void => { - Doc.UnBrushDoc(this.dataDoc); - const pt = [e.clientX, e.clientY]; - const rect = this._header!.current!.getBoundingClientRect(); - const before = pt[1] < rect.top + rect.height / 2; - const inside = pt[0] > Math.min(rect.left + 75, rect.left + rect.width * .75) || (!before && this.treeViewOpen && this.childDocList.length); - this._header!.current!.className = "treeViewItem-header"; - if (inside) this._header!.current!.className += " treeViewItem-header-inside"; - else if (before) this._header!.current!.className += " treeViewItem-header-above"; - else if (!before) this._header!.current!.className += " treeViewItem-header-below"; - e.stopPropagation(); - } - - editableView = (key: string, style?: string) => (<EditableView - oneLine={true} - display={"inline-block"} - editing={true} - contents={StrCast(this.doc[key])} - height={12} - sizeToContent={true} - fontStyle={style} - fontSize={12} - GetValue={() => StrCast(this.doc[key])} - SetValue={undoBatch((value: string) => { - Doc.SetInPlace(this.doc, key, value, false) || true; - Doc.SetInPlace(this.doc, "editTitle", undefined, false); - })} - OnFillDown={undoBatch((value: string) => { - Doc.SetInPlace(this.doc, key, value, false); - const doc = Docs.Create.FreeformDocument([], { title: "-", x: 0, y: 0, _width: 100, _height: 25, templates: new List<string>([Templates.Title.Layout]) }); - Doc.SetInPlace(this.doc, "editTitle", undefined, false); - Doc.SetInPlace(doc, "editTitle", "*", false); - return this.props.addDocument(doc); - })} - onClick={() => { - SelectionManager.DeselectAll(); - Doc.UserDoc().activeSelection = new List([this.doc]); - return false; - }} - OnTab={undoBatch((shift?: boolean) => { - shift ? this.props.outdentDocument?.() : this.props.indentDocument?.(); - setTimeout(() => Doc.SetInPlace(this.doc, "editTitle", "*", false), 0); - })} - />) - - preTreeDrop = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => { - const dragData = de.complete.docDragData; - dragData && (dragData.dropAction = this.props.treeViewDoc === dragData.treeViewDoc ? "same" : dragData.dropAction); - } - - @undoBatch - treeDrop = (e: Event, de: DragManager.DropEvent) => { - const pt = [de.x, de.y]; - const rect = this._header!.current!.getBoundingClientRect(); - const before = pt[1] < rect.top + rect.height / 2; - const inside = pt[0] > Math.min(rect.left + 75, rect.left + rect.width * .75) || (!before && this.treeViewOpen && this.childDocList.length); - const complete = de.complete; - if (complete.linkDragData) { - const sourceDoc = complete.linkDragData.linkSourceDocument; - const destDoc = this.doc; - DocUtils.MakeLink({ doc: sourceDoc }, { doc: destDoc }, "tree link", ""); - e.stopPropagation(); - } - const docDragData = complete.docDragData; - if (docDragData) { - e.stopPropagation(); - if (docDragData.draggedDocuments[0] === this.doc) return true; - const parentAddDoc = (doc: Doc | Doc[]) => this.props.addDocument(doc, undefined, before); - let addDoc = parentAddDoc; - if (inside) { - addDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce( - (flg: boolean, doc) => flg && Doc.AddDocToList(this.dataDoc, this.fieldKey, doc), true) || parentAddDoc(doc); - } - const move = (!docDragData.dropAction || docDragData.dropAction === "move" || docDragData.dropAction === "same") && docDragData.moveDocument; - return docDragData.droppedDocuments.reduce((added, d) => (move ? docDragData.moveDocument?.(d, undefined, addDoc) : addDoc(d)) || added, false); - } - return false; - } - - refTransform = (ref: HTMLDivElement) => { - const { scale, translateX, translateY } = Utils.GetScreenTransform(ref); - const outerXf = this.props.outerXf(); - const offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY); - return this.props.ScreenToLocalTransform().translate(offset[0], offset[1]); - } - docTransform = () => this.refTransform(this._dref.current!); - getTransform = () => this.refTransform(this._tref.current!); - docWidth = () => { - const layoutDoc = this.layoutDoc; - const aspect = NumCast(layoutDoc._nativeHeight, layoutDoc._fitWidth ? 0 : layoutDoc[HeightSym]()) / NumCast(layoutDoc._nativeWidth, layoutDoc._fitWidth ? 1 : layoutDoc[WidthSym]()); - if (aspect) return Math.min(layoutDoc[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT / aspect, this.props.panelWidth() - 20)); - return NumCast(layoutDoc._nativeWidth) ? Math.min(layoutDoc[WidthSym](), this.props.panelWidth() - 20) : this.props.panelWidth() - 20; - } - docHeight = () => { - const layoutDoc = this.layoutDoc; - const bounds = this.boundsOfCollectionDocument; - return Math.max(70, Math.min(this.MAX_EMBED_HEIGHT, (() => { - const aspect = NumCast(layoutDoc._nativeHeight, layoutDoc._fitWidth ? 0 : layoutDoc[HeightSym]()) / NumCast(layoutDoc._nativeWidth, layoutDoc._fitWidth ? 1 : layoutDoc[WidthSym]()); - if (aspect) return this.docWidth() * aspect; - if (bounds) return this.docWidth() * (bounds.b - bounds.y) / (bounds.r - bounds.x); - return layoutDoc._fitWidth ? (!this.doc._nativeHeight ? NumCast(this.props.containingCollection._height) : - Math.min(this.docWidth() * NumCast(layoutDoc.scrollHeight, NumCast(layoutDoc._nativeHeight)) / NumCast(layoutDoc._nativeWidth, - NumCast(this.props.containingCollection._height)))) : - NumCast(layoutDoc._height) ? NumCast(layoutDoc._height) : 50; - })())); - } - - @computed get expandedField() { - const ids: { [key: string]: string } = {}; - const doc = this.doc; - doc && Object.keys(doc).forEach(key => !(key in ids) && doc[key] !== ComputedField.undefined && (ids[key] = key)); - - const rows: JSX.Element[] = []; - for (const key of Object.keys(ids).slice().sort()) { - if (this.props.ignoreFields?.includes(key) || key === "title" || key === "treeViewOpen") continue; - const contents = doc[key]; - let contentElement: (JSX.Element | null)[] | JSX.Element = []; - - if (contents instanceof Doc || (Cast(contents, listSpec(Doc)) && (Cast(contents, listSpec(Doc))!.length && Cast(contents, listSpec(Doc))![0] instanceof Doc))) { - const remDoc = (doc: Doc | Doc[]) => this.remove(doc, key); - const addDoc = (doc: Doc | Doc[], addBefore?: Doc, before?: boolean) => (doc instanceof Doc ? [doc] : doc).reduce( - (flg, doc) => flg && Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false, true), true); - contentElement = TreeView.GetChildElements(contents instanceof Doc ? [contents] : - DocListCast(contents), this.props.treeViewDoc, doc, undefined, key, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move, - this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, - this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.treeViewHideHeaderFields, this.props.treeViewPreventOpen, - [...this.props.renderedIds, doc[Id]], this.props.libraryPath, this.props.onCheckedClick, this.props.onChildClick, this.props.ignoreFields); - } else { - contentElement = <EditableView key="editableView" - contents={contents !== undefined ? Field.toString(contents as Field) : "null"} - height={13} - fontSize={12} - GetValue={() => Field.toKeyValueString(doc, key)} - SetValue={(value: string) => KeyValueBox.SetField(doc, key, value, true)} />; - } - rows.push(<div style={{ display: "flex" }} key={key}> - <span style={{ fontWeight: "bold" }}>{key + ":"}</span> - - {contentElement} - </div>); - } - rows.push(<div style={{ display: "flex" }} key={"newKeyValue"}> - <EditableView - key="editableView" - contents={"+key:value"} - height={13} - fontSize={12} - GetValue={() => ""} - SetValue={(value: string) => { - value.indexOf(":") !== -1 && KeyValueBox.SetField(doc, value.substring(0, value.indexOf(":")), value.substring(value.indexOf(":") + 1, value.length), true); - return true; - }} /> - </div>); - return rows; - } - - rtfWidth = () => Math.min(this.layoutDoc?.[WidthSym](), this.props.panelWidth() - 20); - rtfHeight = () => this.rtfWidth() <= this.layoutDoc?.[WidthSym]() ? Math.min(this.layoutDoc?.[HeightSym](), this.MAX_EMBED_HEIGHT) : this.MAX_EMBED_HEIGHT; - - @computed get renderContent() { - TraceMobx(); - const expandKey = this.treeViewExpandedView; - if (["links", "annotations", this.fieldKey].includes(expandKey)) { - const remDoc = (doc: Doc | Doc[]) => this.remove(doc, expandKey); - const addDoc = (doc: Doc | Doc[], addBefore?: Doc, before?: boolean) => - (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && Doc.AddDocToList(this.dataDoc, expandKey, doc, addBefore, before, false, true), true); - const docs = expandKey === "links" ? this.childLinks : expandKey === "annotations" ? this.childAnnos : this.childDocs; - const sortKey = `${this.fieldKey}-sortAscending`; - return <ul key={expandKey + "more"} onClick={(e) => { - this.doc[sortKey] = (this.doc[sortKey] ? false : (this.doc[sortKey] === false ? undefined : true)); - e.stopPropagation(); - }}> - {!docs ? (null) : - TreeView.GetChildElements(docs, this.props.treeViewDoc, this.layoutDoc, - this.dataDoc, expandKey, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move, - StrCast(this.doc.childDropAction, this.props.dropAction) as dropActionType, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform, - this.props.outerXf, this.props.active, this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.treeViewHideHeaderFields, this.props.treeViewPreventOpen, - [...this.props.renderedIds, this.doc[Id]], this.props.libraryPath, this.props.onCheckedClick, this.props.onChildClick, this.props.ignoreFields)} - </ul >; - } else if (this.treeViewExpandedView === "fields") { - return <ul key={this.doc[Id] + this.doc.title}><div ref={this._dref} style={{ display: "inline-block" }} > - {this.expandedField} - </div></ul>; - } else { - const layoutDoc = this.layoutDoc; - const panelHeight = StrCast(Doc.LayoutField(layoutDoc)).includes("FormattedTextBox") ? this.rtfHeight : this.docHeight; - const panelWidth = StrCast(Doc.LayoutField(layoutDoc)).includes("FormattedTextBox") ? this.rtfWidth : this.docWidth; - return <div ref={this._dref} style={{ display: "inline-block", height: panelHeight() }} key={this.doc[Id]}> - <ContentFittingDocumentView - Document={layoutDoc} - DataDoc={this.dataDoc} - LibraryPath={emptyPath} - renderDepth={this.props.renderDepth + 1} - rootSelected={returnTrue} - treeViewDoc={undefined} - backgroundColor={this.props.backgroundColor} - fitToBox={this.boundsOfCollectionDocument !== undefined} - FreezeDimensions={true} - NativeWidth={layoutDoc.type === DocumentType.RTF ? this.rtfWidth : returnZero} - NativeHeight={layoutDoc.type === DocumentType.RTF ? this.rtfHeight : returnZero} - PanelWidth={panelWidth} - PanelHeight={panelHeight} - focus={returnFalse} - ScreenToLocalTransform={this.docTransform} - docFilters={returnEmptyFilter} - ContainingCollectionDoc={this.props.containingCollection} - ContainingCollectionView={undefined} - addDocument={returnFalse} - moveDocument={this.props.moveDocument} - removeDocument={returnFalse} - parentActive={this.props.active} - whenActiveChanged={emptyFunction} - addDocTab={this.props.addDocTab} - pinToPres={this.props.pinToPres} - bringToFront={returnFalse} - ContentScaling={returnOne} - /> - </div>; - } - } - - get onCheckedClick() { return this.props.onCheckedClick?.() ?? ScriptCast(this.doc.onCheckedClick); } - - @action - bulletClick = (e: React.MouseEvent) => { - if (this.onCheckedClick && this.doc.type !== DocumentType.COL) { - // this.props.document.treeViewChecked = this.props.document.treeViewChecked === "check" ? "x" : this.props.document.treeViewChecked === "x" ? undefined : "check"; - this.onCheckedClick?.script.run({ - this: this.doc.isTemplateForField && this.props.dataDoc ? this.props.dataDoc : this.doc, - heading: this.props.containingCollection.title, - checked: this.doc.treeViewChecked === "check" ? "x" : this.doc.treeViewChecked === "x" ? undefined : "check", - containingTreeView: this.props.treeViewDoc, - }, console.log); - } else { - this.treeViewOpen = !this.treeViewOpen; - } - e.stopPropagation(); - } - - @computed get renderBullet() { - TraceMobx(); - const checked = this.doc.type === DocumentType.COL ? undefined : this.onCheckedClick ? (this.doc.treeViewChecked ?? "unchecked") : undefined; - return <div className="bullet" - title={this.childDocs?.length ? `click to see ${this.childDocs?.length} items` : "view fields"} - onClick={this.bulletClick} - style={{ color: StrCast(this.doc.color, checked === "unchecked" ? "white" : "inherit"), opacity: checked === "unchecked" ? undefined : 0.4 }}> - {<FontAwesomeIcon icon={checked === "check" ? "check" : (checked === "x" ? "times" : checked === "unchecked" ? "square" : !this.treeViewOpen ? (this.childDocs?.length ? "caret-square-right" : "caret-right") : (this.childDocs?.length ? "caret-square-down" : "caret-down"))} />} - </div>; - } - - showContextMenu = (e: React.MouseEvent) => { - this._docRef.current?.ContentDiv && simulateMouseClick(this._docRef.current.ContentDiv, e.clientX, e.clientY + 30, e.screenX, e.screenY + 30); - } - focusOnDoc = (doc: Doc) => DocumentManager.Instance.getFirstDocumentView(doc)?.props.focus(doc, true); - contextMenuItems = () => [{ script: ScriptField.MakeFunction(`DocFocus(self)`)!, label: "Focus" }]; - truncateTitleWidth = () => NumCast(this.props.treeViewDoc.treeViewTruncateTitleWidth, 0); - showTitleEdit = () => ["*", this._uniqueId].includes(Doc.GetT(this.doc, "editTitle", "string", true) || ""); - onChildClick = () => this.props.onChildClick?.() ?? (this._editTitleScript?.() || ScriptCast(this.doc.editTitleScript)); - /** - * Renders the EditableView title element for placement into the tree. - */ - @computed - get renderTitle() { - TraceMobx(); - const headerElements = this.props.treeViewHideHeaderFields() ? (null) : - <> - <FontAwesomeIcon icon="cog" size="sm" onClick={e => { this.showContextMenu(e); e.stopPropagation(); }} /> - <span className="collectionTreeView-keyHeader" key={this.treeViewExpandedView} - onPointerDown={action(() => { - if (this.treeViewOpen) { - this.doc.treeViewExpandedView = this.treeViewExpandedView === this.fieldKey ? (Doc.UserDoc().noviceMode ? "layout" : "fields") : - this.treeViewExpandedView === "fields" && this.layoutDoc ? "layout" : - this.treeViewExpandedView === "layout" && DocListCast(this.doc.links).length ? "links" : - (this.treeViewExpandedView === "links" || this.treeViewExpandedView === "layout") && DocListCast(this.doc[this.fieldKey + "-annotations"]).length ? "annotations" : - this.childDocs.length ? this.fieldKey : (Doc.UserDoc().noviceMode ? "layout" : "fields"); - } - this.treeViewOpen = true; - })}> - {this.treeViewExpandedView} - </span> - </>; - const view = this.showTitleEdit() ? this.editableView("title") : - <DocumentView - ref={this._docRef} - Document={this.doc} - DataDoc={undefined} - treeViewDoc={this.props.treeViewDoc} - LibraryPath={this.props.libraryPath || emptyPath} - addDocument={undefined} - addDocTab={this.props.addDocTab} - rootSelected={returnTrue} - pinToPres={emptyFunction} - onClick={this.onChildClick} - dropAction={this.props.dropAction} - moveDocument={this.move} - removeDocument={this.removeDoc} - ScreenToLocalTransform={this.getTransform} - ContentScaling={returnOne} - PanelWidth={this.truncateTitleWidth} - PanelHeight={returnZero} - NativeHeight={returnZero} - NativeWidth={returnZero} - contextMenuItems={this.contextMenuItems} - opacity={returnOne} - renderDepth={1} - focus={returnTrue} - parentActive={returnTrue} - whenActiveChanged={emptyFunction} - bringToFront={emptyFunction} - dontRegisterView={BoolCast(this.props.treeViewDoc.dontRegisterChildViews)} - docFilters={returnEmptyFilter} - ContainingCollectionView={undefined} - ContainingCollectionDoc={this.props.containingCollection} - />; - return <> - <div className="docContainer" ref={this._tref} title="click to edit title" id={`docContainer-${this.props.parentKey}`} - style={{ - fontWeight: this.doc.searchMatch ? "bold" : undefined, - textDecoration: Doc.GetT(this.doc, "title", "string", true) ? "underline" : undefined, - outline: BoolCast(this.doc.workspaceBrush) ? "dashed 1px #06123232" : undefined, - pointerEvents: this.props.active() || SnappingManager.GetIsDragging() ? undefined : "none" - }} > - {view} - </div > - {headerElements} - <div className="treeViewItem-openRight" onClick={this.openRight}> - <FontAwesomeIcon title="open in a new pane" icon="external-link-alt" size="sm" /> - </div> - </>; - } - - render() { - TraceMobx(); - const sorting = this.doc[`${this.fieldKey}-sortAscending`]; - if (this.showTitleEdit()) { // find containing CollectionTreeView and set our maximum width so the containing tree view won't have to scroll - let par: any = this._header?.current; - if (par) { - while (par && par.className !== "collectionTreeView-dropTarget") par = par.parentNode; - if (par) { - const par_rect = (par as HTMLElement).getBoundingClientRect(); - const my_recct = this._docRef.current?.ContentDiv?.getBoundingClientRect(); - this._editMaxWidth = Math.max(100, par_rect.right - (my_recct?.left || 0)); - } - } - } else this._editMaxWidth = ""; - return <div className="treeViewItem-container" ref={this.createTreeDropTarget} onPointerDown={e => this.props.active(true) && SelectionManager.DeselectAll()}> - <li className="collection-child"> - <div className={`treeViewItem-header` + (this._editMaxWidth ? "-editing" : "")} ref={this._header} style={{ maxWidth: this._editMaxWidth }} onClick={e => { - if (this.props.active(true)) { - e.stopPropagation(); - e.preventDefault(); - SelectionManager.DeselectAll(); - } - }} - onPointerDown={e => { - if (this.props.active(true)) { - e.stopPropagation(); - e.preventDefault(); - } - }} - onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}> - {this.renderBullet} - {this.renderTitle} - </div> - <div className="treeViewItem-border" style={{ borderColor: sorting === undefined ? undefined : sorting ? "crimson" : "blue" }}> - {!this.treeViewOpen || this.props.renderedIds.indexOf(this.doc[Id]) !== -1 ? (null) : this.renderContent} - </div> - </li> - </div>; - } - public static GetChildElements( - childDocs: Doc[], - treeViewDoc: Doc, - containingCollection: Doc, - dataDoc: Doc | undefined, - key: string, - parentCollectionDoc: Doc | undefined, - parentPrevSibling: Doc | undefined, - add: (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => boolean, - remove: ((doc: Doc | Doc[]) => boolean), - move: DragManager.MoveFunction, - dropAction: dropActionType, - addDocTab: (doc: Doc, where: string) => boolean, - pinToPres: (document: Doc) => void, - backgroundColor: undefined | ((document: Doc, renderDepth: number) => string | undefined), - screenToLocalXf: () => Transform, - outerXf: () => { translateX: number, translateY: number }, - active: (outsideReaction?: boolean) => boolean, - panelWidth: () => number, - ChromeHeight: undefined | (() => number), - renderDepth: number, - treeViewHideHeaderFields: () => boolean, - treeViewPreventOpen: boolean, - renderedIds: string[], - libraryPath: Doc[] | undefined, - onCheckedClick: undefined | (() => ScriptField), - onChildClick: undefined | (() => ScriptField), - ignoreFields: string[] | undefined - ) { - const viewSpecScript = Cast(containingCollection.viewSpecScript, ScriptField); - if (viewSpecScript) { - childDocs = childDocs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result); - } - - const docs = childDocs.slice(); - const ascending = containingCollection?.[key + "-sortAscending"]; - if (ascending !== undefined) { - const sortAlphaNum = (a: string, b: string): 0 | 1 | -1 => { - const reN = /[0-9]*$/; - const aA = a.replace(reN, ""); // get rid of trailing numbers - const bA = b.replace(reN, ""); - if (aA === bA) { // if header string matches, then compare numbers numerically - const aN = parseInt(a.match(reN)![0], 10); - const bN = parseInt(b.match(reN)![0], 10); - return aN === bN ? 0 : aN > bN ? 1 : -1; - } else { - return aA > bA ? 1 : -1; - } - }; - docs.sort(function (a, b): 0 | 1 | -1 { - const descA = ascending ? b : a; - const descB = ascending ? a : b; - const first = descA.title; - const second = descB.title; - // TODO find better way to sort how to sort.................. - if (typeof first === 'number' && typeof second === 'number') { - return (first - second) > 0 ? 1 : -1; - } - if (typeof first === 'string' && typeof second === 'string') { - return sortAlphaNum(first, second); - } - if (typeof first === 'boolean' && typeof second === 'boolean') { - // if (first === second) { // bugfixing?: otherwise, the list "flickers" because the list is resorted during every load - // return Number(descA.x) > Number(descB.x) ? 1 : -1; - // } - return first > second ? 1 : -1; - } - return ascending ? 1 : -1; - }); - } - - const rowWidth = () => panelWidth() - 20; - return docs.map((child, i) => { - const pair = Doc.GetLayoutDataDocPair(containingCollection, dataDoc, child); - if (!pair.layout || pair.data instanceof Promise) { - return (null); - } - - const indent = i === 0 ? undefined : () => { - if (StrCast(docs[i - 1].layout).indexOf('fieldKey') !== -1) { - const fieldKeysub = StrCast(docs[i - 1].layout).split('fieldKey')[1]; - const fieldKey = fieldKeysub.split("\'")[1]; - if (fieldKey && Cast(docs[i - 1][fieldKey], listSpec(Doc)) !== undefined) { - Doc.AddDocToList(docs[i - 1], fieldKey, child); - docs[i - 1].treeViewOpen = true; - remove(child); - } - } - }; - const outdent = !parentCollectionDoc ? undefined : () => { - if (StrCast(parentCollectionDoc.layout).indexOf('fieldKey') !== -1) { - const fieldKeysub = StrCast(parentCollectionDoc.layout).split('fieldKey')[1]; - const fieldKey = fieldKeysub.split("\'")[1]; - Doc.AddDocToList(parentCollectionDoc, fieldKey, child, parentPrevSibling, false); - parentCollectionDoc.treeViewOpen = true; - remove(child); - } - }; - const addDocument = (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => { - return add(doc, relativeTo ? relativeTo : docs[i], before !== undefined ? before : false); - }; - const childLayout = Doc.Layout(pair.layout); - const rowHeight = () => { - const aspect = NumCast(childLayout._nativeWidth, 0) / NumCast(childLayout._nativeHeight, 0); - return aspect ? Math.min(childLayout[WidthSym](), rowWidth()) / aspect : childLayout[HeightSym](); - }; - return !(child instanceof Doc) ? (null) : <TreeView - document={pair.layout} - dataDoc={pair.data} - libraryPath={libraryPath ? [...libraryPath, containingCollection] : undefined} - containingCollection={containingCollection} - prevSibling={docs[i]} - treeViewDoc={treeViewDoc} - key={child[Id]} - indentDocument={indent} - outdentDocument={outdent} - onCheckedClick={onCheckedClick} - onChildClick={onChildClick} - renderDepth={renderDepth} - deleteDoc={remove} - addDocument={addDocument} - backgroundColor={backgroundColor} - panelWidth={rowWidth} - panelHeight={rowHeight} - ChromeHeight={ChromeHeight} - moveDocument={move} - dropAction={dropAction} - addDocTab={addDocTab} - pinToPres={pinToPres} - ScreenToLocalTransform={screenToLocalXf} - outerXf={outerXf} - parentKey={key} - active={active} - treeViewHideHeaderFields={treeViewHideHeaderFields} - treeViewPreventOpen={treeViewPreventOpen} - renderedIds={renderedIds} - ignoreFields={ignoreFields} />; - }); - } -} +import { DocumentManager } from '../../util/DocumentManager'; +import { FormattedTextBoxComment } from '../nodes/formattedText/FormattedTextBoxComment'; export type collectionTreeViewProps = { treeViewHideTitle?: boolean; @@ -673,16 +37,16 @@ export type collectionTreeViewProps = { @observer export class CollectionTreeView extends CollectionSubView<Document, Partial<collectionTreeViewProps>>(Document) { private treedropDisposer?: DragManager.DragDropDisposer; + private _isChildActive = false; private _mainEle?: HTMLDivElement; + public _uniqueId = Utils.GenerateGuid(); - @computed get doc() { return this.props.Document; } + @computed get doc() { TraceMobx(); return this.props.Document; } @computed get dataDoc() { return this.props.DataDoc || this.doc; } protected createTreeDropTarget = (ele: HTMLDivElement) => { this.treedropDisposer?.(); - if (this._mainEle = ele) { - this.treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.doc, this.onInternalPreDrop.bind(this)); - } + if (this._mainEle = ele) this.treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.doc, this.onInternalPreDrop.bind(this)); } protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => { @@ -699,18 +63,26 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll this.treedropDisposer?.(); } - @action - remove = (doc: Doc | Doc[]): boolean => { + @undoBatch + remove = action((doc: Doc | Doc[]): boolean => { const docs = doc instanceof Doc ? [doc] : doc; const targetDataDoc = this.doc[DataSym]; const value = DocListCast(targetDataDoc[this.props.fieldKey]); const result = value.filter(v => !docs.includes(v)); + SelectionManager.DeselectAll(); if (result.length !== value.length) { + const ind = targetDataDoc[this.props.fieldKey].indexOf(doc); targetDataDoc[this.props.fieldKey] = new List<Doc>(result); + if (ind > 0) { + const prev = targetDataDoc[this.props.fieldKey][ind - 1]; + FormattedTextBox.SelectOnLoad = prev[Id]; + const prevView = DocumentManager.Instance.getDocumentView(prev, this.props.CollectionView); + prevView?.select(false); + } return true; } return false; - } + }); @action addDoc = (doc: Doc | Doc[], relativeTo: Opt<Doc>, before?: boolean): boolean => { const doAddDoc = (doc: Doc | Doc[]) => @@ -718,172 +90,150 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll flg && Doc.AddDocToList(this.doc[DataSym], this.props.fieldKey, doc, relativeTo, before, false, false, false), true); if (this.doc.resolvedDataDoc instanceof Promise) { this.doc.resolvedDataDoc.then((resolved: any) => doAddDoc(doc)); + } else if (relativeTo === undefined) { + this.props.addDocument(doc); } else { doAddDoc(doc); + (doc instanceof Doc ? [doc] : doc).forEach(d => d.context = this.props.Document); } return true; } onContextMenu = (e: React.MouseEvent): void => { // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout - if (!e.isPropagationStopped() && this.doc === Doc.UserDoc().myWorkspaces) { - ContextMenu.Instance.addItem({ description: "Create Workspace", event: () => MainView.Instance.createNewWorkspace(), icon: "plus" }); - ContextMenu.Instance.addItem({ description: "Delete Workspace", event: () => this.remove(this.doc), icon: "minus" }); - e.stopPropagation(); - e.preventDefault(); - ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); - } else if (!e.isPropagationStopped() && this.doc === Doc.UserDoc().myRecentlyClosed) { - ContextMenu.Instance.addItem({ description: "Clear All", event: () => Doc.UserDoc().myRecentlyClosed = new List<Doc>(), icon: "plus" }); - e.stopPropagation(); - e.preventDefault(); - ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); - } else { + if (!Doc.UserDoc().noviceMode) { const layoutItems: ContextMenuProps[] = []; layoutItems.push({ description: (this.doc.treeViewPreventOpen ? "Persist" : "Abandon") + "Treeview State", event: () => this.doc.treeViewPreventOpen = !this.doc.treeViewPreventOpen, icon: "paint-brush" }); layoutItems.push({ description: (this.doc.treeViewHideHeaderFields ? "Show" : "Hide") + " Header Fields", event: () => this.doc.treeViewHideHeaderFields = !this.doc.treeViewHideHeaderFields, icon: "paint-brush" }); layoutItems.push({ description: (this.doc.treeViewHideTitle ? "Show" : "Hide") + " Title", event: () => this.doc.treeViewHideTitle = !this.doc.treeViewHideTitle, icon: "paint-brush" }); layoutItems.push({ description: (this.doc.treeViewHideLinkLines ? "Show" : "Hide") + " Link Lines", event: () => this.doc.treeViewHideLinkLines = !this.doc.treeViewHideLinkLines, icon: "paint-brush" }); ContextMenu.Instance.addItem({ description: "Options...", subitems: layoutItems, icon: "eye" }); + const existingOnClick = ContextMenu.Instance.findByDescription("OnClick..."); + const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; + onClicks.push({ description: "Edit onChecked Script", event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.doc, undefined, "onCheckedClick"), "edit onCheckedClick"), icon: "edit" }); + !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", noexpand: true, subitems: onClicks, icon: "mouse-pointer" }); } - !Doc.UserDoc().noviceMode && ContextMenu.Instance.addItem({ - description: "Buxton Layout", icon: "eye", event: () => { - const { ImageDocument, PdfDocument } = Docs.Create; - const { Document } = this.props; - const fallbackImg = "http://www.cs.brown.edu/~bcz/face.gif"; - const detailView = Cast(Cast(Doc.UserDoc()["template-button-detail"], Doc, null)?.dragFactory, Doc, null); - const heroView = ImageDocument(fallbackImg, { title: "heroView", isTemplateDoc: true, isTemplateForField: "hero", }); // this acts like a template doc and a template field ... a little weird, but seems to work? - heroView.proto!.layout = ImageBox.LayoutString("hero"); - heroView._showTitle = "title"; - heroView._showTitleHover = "titlehover"; - - const fallback = ImageDocument("http://cs.brown.edu/~bcz/face.gif", { _width: 400 }); // replace with desired double click target - let pdfContent: string; - this.childDocs?.map(d => { - DocListCast(d.data).map((img, i) => { - const caption = (d.captions as any)[i]; - if (caption) { - Doc.GetProto(img).caption = caption; - Doc.GetProto(img).doubleClickView = (pdfContent = StrCast(img.additionalMedia_pdfs)) ? PdfDocument(pdfContent, { title: pdfContent }) : fallback; - } - }); - Doc.GetProto(d).type = "buxton"; - Doc.GetProto(d).proto = heroView; // all devices "are" heroViews that share the same layout & defaults. Seems better than making them all be independent and copy a layout string // .layout = ImageBox.LayoutString("hero"); - }); - - const iconBuxtonView = ImageDocument(fallbackImg, { title: "hero", _width: 60, onDoubleClick: ScriptField.MakeScript("deiconifyView(self)") }); - iconBuxtonView.isTemplateDoc = makeTemplate(iconBuxtonView, true, "icon_buxton"); - Doc.UserDoc()["template-icon-view-buxton"] = new PrefetchProxy(iconBuxtonView); - const tempIcons = Doc.GetProto(Cast(Doc.UserDoc()["template-icons"], Doc, null)); - Doc.AddDocToList(tempIcons, "data", iconBuxtonView); - - Document.childLayoutTemplate = heroView; - Document.childClickedOpenTemplateView = new PrefetchProxy(detailView); - Document._viewType = CollectionViewType.Time; - Document.forceActive = true; - Document._pivotField = "company"; - Document.childDropAction = "alias"; - } - }); - const existingOnClick = ContextMenu.Instance.findByDescription("OnClick..."); - const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; - onClicks.push({ - description: "Edit onChecked Script", event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.doc, undefined, "onCheckedClick"), "edit onCheckedClick"), icon: "edit" - }); - !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", noexpand: true, subitems: onClicks, icon: "mouse-pointer" }); } outerXf = () => Utils.GetScreenTransform(this._mainEle!); onTreeDrop = (e: React.DragEvent) => this.onExternalDrop(e, {}); @computed get renderClearButton() { - return <div id="toolbar" key="toolbar"> - <button className="toolbar-button round-button" title="Empty" - onClick={undoBatch(action(() => Doc.GetProto(this.doc)[this.props.fieldKey] = undefined))}> + return !this.doc.allowClear ? (null) : <div key="toolbar"> + <button className="toolbar-button round-button" title="Empty" onClick={undoBatch(action(() => Doc.GetProto(this.doc)[this.props.fieldKey] = undefined))}> <FontAwesomeIcon icon={"trash"} size="sm" /> </button> </div >; } - onChildClick = () => { - return this.props.onChildClick?.() || ScriptCast(this.doc.onChildClick); + @undoBatch + makeTextCollection = action((childDocs: Doc[]) => { + Doc.SetInPlace(this.doc, "editTitle", undefined, false); + const bullet = TreeView.makeTextBullet(); + bullet.context = this.doc; + this.addDoc(bullet, childDocs.length ? childDocs[0] : undefined, true); + }); + + editableTitle = (childDocs: Doc[]) => { + return !this.dataDoc ? (null) : <EditableView + contents={this.dataDoc.title} + editing={false} + display={"block"} + maxHeight={72} + height={"auto"} + GetValue={() => StrCast(this.dataDoc.title)} + SetValue={undoBatch((value: string, shift: boolean, enter: boolean) => { + if (this.props.Document.treeViewOutlineMode && enter) { + this.makeTextCollection(childDocs); + } + return Doc.SetInPlace(this.dataDoc, "title", value, false); + })} />; } - render() { + + + rtfWidth = () => Math.min(this.layoutDoc?.[WidthSym](), this.props.PanelWidth() - 20); + rtfOutlineHeight = () => Math.min(this.layoutDoc?.[HeightSym](), (StrCast(this.layoutDoc?._fontSize) ? Number(StrCast(this.layoutDoc?._fontSize, "32px").replace("px", "")) : NumCast(this.layoutDoc?._fontSize)) * 2); + titleTransform = () => this.props.ScreenToLocalTransform().translate(-NumCast(this.doc._xPadding, 10), -NumCast(this.doc._yPadding, 20)); + documentTitle = (childDocs: Doc[]) => { + return <div style={{ display: "inline-block", height: this.rtfOutlineHeight() }} key={this.doc[Id]} + onKeyDown={e => { + e.stopPropagation(); + e.key === "Enter" && this.makeTextCollection(childDocs); + }}> + <ContentFittingDocumentView + Document={this.doc} + DataDoc={undefined} + LayoutTemplateString={FormattedTextBox.LayoutString("text")} + LibraryPath={emptyPath} + renderDepth={this.props.renderDepth + 1} + rootSelected={returnTrue} + treeViewDoc={undefined} + //dontRegisterView={true} + backgroundColor={this.props.backgroundColor} + PanelWidth={this.rtfWidth} + PanelHeight={this.rtfOutlineHeight} + focus={this.props.focus} + ScreenToLocalTransform={this.titleTransform} + docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + ContainingCollectionDoc={this.doc} + ContainingCollectionView={this.props.CollectionView} + addDocument={this.props.addDocument} + moveDocument={returnFalse} + removeDocument={returnFalse} + parentActive={this.props.active} + whenActiveChanged={this.props.whenActiveChanged} + addDocTab={this.props.addDocTab} + pinToPres={this.props.pinToPres} + bringToFront={returnFalse} + ContentScaling={returnOne} + /> + </div>; + } + + onChildClick = () => this.props.onChildClick?.() || ScriptCast(this.doc.onChildClick); + whenActiveChanged = (isActive: boolean) => { this.props.whenActiveChanged(this._isChildActive = isActive); }; + active = (outsideReaction: boolean | undefined) => this.props.active(outsideReaction) || this._isChildActive; + @computed get treeChildren() { + TraceMobx(); + return this.props.overrideDocuments ? this.props.overrideDocuments : this.childDocs; + } + @computed get treeViewElements() { TraceMobx(); - if (!(this.doc instanceof Doc)) return (null); const dropAction = StrCast(this.doc.childDropAction) as dropActionType; const addDoc = (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => this.addDoc(doc, relativeTo, before); const moveDoc = (d: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => this.props.moveDocument(d, target, addDoc); - const childDocs = this.props.overrideDocuments ? this.props.overrideDocuments : this.childDocs; - return !childDocs ? (null) : ( + return TreeView.GetChildElements(this.treeChildren, this, this.doc, this.props.DataDoc, this.props.fieldKey, this.props.ContainingCollectionDoc, undefined, addDoc, this.remove, + moveDoc, dropAction, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform, + this.outerXf, this.active, this.props.PanelWidth, this.props.ChromeHeight, this.props.renderDepth, () => this.props.treeViewHideHeaderFields || BoolCast(this.doc.treeViewHideHeaderFields), + BoolCast(this.doc.treeViewPreventOpen), [], this.props.onCheckedClick, + this.onChildClick, this.props.ignoreFields, true, this.whenActiveChanged, this.props.dontRegisterView || Cast(this.props.Document.dontRegisterChildViews, "boolean", null)); + } + @computed get titleBar() { + const hideTitle = this.props.treeViewHideTitle || this.doc.treeViewHideTitle; + return hideTitle ? (null) : (this.doc.treeViewOutlineMode ? this.documentTitle : this.editableTitle)(this.treeChildren); + } + render() { + TraceMobx(); + if (!(this.doc instanceof Doc)) return (null); + const background = StrCast(this.layoutDoc._backgroundColor) || StrCast(this.layoutDoc.backgroundColor) || StrCast(this.doc.backgroundColor) || this.props.backgroundColor?.(this.doc, this.props.renderDepth); + const paddingX = `${NumCast(this.doc._xPadding, 10)}px`; + const paddingTop = `${NumCast(this.doc._yPadding, 20)}px`; + const pointerEvents = !this.props.active() && !SnappingManager.GetIsDragging() && !this._isChildActive ? "none" : undefined; + + return !this.treeChildren ? (null) : ( <div className="collectionTreeView-container" onContextMenu={this.onContextMenu}> - <div className="collectionTreeView-dropTarget" id="body" - style={{ - background: this.props.backgroundColor?.(this.doc, this.props.renderDepth), - paddingLeft: `${NumCast(this.doc._xPadding, 10)}px`, - paddingRight: `${NumCast(this.doc._xPadding, 10)}px`, - paddingTop: `${NumCast(this.doc._yPadding, 20)}px`, - pointerEvents: !this.props.active() && !SnappingManager.GetIsDragging() ? "none" : undefined - }} + <div className="collectionTreeView-dropTarget" + style={{ background, paddingLeft: paddingX, paddingRight: paddingX, paddingTop, pointerEvents }} onWheel={(e) => this._mainEle && this._mainEle.scrollHeight > this._mainEle.clientHeight && e.stopPropagation()} onDrop={this.onTreeDrop} ref={this.createTreeDropTarget}> - {this.props.treeViewHideTitle || this.doc.treeViewHideTitle ? (null) : <EditableView - contents={this.dataDoc.title} - editing={false} - display={"block"} - maxHeight={72} - height={"auto"} - GetValue={() => StrCast(this.dataDoc.title)} - SetValue={undoBatch((value: string) => Doc.SetInPlace(this.dataDoc, "title", value, false) || true)} - OnFillDown={undoBatch((value: string) => { - Doc.SetInPlace(this.dataDoc, "title", value, false); - const doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, _width: 100, _height: 25, templates: new List<string>([Templates.Title.Layout]) }); - Doc.SetInPlace(doc, "editTitle", "*", false); - this.addDoc(doc, childDocs.length ? childDocs[0] : undefined, true); - })} />} - {this.doc.allowClear ? this.renderClearButton : (null)} - <ul className="no-indent" style={{ width: "max-content" }} > - { - TreeView.GetChildElements(childDocs, this.doc, this.doc, this.props.DataDoc, this.props.fieldKey, this.props.ContainingCollectionDoc, undefined, addDoc, this.remove, - moveDoc, dropAction, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform, - this.outerXf, this.props.active, this.props.PanelWidth, this.props.ChromeHeight, this.props.renderDepth, () => this.props.treeViewHideHeaderFields || BoolCast(this.doc.treeViewHideHeaderFields), - BoolCast(this.doc.treeViewPreventOpen), [], this.props.LibraryPath, this.props.onCheckedClick, - this.onChildClick, this.props.ignoreFields) - } + {this.titleBar} + {this.renderClearButton} + <ul className="no-indent"> + {this.treeViewElements} </ul> </div > </div> ); } -} - -Scripting.addGlobal(function readFacetData(layoutDoc: Doc, dataDoc: Doc, dataKey: string, facetHeader: string) { - const allCollectionDocs = DocListCast(dataDoc[dataKey]); - const facetValues = Array.from(allCollectionDocs.reduce((set, child) => - set.add(Field.toString(child[facetHeader] as Field)), new Set<string>())); - - let nonNumbers = 0; - facetValues.map(val => { - const num = Number(val); - if (Number.isNaN(num)) { - nonNumbers++; - } - }); - const facetValueDocSet = (nonNumbers / facetValues.length > .1 ? facetValues.sort() : facetValues.sort((n1: string, n2: string) => Number(n1) - Number(n2))).map(facetValue => { - const doc = new Doc(); - doc.system = true; - doc.title = facetValue.toString(); - doc.treeViewChecked = ComputedField.MakeFunction("determineCheckedState(layoutDoc, facetHeader, facetValue)", {}, { layoutDoc, facetHeader, facetValue }); - return doc; - }); - return new List<Doc>(facetValueDocSet); -}); - -Scripting.addGlobal(function determineCheckedState(layoutDoc: Doc, facetHeader: string, facetValue: string) { - const docFilters = Cast(layoutDoc._docFilters, listSpec("string"), []); - for (let i = 0; i < docFilters.length; i += 3) { - const [header, value, state] = docFilters.slice(i, i + 3); - if (header === facetHeader && value === facetValue) { - return state; - } - } - return undefined; -});
\ No newline at end of file +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 2064b0be4..522f46280 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -1,24 +1,17 @@ -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faEdit, faEye } from '@fortawesome/free-regular-svg-icons'; -import { faColumns, faCopy, faEllipsisV, faFingerprint, faGlobeAmericas, faImage, faProjectDiagram, faSignature, faSquare, faTh, faThList, faTree } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, computed, observable } from 'mobx'; import { observer } from "mobx-react"; import * as React from 'react'; import Lightbox from 'react-image-lightbox-with-rotate'; import 'react-image-lightbox-with-rotate/style.css'; // This only needs to be imported once in your app import { DateField } from '../../../fields/DateField'; -import { AclAddonly, AclReadonly, DataSym, Doc, DocListCast, Field, Opt, AclEdit, AclSym, AclPrivate, AclAdmin } from '../../../fields/Doc'; +import { AclAddonly, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, DataSym, Doc, DocListCast, Field } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { ObjectField } from '../../../fields/ObjectField'; -import { RichTextField } from '../../../fields/RichTextField'; -import { listSpec } from '../../../fields/Schema'; -import { ComputedField, ScriptField } from '../../../fields/ScriptField'; -import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; +import { BoolCast, Cast, ScriptCast, StrCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; -import { TraceMobx, GetEffectiveAcl, SharingPermissions, distributeAcls } from '../../../fields/util'; -import { emptyFunction, emptyPath, returnEmptyFilter, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils'; +import { distributeAcls, GetEffectiveAcl, SharingPermissions, TraceMobx } from '../../../fields/util'; +import { returnFalse, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; import { CurrentUserUtils } from '../../util/CurrentUserUtils'; @@ -26,6 +19,7 @@ import { ImageUtils } from '../../util/Import & Export/ImageUtils'; import { InteractionUtils } from '../../util/InteractionUtils'; import { UndoManager } from '../../util/UndoManager'; import { ContextMenu } from "../ContextMenu"; +import { ContextMenuProps } from '../ContextMenuItem'; import { FieldView, FieldViewProps } from '../nodes/FieldView'; import { Touchable } from '../Touchable'; import { CollectionCarousel3DView } from './CollectionCarousel3DView'; @@ -40,21 +34,16 @@ import { CollectionMultirowView } from './collectionMulticolumn/CollectionMultir import { CollectionPileView } from './CollectionPileView'; import { CollectionSchemaView } from "./CollectionSchemaView"; import { CollectionStackingView } from './CollectionStackingView'; -import { CollectionStaffView } from './CollectionStaffView'; import { SubCollectionViewProps } from './CollectionSubView'; import { CollectionTimeView } from './CollectionTimeView'; import { CollectionTreeView } from "./CollectionTreeView"; import './CollectionView.scss'; -import { ContextMenuProps } from '../ContextMenuItem'; -import { table } from 'console'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; export const COLLECTION_BORDER_WIDTH = 2; const path = require('path'); -library.add(faTh, faTree, faSquare, faProjectDiagram, faSignature, faThList, faFingerprint, faColumns, faGlobeAmericas, faEllipsisV, faImage, faEye as any, faCopy); - export enum CollectionViewType { Invalid = "invalid", Freeform = "freeform", @@ -76,10 +65,9 @@ export enum CollectionViewType { } export interface CollectionViewCustomProps { filterAddDocument?: (doc: Doc | Doc[]) => boolean; // allows a document that renders a Collection view to filter or modify any documents added to the collection (see PresBox for an example) - childLayoutTemplate?: () => Opt<Doc>; // specify a layout Doc template to use for children of the collection - childLayoutString?: string; // specify a layout string to use for children of the collection childOpacity?: () => number; hideFilter?: true; + childIgnoreNativeSize?: boolean; } export interface CollectionRenderProps { @@ -90,8 +78,8 @@ export interface CollectionRenderProps { whenActiveChanged: (isActive: boolean) => void; PanelWidth: () => number; PanelHeight: () => number; - ChildLayoutTemplate?: () => Doc; - ChildLayoutString?: string; + ChildLayoutTemplate?: () => Doc;// specify a layout Doc template to use for children of the collection + ChildLayoutString?: string;// specify a layout string to use for children of the collection } @observer @@ -99,8 +87,8 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus public static LayoutString(fieldStr: string) { return FieldView.LayoutString(CollectionView, fieldStr); } _isChildActive = false; //TODO should this be observable? - get _isLightboxOpen() { return BoolCast(this.props.Document.isLightboxOpen); } - set _isLightboxOpen(value) { this.props.Document.isLightboxOpen = value; } + get _isLightboxOpen() { return BoolCast(this.props.Document._isLightboxOpen); } + set _isLightboxOpen(value) { this.props.Document._isLightboxOpen = value; } @observable private _curLightboxImg = 0; @observable private static _safeMode = false; public static SetSafeMode(safeMode: boolean) { this._safeMode = safeMode; } @@ -156,13 +144,17 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus added.forEach(d => { for (const [key, value] of Object.entries(this.props.Document[AclSym])) { if (d.author === key.substring(4).replace("_", ".") && !d.aliasOf) distributeAcls(key, SharingPermissions.Admin, d, true); - else distributeAcls(key, this.AclMap.get(value) as SharingPermissions, d, true); + //else if (this.props.Document[key] === SharingPermissions.Admin) distributeAcls(key, SharingPermissions.Add, d, true); + //else distributeAcls(key, this.AclMap.get(value) as SharingPermissions, d, true); } }); } if (effectiveAcl === AclAddonly) { - added.map(doc => Doc.AddDocToList(targetDataDoc, this.props.fieldKey, doc)); + added.map(doc => { + Doc.AddDocToList(targetDataDoc, this.props.fieldKey, doc); + doc.context = this.props.Document; + }); } else { added.map(doc => { @@ -183,12 +175,8 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus doc._stayInCollection = undefined; doc.context = this.props.Document; }); - added.map(add => Doc.AddDocToList(Cast(Doc.UserDoc().myCatalog, Doc, null), "data", add)); - // targetDataDoc[this.props.fieldKey] = new List<Doc>([...docList, ...added]); (targetDataDoc[this.props.fieldKey] as List<Doc>).push(...added); targetDataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now())); - const lastModified = "lastModified"; - targetDataDoc[lastModified] = new DateField(new Date(Date.now())); } } } @@ -198,16 +186,21 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus @action.bound removeDocument = (doc: any): boolean => { const effectiveAcl = GetEffectiveAcl(this.props.Document[DataSym]); - if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) { + const docAcl = GetEffectiveAcl(doc); + if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin || docAcl === AclAdmin) { const docs = doc instanceof Doc ? [doc] : doc as Doc[]; const targetDataDoc = this.props.Document[DataSym]; const value = DocListCast(targetDataDoc[this.props.fieldKey]); const toRemove = value.filter(v => docs.includes(v)); if (toRemove.length !== 0) { - const recent = Cast(Doc.UserDoc().myRecentlyClosed, Doc) as Doc; + const recent = Cast(Doc.UserDoc().myRecentlyClosedDocs, Doc) as Doc; toRemove.forEach(doc => { - Doc.RemoveDocFromList(targetDataDoc, this.props.fieldKey, doc); - recent && Doc.AddDocToList(recent, "data", doc, undefined, true, true); + const ind = (targetDataDoc[this.props.fieldKey] as List<Doc>).indexOf(doc); + (targetDataDoc[this.props.fieldKey] as List<Doc>).splice(ind, 0); + if (ind !== -1) { + Doc.RemoveDocFromList(targetDataDoc, this.props.fieldKey, doc); + recent && Doc.AddDocToList(recent, "data", doc, undefined, true, true); + } }); return true; } @@ -250,7 +243,8 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus } screenToLocalTransform = () => this.props.renderDepth ? this.props.ScreenToLocalTransform() : this.props.ScreenToLocalTransform().scale(this.props.PanelWidth() / this.bodyPanelWidth()); - private SubViewHelper = (type: CollectionViewType, renderProps: CollectionRenderProps) => { + private SubView = (type: CollectionViewType, renderProps: CollectionRenderProps) => { + TraceMobx(); const props: SubCollectionViewProps = { ...this.props, ...renderProps, ScreenToLocalTransform: this.screenToLocalTransform, CollectionView: this, annotationsKey: "" }; switch (type) { case CollectionViewType.Schema: return (<CollectionSchemaView key="collview" {...props} />); @@ -269,15 +263,10 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus case CollectionViewType.Map: return (<CollectionMapView key="collview" {...props} />); case CollectionViewType.Grid: return (<CollectionGridView key="gridview" {...props} />); case CollectionViewType.Freeform: - default: { this.props.Document._freeformLayoutEngine = undefined; return (<CollectionFreeFormView key="collview" {...props} />); } + default: { this.props.Document._freeformLayoutEngine = undefined; return (<CollectionFreeFormView key="collview" {...props} ChildLayoutString={props.ChildLayoutString} />); } } } - private SubView = (type: CollectionViewType, renderProps: CollectionRenderProps) => { - return this.SubViewHelper(type, renderProps); - } - - setupViewTypes(category: string, func: (viewType: CollectionViewType) => Doc, addExtras: boolean) { const subItems: ContextMenuProps[] = []; @@ -287,23 +276,24 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus } subItems.push({ description: "Schema", event: () => func(CollectionViewType.Schema), icon: "th-list" }); subItems.push({ description: "Tree", event: () => func(CollectionViewType.Tree), icon: "tree" }); - subItems.push({ description: "Stacking", event: () => func(CollectionViewType.Stacking), icon: "ellipsis-v" }); - subItems.push({ description: "Stacking (AutoHeight)", event: () => func(CollectionViewType.Stacking)._autoHeight = true, icon: "ellipsis-v" }); - subItems.push({ description: "Staff", event: () => func(CollectionViewType.Staff), icon: "music" }); + !Doc.UserDoc().noviceMode && subItems.push({ description: "Stacking", event: () => func(CollectionViewType.Stacking), icon: "ellipsis-v" }); + subItems.push({ description: "Stacking", event: () => func(CollectionViewType.Stacking)._autoHeight = true, icon: "ellipsis-v" }); subItems.push({ description: "Multicolumn", event: () => func(CollectionViewType.Multicolumn), icon: "columns" }); subItems.push({ description: "Multirow", event: () => func(CollectionViewType.Multirow), icon: "columns" }); subItems.push({ description: "Masonry", event: () => func(CollectionViewType.Masonry), icon: "columns" }); subItems.push({ description: "Carousel", event: () => func(CollectionViewType.Carousel), icon: "columns" }); subItems.push({ description: "3D Carousel", event: () => func(CollectionViewType.Carousel3D), icon: "columns" }); - subItems.push({ description: "Pivot/Time", event: () => func(CollectionViewType.Time), icon: "columns" }); - subItems.push({ description: "Map", event: () => func(CollectionViewType.Map), icon: "globe-americas" }); + !Doc.UserDoc().noviceMode && subItems.push({ description: "Pivot/Time", event: () => func(CollectionViewType.Time), icon: "columns" }); + !Doc.UserDoc().noviceMode && subItems.push({ description: "Map", event: () => func(CollectionViewType.Map), icon: "globe-americas" }); subItems.push({ description: "Grid", event: () => func(CollectionViewType.Grid), icon: "th-list" }); - addExtras && subItems.push({ description: "lightbox", event: action(() => this._isLightboxOpen = true), icon: "eye" }); + subItems.push({ description: "lightbox", event: action(() => this._isLightboxOpen = true), icon: "eye" }); - const existingVm = ContextMenu.Instance.findByDescription(category); - const catItems = existingVm && "subitems" in existingVm ? existingVm.subitems : []; - catItems.push({ description: "Add a Perspective...", addDivider: true, noexpand: true, subitems: subItems, icon: "eye" }); - !existingVm && ContextMenu.Instance.addItem({ description: category, subitems: catItems, icon: "eye" }); + if (!Doc.IsSystem(this.props.Document) && !this.props.Document.annotationOn) { + const existingVm = ContextMenu.Instance.findByDescription(category); + const catItems = existingVm && "subitems" in existingVm ? existingVm.subitems : []; + catItems.push({ description: "Add a Perspective...", addDivider: true, noexpand: true, subitems: subItems, icon: "eye" }); + !existingVm && ContextMenu.Instance.addItem({ description: category, subitems: catItems, icon: "eye" }); + } } onContextMenu = (e: React.MouseEvent): void => { @@ -312,7 +302,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus this.setupViewTypes("UI Controls...", vtype => { const newRendition = Doc.MakeAlias(this.props.Document); newRendition._viewType = vtype; - this.props.addDocTab(newRendition, "onRight"); + this.props.addDocTab(newRendition, "add:right"); return newRendition; }, false); @@ -320,34 +310,34 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus const optionItems = options && "subitems" in options ? options.subitems : []; !Doc.UserDoc().noviceMode ? optionItems.splice(0, 0, { description: `${this.props.Document.forceActive ? "Select" : "Force"} Contents Active`, event: () => this.props.Document.forceActive = !this.props.Document.forceActive, icon: "project-diagram" }) : null; if (this.props.Document.childLayout instanceof Doc) { - optionItems.push({ description: "View Child Layout", event: () => this.props.addDocTab(this.props.Document.childLayout as Doc, "onRight"), icon: "project-diagram" }); + optionItems.push({ description: "View Child Layout", event: () => this.props.addDocTab(this.props.Document.childLayout as Doc, "add:right"), icon: "project-diagram" }); } if (this.props.Document.childClickedOpenTemplateView instanceof Doc) { - optionItems.push({ description: "View Child Detailed Layout", event: () => this.props.addDocTab(this.props.Document.childClickedOpenTemplateView as Doc, "onRight"), icon: "project-diagram" }); + optionItems.push({ description: "View Child Detailed Layout", event: () => this.props.addDocTab(this.props.Document.childClickedOpenTemplateView as Doc, "add:right"), icon: "project-diagram" }); } !Doc.UserDoc().noviceMode && optionItems.push({ description: `${this.props.Document.isInPlaceContainer ? "Unset" : "Set"} inPlace Container`, event: () => this.props.Document.isInPlaceContainer = !this.props.Document.isInPlaceContainer, icon: "project-diagram" }); !options && cm.addItem({ description: "Options...", subitems: optionItems, icon: "hand-point-right" }); - const existingOnClick = cm.findByDescription("OnClick..."); - const onClicks = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; - const funcs = [ - { key: "onChildClick", name: "On Child Clicked" }, - { key: "onChildDoubleClick", name: "On Child Double Clicked" }]; - funcs.map(func => onClicks.push({ - description: `Edit ${func.name} script`, icon: "edit", event: (obj: any) => { - const alias = Doc.MakeAlias(this.props.Document); - DocUtils.makeCustomViewClicked(alias, undefined, func.key); - this.props.addDocTab(alias, "onRight"); - } - })); - DocListCast(Cast(Doc.UserDoc()["clickFuncs-child"], Doc, null).data).forEach(childClick => - onClicks.push({ - description: `Set child ${childClick.title}`, - icon: "edit", - event: () => Doc.GetProto(this.props.Document)[StrCast(childClick.targetScriptKey)] = ObjectField.MakeCopy(ScriptCast(childClick.data)), + if (!Doc.UserDoc().noviceMode && !this.props.Document.annotationOn) { + const existingOnClick = cm.findByDescription("OnClick..."); + const onClicks = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; + const funcs = [{ key: "onChildClick", name: "On Child Clicked" }, { key: "onChildDoubleClick", name: "On Child Double Clicked" }]; + funcs.map(func => onClicks.push({ + description: `Edit ${func.name} script`, icon: "edit", event: (obj: any) => { + const alias = Doc.MakeAlias(this.props.Document); + DocUtils.makeCustomViewClicked(alias, undefined, func.key); + this.props.addDocTab(alias, "add:right"); + } })); - !existingOnClick && cm.addItem({ description: "OnClick...", noexpand: true, subitems: onClicks, icon: "mouse-pointer" }); + DocListCast(Cast(Doc.UserDoc()["clickFuncs-child"], Doc, null).data).forEach(childClick => + onClicks.push({ + description: `Set child ${childClick.title}`, + icon: "edit", + event: () => Doc.GetProto(this.props.Document)[StrCast(childClick.targetScriptKey)] = ObjectField.MakeCopy(ScriptCast(childClick.data)), + })); + !Doc.IsSystem(this.props.Document) && !existingOnClick && cm.addItem({ description: "OnClick...", noexpand: true, subitems: onClicks, icon: "mouse-pointer" }); + } if (!Doc.UserDoc().noviceMode) { const more = cm.findByDescription("More..."); @@ -358,222 +348,31 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus } } - lightbox = (images: string[]) => { + lightbox = (images: { image: string, title: string, caption: string }[]) => { if (!images.length) return (null); - const mainPath = path.extname(images[this._curLightboxImg]); - const nextPath = path.extname(images[(this._curLightboxImg + 1) % images.length]); - const prevPath = path.extname(images[(this._curLightboxImg + images.length - 1) % images.length]); - const main = images[this._curLightboxImg].replace(mainPath, "_o" + mainPath); - const next = images[(this._curLightboxImg + 1) % images.length].replace(nextPath, "_o" + nextPath); - const prev = images[(this._curLightboxImg + images.length - 1) % images.length].replace(prevPath, "_o" + prevPath); + const mainPath = path.extname(images[this._curLightboxImg].image); + const nextPath = path.extname(images[(this._curLightboxImg + 1) % images.length].image); + const prevPath = path.extname(images[(this._curLightboxImg + images.length - 1) % images.length].image); + const main = images[this._curLightboxImg].image.replace(mainPath, "_o" + mainPath); + const title = images[this._curLightboxImg].title; + const caption = images[this._curLightboxImg].caption; + const next = images[(this._curLightboxImg + 1) % images.length].image.replace(nextPath, "_o" + nextPath); + const prev = images[(this._curLightboxImg + images.length - 1) % images.length].image.replace(prevPath, "_o" + prevPath); return !this._isLightboxOpen ? (null) : (<Lightbox key="lightbox" mainSrc={main} nextSrc={next} prevSrc={prev} + imageTitle={title} + imageCaption={caption} onCloseRequest={action(() => this._isLightboxOpen = false)} onMovePrevRequest={action(() => this._curLightboxImg = (this._curLightboxImg + images.length - 1) % images.length)} onMoveNextRequest={action(() => this._curLightboxImg = (this._curLightboxImg + 1) % images.length)} />); } - get _facetWidth() { return NumCast(this.props.Document._facetWidth); } - set _facetWidth(value) { this.props.Document._facetWidth = value; } bodyPanelWidth = () => this.props.PanelWidth(); - facetWidth = () => Math.max(0, Math.min(this.props.PanelWidth() - 25, this._facetWidth)); - - @computed get dataDoc() { - return (this.props.DataDoc && this.props.Document.isTemplateForField ? Doc.GetProto(this.props.DataDoc) : - this.props.Document.resolvedDataDoc ? this.props.Document : Doc.GetProto(this.props.Document)); // if the layout document has a resolvedDataDoc, then we don't want to get its parent which would be the unexpanded template - } - // The data field for rendering this collection will be on the this.props.Document unless we're rendering a template in which case we try to use props.DataDoc. - // When a document has a DataDoc but it's not a template, then it contains its own rendering data, but needs to pass the DataDoc through - // to its children which may be templates. - // If 'annotationField' is specified, then all children exist on that field of the extension document, otherwise, they exist directly on the data document under 'fieldKey' - @computed get dataField() { - return this.dataDoc[this.props.fieldKey]; - } - - get childLayoutPairs(): { layout: Doc; data: Doc; }[] { - const { Document, DataDoc } = this.props; - const validPairs = this.childDocs.map(doc => Doc.GetLayoutDataDocPair(Document, DataDoc, doc)).filter(pair => pair.layout); - return validPairs.map(({ data, layout }) => ({ data: data as Doc, layout: layout! })); // this mapping is a bit of a hack to coerce types - } - get childDocList() { - return Cast(this.dataField, listSpec(Doc)); - } - get childDocs() { - const dfield = this.dataField; - const rawdocs = (dfield instanceof Doc) ? [dfield] : Cast(dfield, listSpec(Doc), Cast(this.props.Document.rootDocument, Doc, null) ? [Cast(this.props.Document.rootDocument, Doc, null)] : []); - const docs = rawdocs.filter(d => d && !(d instanceof Promise)).map(d => d as Doc); - const viewSpecScript = ScriptCast(this.props.Document.viewSpecScript); - return viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs; - } - @computed get _allFacets() { - TraceMobx(); - const facets = new Set<string>(["type", "text", "data", "author", "ACL"]); - this.childDocs.filter(child => child).forEach(child => child && Object.keys(Doc.GetProto(child)).forEach(key => facets.add(key))); - Doc.AreProtosEqual(this.dataDoc, this.props.Document) && this.childDocs.filter(child => child).forEach(child => Object.keys(child).forEach(key => facets.add(key))); - return Array.from(facets).filter(f => !f.startsWith("_") && !["proto", "zIndex", "isPrototype", "context", "text-noTemplate"].includes(f)).sort(); - } - - /** - * Responds to clicking the check box in the flyout menu - */ - facetClick = (facetHeader: string) => { - const facetCollection = this.props.Document; - const found = DocListCast(facetCollection[this.props.fieldKey + "-filter"]).findIndex(doc => doc.title === facetHeader); - if (found !== -1) { - (facetCollection[this.props.fieldKey + "-filter"] as List<Doc>).splice(found, 1); - const docFilter = Cast(this.props.Document._docFilters, listSpec("string")); - if (docFilter) { - let index: number; - while ((index = docFilter.findIndex(item => item === facetHeader)) !== -1) { - docFilter.splice(index, 3); - } - } - const docRangeFilters = Cast(this.props.Document._docRangeFilters, listSpec("string")); - if (docRangeFilters) { - let index: number; - while ((index = docRangeFilters.findIndex(item => item === facetHeader)) !== -1) { - docRangeFilters.splice(index, 3); - } - } - } else { - const allCollectionDocs = DocListCast(this.dataDoc[this.props.fieldKey]); - var rtfields = 0; - const facetValues = Array.from(allCollectionDocs.reduce((set, child) => { - const field = child[facetHeader] as Field; - const fieldStr = Field.toString(field); - if (field instanceof RichTextField || (typeof (field) === "string" && fieldStr.split(" ").length > 2)) rtfields++; - return set.add(fieldStr); - }, new Set<string>())); - - let nonNumbers = 0; - let minVal = Number.MAX_VALUE, maxVal = -Number.MAX_VALUE; - facetValues.map(val => { - const num = Number(val); - if (Number.isNaN(num)) { - nonNumbers++; - } else { - minVal = Math.min(num, minVal); - maxVal = Math.max(num, maxVal); - } - }); - let newFacet: Opt<Doc>; - if (facetHeader === "text" || rtfields / allCollectionDocs.length > 0.1) { - newFacet = Docs.Create.TextDocument("", { _width: 100, _height: 25, treeViewExpandedView: "layout", title: facetHeader, treeViewOpen: true, forceActive: true, ignoreClick: true }); - Doc.GetProto(newFacet).type = DocumentType.COL; // forces item to show an open/close button instead ofa checkbox - newFacet.target = this.props.Document; - newFacet._textBoxPadding = 4; - const scriptText = `setDocFilter(this.target, "${facetHeader}", text, "match")`; - newFacet.onTextChanged = ScriptField.MakeScript(scriptText, { this: Doc.name, text: "string" }); - } else if (nonNumbers / facetValues.length < .1) { - newFacet = Docs.Create.SliderDocument({ title: facetHeader, treeViewExpandedView: "layout", treeViewOpen: true }); - const newFacetField = Doc.LayoutFieldKey(newFacet); - const ranged = Doc.readDocRangeFilter(this.props.Document, facetHeader); - Doc.GetProto(newFacet).type = DocumentType.COL; // forces item to show an open/close button instead ofa checkbox - const extendedMinVal = minVal - Math.min(1, Math.abs(maxVal - minVal) * .05); - const extendedMaxVal = maxVal + Math.min(1, Math.abs(maxVal - minVal) * .05); - newFacet[newFacetField + "-min"] = ranged === undefined ? extendedMinVal : ranged[0]; - newFacet[newFacetField + "-max"] = ranged === undefined ? extendedMaxVal : ranged[1]; - Doc.GetProto(newFacet)[newFacetField + "-minThumb"] = extendedMinVal; - Doc.GetProto(newFacet)[newFacetField + "-maxThumb"] = extendedMaxVal; - newFacet.target = this.props.Document; - const scriptText = `setDocFilterRange(this.target, "${facetHeader}", range)`; - newFacet.onThumbChanged = ScriptField.MakeScript(scriptText, { this: Doc.name, range: "number" }); - Doc.AddDocToList(facetCollection, this.props.fieldKey + "-filter", newFacet); - } else { - newFacet = new Doc(); - newFacet.sytem = true; - newFacet.title = facetHeader; - newFacet.treeViewOpen = true; - newFacet.type = DocumentType.COL; - const capturedVariables = { layoutDoc: this.props.Document, dataDoc: this.dataDoc }; - newFacet.data = ComputedField.MakeFunction(`readFacetData(layoutDoc, dataDoc, "${this.props.fieldKey}", "${facetHeader}")`, {}, capturedVariables); - } - newFacet && Doc.AddDocToList(facetCollection, this.props.fieldKey + "-filter", newFacet); - } - } - - onPointerDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => { - this._facetWidth = this.props.PanelWidth() - Math.max(this.props.ScreenToLocalTransform().transformPoint(e.clientX, 0)[0], 0); - return false; - }), returnFalse, action(() => this._facetWidth = this.facetWidth() < 15 ? Math.min(this.props.PanelWidth() - 25, 200) : 0), false); - } - - filterBackground = () => "rgba(105, 105, 105, 0.432)"; - get ignoreFields() { return ["_docFilters", "_docRangeFilters"]; } // this makes the tree view collection ignore these filters (otherwise, the filters would filter themselves) - @computed get scriptField() { - const scriptText = "setDocFilter(containingTreeView, heading, this.title, checked)"; - const script = ScriptField.MakeScript(scriptText, { this: Doc.name, heading: "string", checked: "string", containingTreeView: Doc.name }); - return script ? () => script : undefined; - } - @computed get filterView() { - TraceMobx(); - const facetCollection = this.props.Document; - const flyout = ( - <div className="collectionTimeView-flyout" style={{ width: `${this.facetWidth()}`, height: this.props.PanelHeight() - 30 }} onWheel={e => e.stopPropagation()}> - {this._allFacets.map(facet => <label className="collectionTimeView-flyout-item" key={`${facet}`} onClick={e => this.facetClick(facet)}> - <input type="checkbox" onChange={e => { }} checked={DocListCast(this.props.Document[this.props.fieldKey + "-filter"]).some(d => d.title === facet)} /> - <span className="checkmark" /> - {facet} - </label>)} - </div> - ); - - return !this._facetWidth || this.props.dontRegisterView ? (null) : <div className="collectionTimeView-treeView" style={{ width: `${this.facetWidth()}px`, overflow: this.facetWidth() < 15 ? "hidden" : undefined }}> - <div className="collectionTimeView-addFacet" style={{ width: `${this.facetWidth()}px` }} onPointerDown={e => e.stopPropagation()}> - <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout}> - <div className="collectionTimeView-button"> - <FontAwesomeIcon icon={faEdit} size={"lg"} /> - <span className="collectionTimeView-span">Facet Filters</span> - </div> - </Flyout> - </div> - <div className="collectionTimeView-tree" key="tree"> - <CollectionTreeView - PanelPosition={""} - Document={facetCollection} - DataDoc={facetCollection} - fieldKey={`${this.props.fieldKey}-filter`} - CollectionView={this} - docFilters={returnEmptyFilter} - ContainingCollectionDoc={this.props.ContainingCollectionDoc} - ContainingCollectionView={this.props.ContainingCollectionView} - PanelWidth={this.facetWidth} - PanelHeight={this.props.PanelHeight} - NativeHeight={returnZero} - NativeWidth={returnZero} - LibraryPath={emptyPath} - rootSelected={this.props.rootSelected} - renderDepth={1} - dropAction={this.props.dropAction} - ScreenToLocalTransform={this.props.ScreenToLocalTransform} - addDocTab={returnFalse} - pinToPres={returnFalse} - isSelected={returnFalse} - select={returnFalse} - bringToFront={emptyFunction} - active={this.props.active} - whenActiveChanged={returnFalse} - treeViewHideTitle={true} - ContentScaling={returnOne} - focus={returnFalse} - treeViewHideHeaderFields={true} - onCheckedClick={this.scriptField} - ignoreFields={this.ignoreFields} - annotationsKey={""} - dontRegisterView={true} - backgroundColor={this.filterBackground} - moveDocument={returnFalse} - removeDocument={returnFalse} - addDocument={returnFalse} /> - </div> - </div>; - } childLayoutTemplate = () => this.props.childLayoutTemplate?.() || Cast(this.props.Document.childLayoutTemplate, Doc, null); - childLayoutString = this.props.childLayoutString || StrCast(this.props.Document.childLayoutString); + @computed get childLayoutString() { return StrCast(this.props.Document.childLayoutString); } render() { TraceMobx(); @@ -588,25 +387,19 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus ChildLayoutTemplate: this.childLayoutTemplate, ChildLayoutString: this.childLayoutString, }; - const boxShadow = Doc.UserDoc().renderStyle === "comic" || this.props.Document.isBackground || this.collectionViewType === CollectionViewType.Linear ? undefined : - `${Cast(Doc.UserDoc().activeWorkspace, Doc, null)?.darkScheme ? "rgb(30, 32, 31) " : "#9c9396 "} ${StrCast(this.props.Document.boxShadow, "0.2vw 0.2vw 0.8vw")}`; + const boxShadow = Doc.UserDoc().renderStyle === "comic" || this.props.Document.treeViewOutlineMode || this.props.Document._isBackground || this.collectionViewType === CollectionViewType.Linear ? undefined : + `${CurrentUserUtils.ActiveDashboard?.darkScheme ? "rgb(30, 32, 31) " : "#9c9396 "} ${StrCast(this.props.Document.boxShadow, "0.2vw 0.2vw 0.8vw")}`; return (<div className={"collectionView"} onContextMenu={this.onContextMenu} - style={{ pointerEvents: this.props.Document.isBackground ? "none" : undefined, boxShadow }}> + style={{ pointerEvents: this.props.Document._isBackground ? "none" : undefined, boxShadow }}> {this.showIsTagged()} - <div className="collectionView-facetCont" style={{ display: this.props.PanelPosition === "absolute" ? "flex" : "", justifyContent: this.props.PanelPosition === "absolute" ? "center" : "", width: `calc(100% - ${this.facetWidth()}px)` }}> - {this.collectionViewType !== undefined ? this.SubView(this.collectionViewType, props) : (null)} - </div> - {this.lightbox(DocListCast(this.props.Document[this.props.fieldKey]).filter(d => d.type === DocumentType.IMG).map(d => - Cast(d.data, ImageField) ? - (Cast(d.data, ImageField)!.url.href.indexOf(window.location.origin) === -1) ? - Utils.CorsProxy(Cast(d.data, ImageField)!.url.href) : Cast(d.data, ImageField)!.url.href - : - ""))} - {(Doc.UserDoc()?.noviceMode || !this.props.isSelected() && !this.props.Document.forceActive) || this.props.Document.hideFilterView ? (null) : - <div className="collectionView-filterDragger" title="library View Dragger" onPointerDown={this.onPointerDown} - style={{ right: this.facetWidth() - 1, top: this.props.Document._viewType === CollectionViewType.Docking ? "25%" : "60%" }} /> - } - {Doc.UserDoc()?.noviceMode || this.facetWidth() < 10 ? (null) : this.filterView} + {this.collectionViewType !== undefined ? this.SubView(this.collectionViewType, props) : (null)} + {this.lightbox(DocListCast(this.props.Document[this.props.fieldKey]).filter(d => Cast(d.data, ImageField, null)).map(d => + ({ + image: (Cast(d.data, ImageField)!.url.href.indexOf(window.location.origin) === -1) ? + Utils.CorsProxy(Cast(d.data, ImageField)!.url.href) : Cast(d.data, ImageField)!.url.href, + title: StrCast(d.title), + caption: Field.toString(d.caption as Field) + })))} </div>); } } diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx deleted file mode 100644 index 149d4927b..000000000 --- a/src/client/views/collections/ParentDocumentSelector.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import * as React from "react"; -import './ParentDocumentSelector.scss'; -import { Doc } from "../../../fields/Doc"; -import { observer } from "mobx-react"; -import { observable, action, runInAction, trace, computed, reaction, IReactionDisposer } from "mobx"; -import { Id } from "../../../fields/FieldSymbols"; -import { SearchUtil } from "../../util/SearchUtil"; -import { CollectionDockingView } from "./CollectionDockingView"; -import { NumCast, StrCast } from "../../../fields/Types"; -import { CollectionViewType } from "./CollectionView"; -import { DocumentButtonBar } from "../DocumentButtonBar"; -import { DocumentManager } from "../../util/DocumentManager"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faCog, faChevronCircleUp } from "@fortawesome/free-solid-svg-icons"; -import { library } from "@fortawesome/fontawesome-svg-core"; -import { DocumentView } from "../nodes/DocumentView"; -import { SelectionManager } from "../../util/SelectionManager"; -import { Tooltip } from "@material-ui/core"; -const higflyout = require("@hig/flyout"); -export const { anchorPoints } = higflyout; -export const Flyout = higflyout.default; - -library.add(faCog); - -type SelectorProps = { - Document: Doc, - Stack?: any, - addDocTab(doc: Doc, location: string): void -}; - -@observer -export class SelectorContextMenu extends React.Component<SelectorProps> { - @observable private _docs: { col: Doc, target: Doc }[] = []; - @observable private _otherDocs: { col: Doc, target: Doc }[] = []; - _reaction: IReactionDisposer | undefined; - - componentDidMount() { - this._reaction = reaction(() => this.props.Document, () => this.fetchDocuments(), { fireImmediately: true }); - } - componentWillUnmount() { - this._reaction?.(); - } - async fetchDocuments() { - const aliases = (await SearchUtil.GetAliasesOfDocument(this.props.Document)); - const containerProtoSets = await Promise.all(aliases.map(async alias => - ((await SearchUtil.Search("", true, { fq: `data_l:"${alias[Id]}"` })).docs))); - const containerProtos = containerProtoSets.reduce((p, set) => { set.map(s => p.add(s)); return p; }, new Set<Doc>()); - const containerSets = await Promise.all(Array.from(containerProtos.keys()).map(async container => { - return (SearchUtil.GetAliasesOfDocument(container)); - })); - const containers = containerSets.reduce((p, set) => { set.map(s => p.add(s)); return p; }, new Set<Doc>()); - const doclayoutSets = await Promise.all(Array.from(containers.keys()).map(async (dp) => { - return (SearchUtil.GetAliasesOfDocument(dp)); - })); - const doclayouts = Array.from(doclayoutSets.reduce((p, set) => { set.map(s => p.add(s)); return p; }, new Set<Doc>()).keys()); - runInAction(() => { - this._docs = doclayouts.filter(doc => !Doc.AreProtosEqual(doc, CollectionDockingView.Instance.props.Document)).map(doc => ({ col: doc, target: this.props.Document })); - this._otherDocs = []; - }); - } - - getOnClick({ col, target }: { col: Doc, target: Doc }) { - return () => { - col = Doc.IsPrototype(col) ? Doc.MakeDelegate(col) : col; - if (col._viewType === CollectionViewType.Freeform) { - const newPanX = NumCast(target.x) + NumCast(target._width) / 2; - const newPanY = NumCast(target.y) + NumCast(target._height) / 2; - col._panX = newPanX; - col._panY = newPanY; - } - this.props.addDocTab(col, "inTab"); // bcz: dataDoc? - }; - } - - render() { - return <div > - <p key="contexts">Contexts:</p> - {this._docs.map(doc => <p key={doc.col[Id] + doc.target[Id]}><a onClick={this.getOnClick(doc)}>{doc.col.title?.toString()}</a></p>)} - {this._otherDocs.length ? <hr key="hr" /> : null} - {this._otherDocs.map(doc => <p key={"p" + doc.col[Id] + doc.target[Id]}><a onClick={this.getOnClick(doc)}>{doc.col.title?.toString()}</a></p>)} - </div>; - } -} - -@observer -export class ParentDocSelector extends React.Component<SelectorProps> { - render() { - const flyout = ( - <div className="parentDocumentSelector-flyout" title=" "> - <SelectorContextMenu {...this.props} /> - </div> - ); - return <div title="Show Contexts" onPointerDown={e => e.stopPropagation()} className="parentDocumentSelector-linkFlyout"> - <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout}> - <span className="parentDocumentSelector-button" > - <FontAwesomeIcon icon={faChevronCircleUp} size={"lg"} /> - </span> - </Flyout> - </div>; - } -} - -@observer -export class DockingViewButtonSelector extends React.Component<{ views: () => DocumentView[], Stack: any }> { - customStylesheet(styles: any) { - return { - ...styles, - panel: { - ...styles.panel, - minWidth: "100px" - }, - }; - } - _ref = React.createRef<HTMLDivElement>(); - - @computed get flyout() { - return ( - <div className="ParentDocumentSelector-flyout" title=" " ref={this._ref}> - <DocumentButtonBar views={this.props.views} stack={this.props.Stack} /> - </div> - ); - } - - render() { - return <Tooltip title={<><div className="dash-tooltip">Tap for toolbar, drag to create alias in another pane</div></>} placement="bottom"> - <span onPointerDown={e => { - if (getComputedStyle(this._ref.current!).width !== "100%") { - e.stopPropagation(); e.preventDefault(); - } - this.props.views()[0]?.select(false); - }} className="buttonSelector"> - <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={this.flyout} stylesheet={this.customStylesheet}> - <FontAwesomeIcon icon={"arrows-alt"} size={"sm"} /> - </Flyout> - </span> - </Tooltip>; - } -} diff --git a/src/client/views/collections/SchemaTable.tsx b/src/client/views/collections/SchemaTable.tsx index abbb7f6b5..eda62bf0b 100644 --- a/src/client/views/collections/SchemaTable.tsx +++ b/src/client/views/collections/SchemaTable.tsx @@ -5,16 +5,15 @@ import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; import ReactTable, { CellInfo, Column, ComponentPropsGetterR, Resize, SortingRule } from "react-table"; import "react-table/react-table.css"; -import { Doc, DocListCast, Field, Opt } from "../../../fields/Doc"; +import { Doc, DocListCast, Field, Opt, AclPrivate, AclReadonly, DataSym } from "../../../fields/Doc"; import { Id } from "../../../fields/FieldSymbols"; import { List } from "../../../fields/List"; import { listSpec } from "../../../fields/Schema"; import { SchemaHeaderField } from "../../../fields/SchemaHeaderField"; import { ComputedField } from "../../../fields/ScriptField"; import { Cast, FieldValue, NumCast, StrCast } from "../../../fields/Types"; -import { emptyFunction, emptyPath, returnEmptyFilter, returnFalse, returnOne, returnZero } from "../../../Utils"; +import { emptyFunction, emptyPath, returnEmptyFilter, returnFalse, returnOne, returnZero, returnEmptyDoclist } from "../../../Utils"; import { Docs, DocumentOptions } from "../../documents/Documents"; -import { DocumentType } from "../../documents/DocumentTypes"; import { CompileScript, Transformer, ts } from "../../util/Scripting"; import { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; @@ -22,11 +21,15 @@ import { COLLECTION_BORDER_WIDTH } from '../../views/globalCssVariables.scss'; import { ContextMenu } from "../ContextMenu"; import '../DocumentDecorations.scss'; import { ContentFittingDocumentView } from "../nodes/ContentFittingDocumentView"; -import { CellProps, CollectionSchemaButtons, CollectionSchemaCell, CollectionSchemaCheckboxCell, CollectionSchemaDateCell, CollectionSchemaDocCell, CollectionSchemaImageCell, CollectionSchemaListCell, CollectionSchemaNumberCell, CollectionSchemaStringCell } from "./CollectionSchemaCells"; +import { CellProps, CollectionSchemaButtons, CollectionSchemaCell, CollectionSchemaCheckboxCell, CollectionSchemaDateCell, CollectionSchemaDocCell, CollectionSchemaImageCell, CollectionSchemaListCell, CollectionSchemaNumberCell, CollectionSchemaStringCell, CollectionSchemaBooleanCell } from "./CollectionSchemaCells"; import { CollectionSchemaAddColumnHeader, KeysDropdown } from "./CollectionSchemaHeaders"; import { MovableColumn, MovableRow } from "./CollectionSchemaMovableTableHOC"; import "./CollectionSchemaView.scss"; import { CollectionView } from "./CollectionView"; +import { DocumentType } from "../../documents/DocumentTypes"; +import { GetEffectiveAcl } from "../../../fields/util"; +import { DateField } from "../../../fields/DateField"; +import { ImageField } from "../../../fields/URLField"; enum ColumnType { @@ -45,7 +48,7 @@ const columnTypes: Map<string, ColumnType> = new Map([ ["title", ColumnType.String], ["x", ColumnType.Number], ["y", ColumnType.Number], ["_width", ColumnType.Number], ["_height", ColumnType.Number], ["_nativeWidth", ColumnType.Number], ["_nativeHeight", ColumnType.Number], ["isPrototype", ColumnType.Boolean], - ["page", ColumnType.Number], ["curPage", ColumnType.Number], ["currentTimecode", ColumnType.Number], ["zIndex", ColumnType.Number] + ["_curPage", ColumnType.Number], ["_currentTimecode", ColumnType.Number], ["zIndex", ColumnType.Number] ]); export interface SchemaTableProps { @@ -70,7 +73,7 @@ export interface SchemaTableProps { isSelected: (outsideReaction?: boolean) => boolean; isFocused: (document: Doc, outsideReaction: boolean) => boolean; setFocused: (document: Doc) => void; - setPreviewDoc: (document: Doc) => void; + setPreviewDoc: (document: Opt<Doc>) => void; columns: SchemaHeaderField[]; documentKeys: any[]; headerIsEditing: boolean; @@ -91,7 +94,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { @observable _cellIsEditing: boolean = false; @observable _focusedCell: { row: number, col: number } = { row: 0, col: 0 }; - @observable _openCollections: Array<string> = []; + @observable _openCollections: Set<number> = new Set; @observable _showDoc: Doc | undefined; @observable _showDataDoc: any = ""; @@ -136,16 +139,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { @action changeSorting = (col: any) => { - if (col.desc === undefined) { - // no sorting - this.props.changeColumnSort(col, true); - } else if (col.desc === true) { - // descending sort - this.props.changeColumnSort(col, false); - } else if (col.desc === false) { - // ascending sort - this.props.changeColumnSort(col, undefined); - } + this.props.changeColumnSort(col, col.desc === true ? false : col.desc === false ? undefined : true); } @action @@ -153,7 +147,6 @@ export class SchemaTable extends React.Component<SchemaTableProps> { @computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); } @computed get tableColumns(): Column<Doc>[] { - const possibleKeys = this.props.documentKeys.filter(key => this.props.columns.findIndex(existingKey => existingKey.heading.toUpperCase() === key.toUpperCase()) === -1); const columns: Column<Doc>[] = []; const tableIsFocused = this.props.isFocused(this.props.Document, false); @@ -161,27 +154,16 @@ export class SchemaTable extends React.Component<SchemaTableProps> { const focusedCol = this._focusedCell.col; const isEditable = !this.props.headerIsEditing; - if (this.childDocs.reduce((found, doc) => found || doc.type === DocumentType.COL, false)) { - columns.push( - { - expander: true, - Header: "", - width: 30, - Expander: (rowInfo) => { - if (rowInfo.original.type === "collection") { - return rowInfo.isExpanded ? - <div className="collectionSchemaView-expander" onClick={() => this.onCloseCollection(rowInfo.original)}><FontAwesomeIcon icon={"caret-down"} size="sm" /></div> : - <div className="collectionSchemaView-expander" onClick={() => this.onExpandCollection(rowInfo.original)}><FontAwesomeIcon icon={"caret-right"} size="sm" /></div>; - } else { - return null; - } - } - } - ); - } - this.props.active; - - const cols = this.props.columns.map(col => { + columns.push({ + expander: true, Header: "", width: 58, + Expander: (rowInfo) => { + return rowInfo.original.type !== DocumentType.COL ? (null) : + <div className="collectionSchemaView-expander" onClick={action(() => (this._openCollections[rowInfo.isExpanded ? "delete" : "add"])(rowInfo.viewIndex))}> + <FontAwesomeIcon icon={rowInfo.isExpanded ? "caret-down" : "caret-right"} size="lg" /> + </div>; + } + }); + columns.push(...this.props.columns.map(col => { const icon: IconProp = this.getColumnType(col) === ColumnType.Number ? "hashtag" : this.getColumnType(col) === ColumnType.String ? "font" : this.getColumnType(col) === ColumnType.Boolean ? "check-square" : this.getColumnType(col) === ColumnType.Doc ? "file" : this.getColumnType(col) === ColumnType.Image ? "image" : this.getColumnType(col) === ColumnType.List ? "list-ul" : @@ -209,25 +191,13 @@ export class SchemaTable extends React.Component<SchemaTableProps> { width={"100%"} />; - - const sortIcon = col.desc === undefined ? "caret-right" : col.desc === true ? "caret-down" : "caret-up"; - - const header = - <div //className="collectionSchemaView-header" - //onClick={e => this.props.openHeader(col, menuContent, e.clientX, e.clientY)} - className="collectionSchemaView-menuOptions-wrapper" - style={{ - background: col.color, padding: "2px", - display: "flex", cursor: "default", height: "100%", - }}> - {/* <FontAwesomeIcon onClick={e => this.props.openHeader(col, e.clientX, e.clientY)} icon={icon} size="lg" style={{ display: "inline", paddingBottom: "1px", paddingTop: "4px", cursor: "hand" }} /> */} - {keysDropdown} - <div onClick={e => this.changeSorting(col)} - style={{ width: 21, padding: 1, display: "inline", zIndex: 1, background: "inherit", cursor: "hand" }}> - <FontAwesomeIcon icon={sortIcon} size="lg" /> - </div> - </div>; + const header = <div className="collectionSchemaView-menuOptions-wrapper" style={{ background: col.color, padding: "2px", display: "flex", cursor: "default", height: "100%", }}> + {keysDropdown} + <div onClick={e => this.changeSorting(col)} style={{ width: 21, padding: 1, display: "inline", zIndex: 1, background: "inherit", cursor: "hand" }}> + <FontAwesomeIcon icon={sortIcon} size="lg" /> + </div> + </div>; return { Header: <MovableColumn columnRenderer={header} columnValue={col} allColumns={this.props.columns} reorderColumns={this.props.reorderColumns} ScreenToLocalTransform={this.props.ScreenToLocalTransform} />, @@ -260,21 +230,22 @@ export class SchemaTable extends React.Component<SchemaTableProps> { showDoc: this.showDoc, }; - const colType = this.getColumnType(col); - if (colType === ColumnType.Number) return <CollectionSchemaNumberCell {...props} />; - if (colType === ColumnType.String) return <CollectionSchemaStringCell {...props} />; - if (colType === ColumnType.Boolean) return <CollectionSchemaCheckboxCell {...props} />; - if (colType === ColumnType.Doc) return <CollectionSchemaDocCell {...props} />; - if (colType === ColumnType.Image) return <CollectionSchemaImageCell {...props} />; - if (colType === ColumnType.List) return <CollectionSchemaListCell {...props} />; - if (colType === ColumnType.Date) return <CollectionSchemaDateCell {...props} />; - return <CollectionSchemaCell {...props} />; + + switch (this.getColumnType(col, rowProps.original, rowProps.column.id)) { + case ColumnType.Number: return <CollectionSchemaNumberCell {...props} />; + case ColumnType.String: return <CollectionSchemaStringCell {...props} />; + case ColumnType.Boolean: return <CollectionSchemaCheckboxCell {...props} />; + case ColumnType.Doc: return <CollectionSchemaDocCell {...props} />; + case ColumnType.Image: return <CollectionSchemaImageCell {...props} />; + case ColumnType.List: return <CollectionSchemaListCell {...props} />; + case ColumnType.Date: return <CollectionSchemaDateCell {...props} />; + default: + return <CollectionSchemaCell {...props} />; + } }, minWidth: 200, }; - }); - columns.push(...cols); - + })); columns.push({ Header: <CollectionSchemaAddColumnHeader createColumn={this.createColumn} />, accessor: (doc: Doc) => 0, @@ -283,8 +254,8 @@ export class SchemaTable extends React.Component<SchemaTableProps> { const rowIndex = rowProps.index; const columnIndex = this.props.columns.map(c => c.heading).indexOf(rowProps.column.id!); const isFocused = focusedRow === rowIndex && focusedCol === columnIndex && tableIsFocused; - const props: CellProps = { - row: rowIndex, + return <CollectionSchemaButtons {...{ + row: rowProps.index, col: columnIndex, rowProps: rowProps, isFocused: isFocused, @@ -303,9 +274,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { setComputed: this.setComputed, getField: this.getField, showDoc: this.showDoc, - }; - - return <CollectionSchemaButtons {...props} />; + }} />; }, width: 28, resizable: false @@ -316,12 +285,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { constructor(props: SchemaTableProps) { super(props); - // convert old schema columns (list of strings) into new schema columns (list of schema header fields) - const oldSchemaHeaders = Cast(this.props.Document._schemaHeaders, listSpec("string"), []); - if (oldSchemaHeaders?.length && typeof oldSchemaHeaders[0] !== "object") { - const newSchemaHeaders = oldSchemaHeaders.map(i => typeof i === "string" ? new SchemaHeaderField(i, "#f1efeb") : i); - this.props.Document._schemaHeaders = new List<SchemaHeaderField>(newSchemaHeaders); - } else if (this.props.Document._schemaHeaders === undefined) { + if (this.props.Document._schemaHeaders === undefined) { this.props.Document._schemaHeaders = new List<SchemaHeaderField>([new SchemaHeaderField("title", "#f1efeb"), new SchemaHeaderField("author", "#f1efeb"), new SchemaHeaderField("*lastModified", "#f1efeb", ColumnType.Date), new SchemaHeaderField("text", "#f1efeb", ColumnType.String), new SchemaHeaderField("type", "#f1efeb"), new SchemaHeaderField("context", "#f1efeb", ColumnType.Doc)]); } @@ -336,7 +300,15 @@ export class SchemaTable extends React.Component<SchemaTableProps> { } tableAddDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => { - return Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before); + const tableDoc = this.props.Document[DataSym]; + const effectiveAcl = GetEffectiveAcl(tableDoc); + + if (effectiveAcl !== AclPrivate && effectiveAcl !== AclReadonly) { + doc.context = this.props.Document; + tableDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now())); + return Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before); + } + return false; } private getTrProps: ComponentPropsGetterR = (state, rowInfo) => { @@ -362,19 +334,10 @@ export class SchemaTable extends React.Component<SchemaTableProps> { const isFocused = this._focusedCell.row === row && this._focusedCell.col === col && this.props.isFocused(this.props.Document, true); // TODO: editing border doesn't work :( return { - style: { - border: !this.props.headerIsEditing && isFocused ? "2px solid rgb(255, 160, 160)" : "1px solid #f1efeb" - } + style: { border: !this.props.headerIsEditing && isFocused ? "2px solid rgb(255, 160, 160)" : "1px solid #f1efeb" } }; } - @action - onCloseCollection = (collection: Doc): void => { - const index = this._openCollections.findIndex(col => col === collection[Id]); - if (index > -1) this._openCollections.splice(index, 1); - } - - @action onExpandCollection = (collection: Doc) => this._openCollections.push(collection[Id]); @action setCellIsEditing = (isEditing: boolean) => this._cellIsEditing = isEditing; @action @@ -385,7 +348,9 @@ export class SchemaTable extends React.Component<SchemaTableProps> { const pdoc = FieldValue(this.childDocs[this._focusedCell.row]); pdoc && this.props.setPreviewDoc(pdoc); - } else if ((this._cellIsEditing || this.props.headerIsEditing) && (e.keyCode === 37 || e.keyCode === 39)) { + e.stopPropagation(); + } else if (e.keyCode === 27) { + this.props.setPreviewDoc(undefined); e.stopPropagation(); // stopPropagation for left/right arrows } } @@ -410,9 +375,10 @@ export class SchemaTable extends React.Component<SchemaTableProps> { } @undoBatch - createRow = () => { + createRow = action(() => { this.props.addDocument(Docs.Create.TextDocument("", { title: "", _width: 100, _height: 30 })); - } + this._focusedCell = { row: this.childDocs.length, col: this._focusedCell.col }; + }); @undoBatch @action @@ -427,22 +393,21 @@ export class SchemaTable extends React.Component<SchemaTableProps> { } @action - getColumnType = (column: SchemaHeaderField): ColumnType => { - // added functionality to convert old column type stuff to new column type stuff -syip + getColumnType = (column: SchemaHeaderField, doc?: Doc, field?: string): ColumnType => { + if (doc && field && column.type === ColumnType.Any) { + const val = doc[CollectionSchemaCell.resolvedFieldKey(field, doc)]; + if (val instanceof ImageField) return ColumnType.Image; + if (val instanceof Doc) return ColumnType.Doc; + if (val instanceof DateField) return ColumnType.Date; + if (val instanceof List) return ColumnType.List; + } if (column.type && column.type !== 0) { return column.type; } if (columnTypes.get(column.heading)) { - column.type = columnTypes.get(column.heading)!; - return columnTypes.get(column.heading)!; - } - const typesDoc = FieldValue(Cast(this.props.Document.schemaColumnTypes, Doc)); - if (!typesDoc) { - column.type = ColumnType.Any; - return ColumnType.Any; + return column.type = columnTypes.get(column.heading)!; } - column.type = NumCast(typesDoc[column.heading]); - return NumCast(typesDoc[column.heading]); + return column.type = ColumnType.Any; } @undoBatch @@ -471,11 +436,9 @@ export class SchemaTable extends React.Component<SchemaTableProps> { @computed get reactTable() { const children = this.childDocs; - const hasCollectionChild = children.reduce((found, doc) => found || doc.type === "collection", false); - const expandedRowsList = this._openCollections.map(col => children.findIndex(doc => doc[Id] === col).toString()); - const expanded = {}; - //@ts-ignore - expandedRowsList.forEach(row => expanded[row] = true); + const hasCollectionChild = children.reduce((found, doc) => found || doc.type === DocumentType.COL, false); + const expanded: { [name: string]: any } = {}; + Array.from(this._openCollections.keys()).map(col => expanded[col.toString()] = true); const rerender = [...this.textWrappedRows]; // TODO: get component to rerender on text wrap change without needign to console.log :(((( return <ReactTable @@ -493,17 +456,14 @@ export class SchemaTable extends React.Component<SchemaTableProps> { expanded={expanded} resized={this.resized} onResizedChange={this.props.onResizedChange} - SubComponent={!hasCollectionChild ? undefined : row => (row.original.type !== "collection") ? (null) : + SubComponent={!hasCollectionChild ? undefined : row => (row.original.type !== DocumentType.COL) ? (null) : <div className="reactTable-sub"><SchemaTable {...this.props} Document={row.original} dataDoc={undefined} childDocs={undefined} /></div>} />; } onContextMenu = (e: React.MouseEvent): void => { - if (!e.isPropagationStopped() && this.props.Document[Id] !== "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 - // ContextMenu.Instance.addItem({ description: "Make DB", event: this.makeDB, icon: "table" }); - ContextMenu.Instance.addItem({ description: "Toggle text wrapping", event: this.toggleTextwrap, icon: "table" }); - } + ContextMenu.Instance.addItem({ description: "Toggle text wrapping", event: this.toggleTextwrap, icon: "table" }); } getField = (row: number, col?: number) => { @@ -564,10 +524,8 @@ export class SchemaTable extends React.Component<SchemaTableProps> { setComputed = (script: string, doc: Doc, field: string, row: number, col: number): boolean => { script = `const $ = (row:number, col?:number) => { - if(col === undefined) { - return (doc as any)[key][row + ${row}]; - } - return (doc as any)[key][row + ${row}][(doc as any)._schemaHeaders[col + ${col}].heading]; + const rval = (doc as any)[key][row + ${row}]; + return col === undefined ? rval : rval[(doc as any)._schemaHeaders[col + ${col}].heading]; } return ${script}`; const compiled = CompileScript(script, { params: { this: Doc.name }, capturedVariables: { doc: this.props.Document, key: this.props.fieldKey }, typecheck: false, transformer: this.createTransformer(row, col) }); @@ -587,9 +545,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { } onOpenClick = () => { - if (this._showDoc) { - this.props.addDocTab(this._showDoc, "onRight"); - } + this._showDoc && this.props.addDocTab(this._showDoc, "add:right"); } getPreviewTransform = (): Transform => { @@ -598,14 +554,14 @@ export class SchemaTable extends React.Component<SchemaTableProps> { render() { const preview = ""; - return <div className="collectionSchemaView-table" style={{ overflow: this.props.Document._searchDoc ? undefined : "auto" }} + return <div className="collectionSchemaView-table" onPointerDown={this.props.onPointerDown} onClick={this.props.onClick} onWheel={e => this.props.active(true) && e.stopPropagation()} onDrop={e => this.props.onDrop(e, {})} onContextMenu={this.onContextMenu} > {this.reactTable} - {StrCast(this.props.Document.type) !== "search" ? <div className="collectionSchemaView-addRow" onClick={() => this.createRow()}>+ new</div> + {StrCast(this.props.Document._chromeStatus) !== "disabled" ? <div className="collectionSchemaView-addRow" onClick={() => this.createRow()}>+ new</div> : undefined} {!this._showDoc ? (null) : - <div className="collectionSchemaView-documentPreview" //onClick={() => { this.onOpenClick(); }} + <div className="collectionSchemaView-documentPreview" style={{ position: "absolute", width: 150, height: 150, background: "dimGray", display: "block", top: 0, left: 0, @@ -614,8 +570,6 @@ export class SchemaTable extends React.Component<SchemaTableProps> { ref="overlay"><ContentFittingDocumentView Document={this._showDoc} DataDoc={this._showDataDoc} - NativeHeight={returnZero} - NativeWidth={returnZero} fitToBox={true} FreezeDimensions={true} focus={emptyFunction} @@ -626,6 +580,8 @@ export class SchemaTable extends React.Component<SchemaTableProps> { PanelHeight={() => 150} ScreenToLocalTransform={this.getPreviewTransform} docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} ContainingCollectionDoc={this.props.CollectionView?.props.Document} ContainingCollectionView={this.props.CollectionView} moveDocument={this.props.moveDocument} diff --git a/src/client/views/collections/TabDocView.scss b/src/client/views/collections/TabDocView.scss new file mode 100644 index 000000000..edf556c9f --- /dev/null +++ b/src/client/views/collections/TabDocView.scss @@ -0,0 +1,54 @@ +input.lm_title:focus, +input.lm_title +{ + max-width: unset !important; + transition-delay: unset; + width: 100%; + cursor: text; +} +input.lm_title { + transition-delay: 0.35s; + width: 100px; + cursor: pointer; +} +.tabDocView-drag { + margin: auto; +} +.miniMap-hidden, +.miniMap { + position: absolute; + overflow: hidden; + right: 10; + bottom: 10; + border: solid 1px; + box-shadow: black 0.4vw 0.4vw 0.8vw; + width: 100%; + height: 100%; + transition: all 0.5s; + + .miniOverlay { + width: 100%; + height: 100%; + position: absolute; + + .miniThumb { + background: #25252525; + position: absolute; + } + } +} +.miniMap-hidden { + position: absolute; + bottom: 0; + right: 0; + width: 45px; + height: 45px; + background: white; + transform: translate(20px, 20px) rotate(45deg); + border-radius: 30px; + padding: 2px; + > svg { + margin-top: 3px; + transform: translate(0px, 7px); + } +}
\ No newline at end of file diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx new file mode 100644 index 000000000..8c1a003b9 --- /dev/null +++ b/src/client/views/collections/TabDocView.tsx @@ -0,0 +1,384 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Tooltip } from '@material-ui/core'; +import 'golden-layout/src/css/goldenlayout-base.css'; +import 'golden-layout/src/css/goldenlayout-dark-theme.css'; +import { clamp } from 'lodash'; +import { action, computed, IReactionDisposer, observable, reaction } from "mobx"; +import { observer } from "mobx-react"; +import * as ReactDOM from 'react-dom'; +import { DataSym, Doc, DocListCast, Opt } from "../../../fields/Doc"; +import { Id } from '../../../fields/FieldSymbols'; +import { FieldId } from "../../../fields/RefField"; +import { listSpec } from '../../../fields/Schema'; +import { Cast, NumCast, StrCast } from "../../../fields/Types"; +import { TraceMobx } from '../../../fields/util'; +import { emptyFunction, emptyPath, returnFalse, returnOne, returnTrue, setupMoveUpEvents, Utils } from "../../../Utils"; +import { DocServer } from "../../DocServer"; +import { CurrentUserUtils } from '../../util/CurrentUserUtils'; +import { DocumentManager } from '../../util/DocumentManager'; +import { DragManager, dropActionType } from "../../util/DragManager"; +import { SelectionManager } from '../../util/SelectionManager'; +import { SnappingManager } from '../../util/SnappingManager'; +import { Transform } from '../../util/Transform'; +import { undoBatch, UndoManager } from "../../util/UndoManager"; +import { DocumentView } from "../nodes/DocumentView"; +import { PresBox, PresMovement } from '../nodes/PresBox'; +import { CollectionDockingView } from './CollectionDockingView'; +import { CollectionDockingViewMenu } from './CollectionDockingViewMenu'; +import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; +import { CollectionViewType } from './CollectionView'; +import "./TabDocView.scss"; +import React = require("react"); +const _global = (window /* browser */ || global /* node */) as any; + +interface TabDocViewProps { + documentId: FieldId; + glContainer: any; +} +@observer +export class TabDocView extends React.Component<TabDocViewProps> { + _mainCont: HTMLDivElement | null = null; + _tabReaction: IReactionDisposer | undefined; + @observable private _panelWidth = 0; + @observable private _panelHeight = 0; + @observable private _isActive: boolean = false; + @observable private _document: Doc | undefined; + @observable private _view: DocumentView | undefined; + + get stack(): any { return (this.props as any).glContainer.parent.parent; } + get tab() { return (this.props as any).glContainer.tab; } + get view() { return this._view; } + + @action + init = (tab: any, doc: Opt<Doc>) => { + if (tab.DashDoc !== doc && doc && tab.hasOwnProperty("contentItem") && tab.contentItem.config.type !== "stack") { + tab._disposers = {} as { [name: string]: IReactionDisposer }; + tab.contentItem.config.fixed && (tab.contentItem.parent.config.fixed = true); + tab.DashDoc = doc; + CollectionDockingView.Instance.tabMap.add(tab); + + // setup the title element and set its size according to the # of chars in the title. Show the full title when clicked. + const titleEle = tab.titleElement[0]; + titleEle.size = StrCast(doc.title).length + 3; + titleEle.value = doc.title; + titleEle.onchange = (e: any) => { + titleEle.size = e.currentTarget.value.length + 3; + Doc.GetProto(doc).title = e.currentTarget.value; + }; + // shifts the focus to this tab when another tab is dragged over it + tab.element[0].onmouseenter = (e: MouseEvent) => { + if (SnappingManager.GetIsDragging() && tab.contentItem !== tab.header.parent.getActiveContentItem()) { + tab.header.parent.setActiveContentItem(tab.contentItem); + tab.setActive(true); + } + }; + const dragBtnDown = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, e => !e.defaultPrevented && DragManager.StartDocumentDrag([dragHdl], new DragManager.DocumentDragData([doc], doc.dropAction as dropActionType), e.clientX, e.clientY), returnFalse, emptyFunction); + }; + + // select the tab document when the tab is directly clicked and activate the tab whenver the tab document is selected + titleEle.onpointerdown = (e: any) => { + if (e.target.className !== "lm_close_tab" && this.view) { + SelectionManager.SelectDoc(this.view, false); + if (Date.now() - titleEle.lastClick < 1000) titleEle.select(); + titleEle.lastClick = Date.now(); + (document.activeElement !== titleEle) && titleEle.focus(); + } + }; + tab._disposers.selectionDisposer = reaction(() => SelectionManager.SelectedDocuments().some(v => (v.topMost || v.props.treeViewDoc) && v.props.Document === doc), + (selected) => selected && tab.contentItem !== tab.header.parent.getActiveContentItem() && + UndoManager.RunInBatch(() => tab.header.parent.setActiveContentItem(tab.contentItem), "tab switch")); + + //attach the selection doc buttons menu to the drag handle + const stack = tab.contentItem.parent; + const dragHdl = document.createElement("div"); + dragHdl.className = "lm_drag_tab"; + tab._disposers.buttonDisposer = reaction(() => this.view, view => + view && [ReactDOM.render(<span className="tabDocView-drag" onPointerDown={dragBtnDown}><CollectionDockingViewMenu views={() => [view]} Stack={stack} /></span>, dragHdl), tab._disposers.buttonDisposer?.()], + { fireImmediately: true }); + tab.reactComponents = [dragHdl]; + tab.closeElement.before(dragHdl); + + // highlight the tab when the tab document is brushed in any part of the UI + tab._disposers.reactionDisposer = reaction(() => ({ title: doc.title, degree: Doc.IsBrushedDegree(doc) }), ({ title, degree }) => { + titleEle.value = title; + titleEle.style.padding = degree ? 0 : 2; + titleEle.style.border = `${["gray", "gray", "gray"][degree]} ${["none", "dashed", "solid"][degree]} 2px`; + }, { fireImmediately: true }); + + // clean up the tab when it is closed + tab.closeElement.off('click') //unbind the current click handler + .click(function () { + Object.values(tab._disposers).forEach((disposer: any) => disposer?.()); + Doc.AddDocToList(CurrentUserUtils.MyRecentlyClosed, "data", doc, undefined, true, true); + SelectionManager.DeselectAll(); + tab.contentItem.remove(); + }); + } + } + + /** + * Adds a document to the presentation view + **/ + @undoBatch + @action + public static PinDoc(doc: Doc, unpin = false, audioRange?: boolean) { + if (unpin) console.log('remove unpin'); + //add this new doc to props.Document + const curPres = CurrentUserUtils.ActivePresentation; + if (curPres) { + const pinDoc = Doc.MakeAlias(doc); + pinDoc.presentationTargetDoc = doc; + pinDoc.title = doc.title; + pinDoc.presMovement = PresMovement.Zoom; + pinDoc.context = curPres; + Doc.AddDocToList(curPres, "data", pinDoc); + if (pinDoc.type === "audio" && !audioRange) { + pinDoc.presStartTime = 0; + pinDoc.presEndTime = doc.duration; + } + if (curPres.expandBoolean) pinDoc.presExpandInlineButton = true; + const curPresDocView = DocumentManager.Instance.getDocumentView(curPres); + if (!curPresDocView) { + CollectionDockingView.AddSplit(curPres, "right"); + } + DocumentManager.Instance.jumpToDocument(doc, false, undefined, Cast(doc.context, Doc, null)); + } + } + + /** + * Adds a document to the presentation view + **/ + @undoBatch + @action + public static UnpinDoc(doc: Doc) { + const curPres = CurrentUserUtils.ActivePresentation; + if (curPres) { + const ind = DocListCast(curPres.data).findIndex((val) => Doc.AreProtosEqual(val, doc)); + ind !== -1 && Doc.RemoveDocFromList(curPres, "data", DocListCast(curPres.data)[ind]); + } + } + + componentDidMount() { + const selected = () => SelectionManager.SelectedDocuments().some(v => v.props.Document === this._document); + new _global.ResizeObserver(action((entries: any) => { + for (const entry of entries) { + this._panelWidth = entry.contentRect.width; + this._panelHeight = entry.contentRect.height; + } + })).observe(this.props.glContainer._element[0]); + this.props.glContainer.layoutManager.on("activeContentItemChanged", this.onActiveContentItemChanged); + this.props.glContainer.tab?.isActive && this.onActiveContentItemChanged(); + this._tabReaction = reaction(() => ({ selected: selected(), color: this.tabColor, title: this.tab?.titleElement[0] }), + ({ selected, color, title }) => title && (title.style.backgroundColor = selected ? color : ""), + { fireImmediately: true }); + } + + componentWillUnmount() { + this._tabReaction?.(); + this.props.glContainer.layoutManager.off("activeContentItemChanged", this.onActiveContentItemChanged); + } + + @action.bound + private onActiveContentItemChanged() { + if (this.props.glContainer.tab && this._isActive !== this.props.glContainer.tab.isActive) { + this._isActive = this.props.glContainer.tab.isActive; + (CollectionDockingView.Instance as any)._goldenLayout?.isInitialised && CollectionDockingView.Instance.stateChanged(); + !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. + } + } + + nativeAspect = () => this.nativeWidth() ? this.nativeWidth() / this.nativeHeight() : 0; + panelWidth = () => this.layoutDoc?.maxWidth ? Math.min(Math.max(NumCast(this.layoutDoc._width), NumCast(this.layoutDoc._nativeWidth)), this._panelWidth) : + (this.nativeAspect() && this.nativeAspect() < this._panelWidth / this._panelHeight ? this._panelHeight * this.nativeAspect() : this._panelWidth) + panelHeight = () => this.nativeAspect() && this.nativeAspect() > this._panelWidth / this._panelHeight ? this._panelWidth / this.nativeAspect() : this._panelHeight; + nativeWidth = () => !this.layoutDoc?._fitWidth ? NumCast(this.layoutDoc?._nativeWidth) || this._panelWidth : 0; + nativeHeight = () => !this.layoutDoc?._fitWidth ? NumCast(this.layoutDoc?._nativeHeight) || this._panelHeight : 0; + contentScaling = () => { + const nativeH = NumCast(this.layoutDoc?._nativeHeight); + const nativeW = NumCast(this.layoutDoc?._nativeWidth); + let scaling = 1; + if (nativeW && (this.layoutDoc?._fitWidth || this._panelHeight / nativeH > this._panelWidth / nativeW)) { + scaling = this._panelWidth / nativeW; // width-limited or fitWidth + } else if (nativeW && nativeH) { + scaling = this._panelHeight / nativeH; // height-limited + } + return scaling; + } + + ScreenToLocalTransform = () => { + if (this._mainCont?.children) { + const { translateX, translateY } = Utils.GetScreenTransform(this._mainCont.children[0]?.firstChild as HTMLElement); + const scale = Utils.GetScreenTransform(this._mainCont).scale; + return CollectionDockingView.Instance?.props.ScreenToLocalTransform().translate(-translateX, -translateY).scale(1 / this.contentScaling() / scale); + } + return Transform.Identity(); + } + @computed get previewPanelCenteringOffset() { return this.nativeWidth() ? (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2 : 0; } + @computed get widthpercent() { return this.nativeWidth() ? `${(this.nativeWidth() * this.contentScaling()) / this._panelWidth * 100}% ` : undefined; } + @computed get layoutDoc() { return this._document && Doc.Layout(this._document); } + + // 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, + // add[:{left,right,top,bottom}] - e.g., "add" will add a tab to the current stack, "add:right" will add a tab on the right + // replace[:{left,right,top,bottom,<any string>}] - e.g., "replace" will replace the current stack contents, + // "replace:right" - will replace the stack on the right named "right" if it exists, or create a stack on the right with that name, + // "replace:monkeys" - will replace any tab that has the label 'monkeys', or a tab with that label will be created by default on the right + // inPlace - will add the document to any collection along the path from the document to the docking view that has a field isInPlaceContainer. if none is found, inPlace adds a tab to current stack + addDocTab = (doc: Doc, location: string, libraryPath?: Doc[]) => { + SelectionManager.DeselectAll(); + const locationFields = doc._viewType === CollectionViewType.Docking ? ["dashboard"] : location.split(":"); + const locationParams = locationFields.length > 1 ? locationFields[1] : ""; + switch (locationFields[0]) { + case "dashboard": return CurrentUserUtils.openDashboard(Doc.UserDoc(), doc); + case "close": return CollectionDockingView.CloseSplit(doc, locationParams); + case "fullScreen": return CollectionDockingView.OpenFullScreen(doc); + case "replace": return CollectionDockingView.ReplaceTab(doc, locationParams, this.stack); + case "inPlace": + case "add": + default: return CollectionDockingView.AddSplit(doc, locationParams, this.stack); + } + } + + @computed get tabColor() { return StrCast(this._document?._backgroundColor, StrCast(this._document?.backgroundColor, CollectionDockingView.Instance.props.backgroundColor?.(this._document, 0))); } + @computed get renderBounds() { + const bounds = this._document ? Cast(this._document._renderContentBounds, listSpec("number"), [0, 0, this.returnMiniSize(), this.returnMiniSize()]) : [0, 0, 0, 0]; + const xbounds = bounds[2] - bounds[0]; + const ybounds = bounds[3] - bounds[1]; + const dim = Math.max(xbounds, ybounds); + return { l: bounds[0] + xbounds / 2 - dim / 2, t: bounds[1] + ybounds / 2 - dim / 2, cx: bounds[0] + xbounds / 2, cy: bounds[1] + ybounds / 2, dim }; + } + childLayoutTemplate = () => Cast(this._document?.childLayoutTemplate, Doc, null); + returnMiniSize = () => NumCast(this._document?._miniMapSize, 150); + miniDown = (e: React.PointerEvent) => { + const doc = this._document; + const miniSize = this.returnMiniSize(); + doc && setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => { + doc._panX = clamp(NumCast(doc._panX) + delta[0] / miniSize * this.renderBounds.dim, this.renderBounds.l, this.renderBounds.l + this.renderBounds.dim); + doc._panY = clamp(NumCast(doc._panY) + delta[1] / miniSize * this.renderBounds.dim, this.renderBounds.t, this.renderBounds.t + this.renderBounds.dim); + return false; + }), emptyFunction, emptyFunction); + } + getCurrentFrame = () => { + return NumCast(Cast(PresBox.Instance.childDocs[PresBox.Instance.itemIndex].presentationTargetDoc, Doc, null)._currentFrame); + } + renderMiniMap() { + const miniWidth = this.panelWidth() / NumCast(this._document?._viewScale, 1) / this.renderBounds.dim * 100; + const miniHeight = this.panelHeight() / NumCast(this._document?._viewScale, 1) / this.renderBounds.dim * 100; + const miniLeft = 50 + (NumCast(this._document?._panX) - this.renderBounds.cx) / this.renderBounds.dim * 100 - miniWidth / 2; + const miniTop = 50 + (NumCast(this._document?._panY) - this.renderBounds.cy) / this.renderBounds.dim * 100 - miniHeight / 2; + const miniSize = this.returnMiniSize(); + return <> + <div className="miniMap" style={{ width: miniSize, height: miniSize, background: this.tabColor }}> + <CollectionFreeFormView + Document={this._document!} + LibraryPath={emptyPath} + CollectionView={undefined} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} + ChildLayoutTemplate={this.childLayoutTemplate} // bcz: Ugh .. should probably be rendering a CollectionView or the minimap should be part of the collectionFreeFormView to avoid having to set stuff like this. + noOverlay={true} // don't render overlay Docs since they won't scale + active={returnTrue} + select={emptyFunction} + dropAction={undefined} + isSelected={returnFalse} + dontRegisterView={true} + annotationsKey={""} + fieldKey={Doc.LayoutFieldKey(this._document!)} + bringToFront={emptyFunction} + rootSelected={returnTrue} + addDocument={returnFalse} + moveDocument={returnFalse} + removeDocument={returnFalse} + ContentScaling={returnOne} + PanelWidth={this.returnMiniSize} + PanelHeight={this.returnMiniSize} + ScreenToLocalTransform={this.ScreenToLocalTransform} + renderDepth={0} + whenActiveChanged={emptyFunction} + focus={emptyFunction} + backgroundColor={CollectionDockingView.Instance.props.backgroundColor} + addDocTab={this.addDocTab} + pinToPres={TabDocView.PinDoc} + docFilters={CollectionDockingView.Instance.docFilters} + docRangeFilters={CollectionDockingView.Instance.docRangeFilters} + searchFilterDocs={CollectionDockingView.Instance.searchFilterDocs} + fitToBox={true} + /> + <div className="miniOverlay" onPointerDown={this.miniDown} > + <div className="miniThumb" style={{ width: `${miniWidth}% `, height: `${miniHeight}% `, left: `${miniLeft}% `, top: `${miniTop}% `, }} /> + </div> + </div> + + <Tooltip title={<div className="dash-tooltip">{"toggle minimap"}</div>}> + <div className="miniMap-hidden" onPointerDown={e => e.stopPropagation()} onClick={action(e => { e.stopPropagation(); this._document!.hideMinimap = !this._document!.hideMinimap; })} > + <FontAwesomeIcon icon={"globe-asia"} size="lg" /> + </div> + </Tooltip> + </>; + } + focusFunc = (doc: Doc, willZoom: boolean, scale?: number, afterFocus?: () => void) => { + 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) + } + afterFocus?.(); + } + setView = action((view: DocumentView) => this._view = view); + active = () => this._isActive; + @computed get docView() { + TraceMobx(); + return !this._document || this._document._viewType === CollectionViewType.Docking ? (null) : + <><DocumentView key={this._document[Id]} + LibraryPath={emptyPath} + Document={this._document} + getView={this.setView} + DataDoc={!Doc.AreProtosEqual(this._document[DataSym], this._document) ? this._document[DataSym] : undefined} + bringToFront={emptyFunction} + rootSelected={returnTrue} + addDocument={undefined} + removeDocument={undefined} + ContentScaling={this.contentScaling} + PanelWidth={this.panelWidth} + PanelHeight={this.panelHeight} + NativeHeight={this.nativeHeight() ? this.nativeHeight : undefined} + NativeWidth={this.nativeWidth() ? this.nativeWidth : undefined} + ScreenToLocalTransform={this.ScreenToLocalTransform} + renderDepth={0} + parentActive={this.active} + whenActiveChanged={emptyFunction} + focus={this.focusFunc} + backgroundColor={CollectionDockingView.Instance.props.backgroundColor} + addDocTab={this.addDocTab} + pinToPres={TabDocView.PinDoc} + docFilters={CollectionDockingView.Instance.docFilters} + docRangeFilters={CollectionDockingView.Instance.docRangeFilters} + searchFilterDocs={CollectionDockingView.Instance.searchFilterDocs} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} /> + {this._document._viewType !== CollectionViewType.Freeform ? (null) : + <>{this._document.hideMinimap ? (null) : this.renderMiniMap()} + <Tooltip key="ttip" title={<div className="dash-tooltip">{"toggle minimap"}</div>}> + <div className="miniMap-hidden" onPointerDown={e => e.stopPropagation()} onClick={action(e => { e.stopPropagation(); this._document!.hideMinimap = !this._document!.hideMinimap; })} > + <FontAwesomeIcon icon={"globe-asia"} size="lg" /> + </div> + </Tooltip> + </>} + </>; + } + + render() { + return (<div className="collectionDockingView-content" ref={ref => { + if (this._mainCont = ref) { + (this._mainCont as any).InitTab = (tab: any) => 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))); + } + }} + style={{ + transform: `translate(${this.previewPanelCenteringOffset}px, 0px)`, + height: this.layoutDoc?._fitWidth ? undefined : "100%", + width: this.widthpercent + }}> + {this.docView} + </div >); + } +}
\ No newline at end of file diff --git a/src/client/views/collections/TreeView.scss b/src/client/views/collections/TreeView.scss new file mode 100644 index 000000000..17c6b0750 --- /dev/null +++ b/src/client/views/collections/TreeView.scss @@ -0,0 +1,115 @@ +@import "../globalCssVariables"; + +.treeView-container, +.treeView-container-active { + .bullet-outline { + position: relative; + width: 15px; + color: $intermediate-color; + transform: scale(0.5); + display: inline-block; + } + + .bullet { + position: relative; + width: 15px; + color: $intermediate-color; + margin-top: 3px; + transform: scale(1.3, 1.3); + border: #80808030 1px solid; + border-radius: 4px; + } +} +.treeView-container-active { + z-index: 100; + position: relative;; + .formattedTextbox-sidebar { + background-color: #ffff001f !important; + height: 500px !important; + } +} + +.treeView-openRight { + display: none; + height: 17px; + width: 15px; +} + +.treeView-openRight:hover { + background: #797777; + cursor: pointer; +} + +.treeView-border-outline, +.treeView-border { + display: flex; + overflow: hidden; +} +.treeView-border{ + border-left: dashed 1px #00000042; +} + +.treeView-header-editing, +.treeView-header { + border: transparent 1px solid; + display: flex; + //align-items: center; + ::-webkit-scrollbar { + display: none; + } + .formattedTextBox-cont { + .formattedTextbox-sidebar { + overflow: visible !important; + border-left: unset; + } + overflow: visible !important; + } + + .editableView-container-editing-oneLine { + min-width: 15px; + } + + .documentView-node-topmost { + width: unset; + } + + >svg { + display: none; + } + +} + +.treeView-header:hover { + .collectionTreeView-keyHeader { + display: inherit; + } + + >svg { + display: inherit; + } + + .treeView-openRight { + display: inline-block; + height: 17px; + width: 15px; + + // display: inline; + svg { + display: block; + padding: 0px; + margin-left: 3px; + } + } +} + +.treeView-header-above { + border-top: black 1px solid; +} + +.treeView-header-below { + border-bottom: black 1px solid; +} + +.treeView-header-inside { + border: black 1px solid; +}
\ No newline at end of file diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx new file mode 100644 index 000000000..1dfa5615a --- /dev/null +++ b/src/client/views/collections/TreeView.tsx @@ -0,0 +1,768 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, computed, observable } from "mobx"; +import { observer } from "mobx-react"; +import { DataSym, Doc, DocListCast, DocListCastOrNull, Field, HeightSym, Opt, WidthSym } from '../../../fields/Doc'; +import { Id } from '../../../fields/FieldSymbols'; +import { List } from '../../../fields/List'; +import { RichTextField } from '../../../fields/RichTextField'; +import { listSpec } from '../../../fields/Schema'; +import { ComputedField, ScriptField } from '../../../fields/ScriptField'; +import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; +import { TraceMobx } from '../../../fields/util'; +import { emptyFunction, emptyPath, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnOne, returnTrue, returnZero, simulateMouseClick, Utils } from '../../../Utils'; +import { Docs, DocUtils } from '../../documents/Documents'; +import { DocumentType } from "../../documents/DocumentTypes"; +import { CurrentUserUtils } from '../../util/CurrentUserUtils'; +import { DocumentManager } from '../../util/DocumentManager'; +import { DragManager, dropActionType } from "../../util/DragManager"; +import { SelectionManager } from '../../util/SelectionManager'; +import { SnappingManager } from '../../util/SnappingManager'; +import { Transform } from '../../util/Transform'; +import { undoBatch, UndoManager } from '../../util/UndoManager'; +import { EditableView } from "../EditableView"; +import { ContentFittingDocumentView } from '../nodes/ContentFittingDocumentView'; +import { DocumentView } from '../nodes/DocumentView'; +import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; +import { RichTextMenu } from '../nodes/formattedText/RichTextMenu'; +import { KeyValueBox } from '../nodes/KeyValueBox'; +import { CollectionTreeView } from './CollectionTreeView'; +import { CollectionView, CollectionViewType } from './CollectionView'; +import "./TreeView.scss"; +import React = require("react"); + +export interface TreeViewProps { + document: Doc; + dataDoc?: Doc; + containingCollection: Doc; + prevSibling?: Doc; + renderDepth: number; + removeDoc: ((doc: Doc | Doc[]) => boolean) | undefined; + moveDocument: DragManager.MoveFunction; + dropAction: dropActionType; + addDocTab: (doc: Doc, where: string, libraryPath?: Doc[]) => boolean; + pinToPres: (document: Doc) => void; + panelWidth: () => number; + panelHeight: () => number; + ChromeHeight: undefined | (() => number); + addDocument: (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => boolean; + indentDocument?: () => void; + outdentDocument?: () => void; + ScreenToLocalTransform: () => Transform; + dontRegisterView?: boolean; + backgroundColor?: (doc: Opt<Doc>, renderDepth: number) => string | undefined; + outerXf: () => { translateX: number, translateY: number }; + treeView: CollectionTreeView; + parentKey: string; + active: (outsideReaction?: boolean) => boolean; + treeViewHideHeaderFields: () => boolean; + treeViewPreventOpen: boolean; + renderedIds: string[]; // list of document ids rendered used to avoid unending expansion of items in a cycle + onCheckedClick?: () => ScriptField; + onChildClick?: () => ScriptField; + ignoreFields?: string[]; + firstLevel: boolean; + whenActiveChanged: (isActive: boolean) => void; +} + +@observer +/** + * Renders a treeView of a collection of documents + * + * special fields: + * treeViewOpen : flag denoting whether the documents sub-tree (contents) is visible or hidden + * treeViewPreventOpen : ignores the treeViewOpen flag (for allowing a view to not be slaved to other views of the document) + * treeViewExpandedView : name of field whose contents are being displayed as the document's subtree + */ +export class TreeView extends React.Component<TreeViewProps> { + private _editTitleScript: (() => ScriptField) | undefined; + private _openScript: (() => ScriptField) | undefined; + private _header?: React.RefObject<HTMLDivElement> = React.createRef(); + private _treedropDisposer?: DragManager.DragDropDisposer; + private _dref = React.createRef<HTMLDivElement>(); + private _tref = React.createRef<HTMLDivElement>(); + private _docRef = React.createRef<DocumentView>(); + private _uniqueId = Utils.GenerateGuid(); + private _editMaxWidth: number | string = 0; + + @computed get doc() { TraceMobx(); return this.props.document; } + get noviceMode() { return BoolCast(Doc.UserDoc().noviceMode, false); } + get displayName() { return "TreeView(" + this.props.document.title + ")"; } // this makes mobx trace() statements more descriptive + get treeViewLockExpandedView() { return this.doc.treeViewLockExpandedView; } + get defaultExpandedView() { return StrCast(this.doc.treeViewDefaultExpandedView, this.noviceMode || this.outlineMode ? "layout" : "fields"); } + get treeViewDefaultExpandedView() { return this.treeViewLockExpandedView ? this.defaultExpandedView : (this.childDocs ? this.fieldKey : this.defaultExpandedView); } + @observable _overrideTreeViewOpen = false; // override of the treeViewOpen field allowing the display state to be independent of the document's state + set treeViewOpen(c: boolean) { + if (this.props.treeViewPreventOpen) this._overrideTreeViewOpen = c; + else this.doc.treeViewOpen = this._overrideTreeViewOpen = c; + } + @computed get outlineMode() { return this.props.treeView.doc.treeViewOutlineMode; } + @computed get treeViewOpen() { return (!this.props.treeViewPreventOpen && !this.doc.treeViewPreventOpen && BoolCast(this.doc.treeViewOpen)) || this._overrideTreeViewOpen; } + @computed get treeViewExpandedView() { return StrCast(this.doc.treeViewExpandedView, this.treeViewDefaultExpandedView); } + @computed get MAX_EMBED_HEIGHT() { return NumCast(this.props.containingCollection.maxEmbedHeight, 200); } + @computed get dataDoc() { return this.doc[DataSym]; } + @computed get layoutDoc() { return Doc.Layout(this.doc); } + @computed get fieldKey() { TraceMobx(); const splits = StrCast(Doc.LayoutField(this.doc)).split("fieldKey={\'"); return splits.length > 1 ? splits[1].split("\'")[0] : "data"; } + childDocList(field: string) { + const layout = Doc.LayoutField(this.doc) instanceof Doc ? Doc.LayoutField(this.doc) as Doc : undefined; + return ((this.props.dataDoc ? DocListCastOrNull(this.props.dataDoc[field]) : undefined) || // if there's a data doc for an expanded template, use it's data field + (layout ? DocListCastOrNull(layout[field]) : undefined) || // else if there's a layout doc, display it's fields + DocListCastOrNull(this.doc[field])); // otherwise use the document's data field + } + @computed get childDocs() { TraceMobx(); return this.childDocList(this.fieldKey); } + @computed get childLinks() { return this.childDocList("links"); } + @computed get childAnnos() { return this.childDocList(this.fieldKey + "-annotations"); } + @computed get boundsOfCollectionDocument() { + return StrCast(this.props.document.type).indexOf(DocumentType.COL) === -1 || !DocListCast(this.props.document[this.fieldKey]).length ? undefined : + Doc.ComputeContentBounds(DocListCast(this.props.document[this.fieldKey])); + } + + @undoBatch openRight = () => this.props.addDocTab(this.doc, "add:right"); + @undoBatch move = (doc: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => { + return this.doc !== target && this.props.removeDoc?.(doc) === true && addDoc(doc); + } + @undoBatch @action remove = (doc: Doc | Doc[], key: string) => { + this.props.treeView.props.select(false); + const ind = this.dataDoc[key].indexOf(doc); + const res = (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && Doc.RemoveDocFromList(this.dataDoc, key, doc), true); + res && ind > 0 && DocumentManager.Instance.getDocumentView(this.dataDoc[key][ind - 1], this.props.treeView.props.CollectionView)?.select(false); + return res; + } + @undoBatch @action removeDoc = (doc: Doc | Doc[]) => this.remove(doc, Doc.LayoutFieldKey(this.doc)); + + constructor(props: any) { + super(props); + const titleScript = ScriptField.MakeScript(`{setInPlace(self, 'editTitle', '${this._uniqueId}'); documentView.select();} `, { documentView: "any" }); + const openScript = ScriptField.MakeScript(`openOnRight(self)`); + const treeOpenScript = ScriptField.MakeScript(`self.treeViewOpen = !self.treeViewOpen`); + this._editTitleScript = !Doc.IsSystem(this.props.document) ? titleScript && (() => titleScript) : treeOpenScript && (() => treeOpenScript); + this._openScript = !Doc.IsSystem(this.props.document) ? openScript && (() => openScript) : undefined; + if (Doc.GetT(this.props.document, "editTitle", "string", true) === "*") Doc.SetInPlace(this.props.document, "editTitle", this._uniqueId, false); + } + + protected createTreeDropTarget = (ele: HTMLDivElement) => { + this._treedropDisposer?.(); + ele && (this._treedropDisposer = DragManager.MakeDropTarget(ele, this.treeDrop.bind(this), undefined, this.preTreeDrop.bind(this)), this.doc); + } + + componentWillUnmount() { + document.removeEventListener("pointermove", this.onDragMove, true); + document.removeEventListener("pointermove", this.onDragUp, true); + } + + onDragUp = (e: PointerEvent) => { + document.removeEventListener("pointerup", this.onDragUp, true); + document.removeEventListener("pointermove", this.onDragMove, true); + } + onPointerEnter = (e: React.PointerEvent): void => { + this.props.active(true) && Doc.BrushDoc(this.dataDoc); + if (e.buttons === 1 && SnappingManager.GetIsDragging()) { + this._header!.current!.className = "treeView-header"; + document.removeEventListener("pointermove", this.onDragMove, true); + document.addEventListener("pointermove", this.onDragMove, true); + document.removeEventListener("pointerup", this.onDragUp, true); + document.addEventListener("pointerup", this.onDragUp, true); + } + } + onPointerLeave = (e: React.PointerEvent): void => { + Doc.UnBrushDoc(this.dataDoc); + if (this._header?.current?.className !== "treeView-header-editing") { + this._header!.current!.className = "treeView-header"; + } + document.removeEventListener("pointerup", this.onDragUp, true); + document.removeEventListener("pointermove", this.onDragMove, true); + } + onDragMove = (e: PointerEvent): void => { + Doc.UnBrushDoc(this.dataDoc); + const pt = [e.clientX, e.clientY]; + const rect = this._header!.current!.getBoundingClientRect(); + const before = pt[1] < rect.top + rect.height / 2; + const inside = pt[0] > Math.min(rect.left + 75, rect.left + rect.width * .75) || (!before && this.treeViewOpen && this.childDocList.length); + this._header!.current!.className = "treeView-header"; + if (inside) this._header!.current!.className += " treeView-header-inside"; + else if (before) this._header!.current!.className += " treeView-header-above"; + else if (!before) this._header!.current!.className += " treeView-header-below"; + e.stopPropagation(); + } + + public static makeTextBullet() { + const bullet = Docs.Create.TextDocument("-text-", { title: "-title-", "sidebarColor": "transparent", "sidebarViewType": CollectionViewType.Freeform, forceActive: true, _viewType: CollectionViewType.Tree, hideLinkButton: true, _showSidebar: true, treeViewOutlineMode: true, x: 0, y: 0, _xMargin: 0, _yMargin: 0, _autoHeight: true, _singleLine: true, _backgroundColor: "transparent", _width: 1000, _height: 10 }); + Doc.GetProto(bullet).layout = CollectionView.LayoutString("data"); + Doc.GetProto(bullet).title = ComputedField.MakeFunction('self.text?.Text'); + Doc.GetProto(bullet).data = new List<Doc>([]); + Doc.SetInPlace(bullet, "editTitle", "*", false); + FormattedTextBox.SelectOnLoad = bullet[Id]; + return bullet; + } + + makeTextCollection = () => { + Doc.SetInPlace(this.doc, "editTitle", undefined, false); + const bullet = TreeView.makeTextBullet(); + const added = this.props.addDocument(bullet); + bullet.context = this.props.treeView.Document; + return added; + } + + editableView = (key: string, style?: string) => (<EditableView + oneLine={true} + display={"inline-block"} + editing={true} + contents={StrCast(this.doc[key])} + height={12} + sizeToContent={true} + fontStyle={style} + fontSize={12} + GetValue={() => StrCast(this.doc[key])} + SetValue={undoBatch((value: string, shiftKey: boolean, enterKey: boolean) => { + Doc.SetInPlace(this.doc, key, value, false); + if (this.outlineMode && enterKey) { + this.makeTextCollection(); + } else { + Doc.SetInPlace(this.doc, "editTitle", undefined, false); + } + })} + onClick={() => { + SelectionManager.DeselectAll(); + return false; + }} + OnEmpty={undoBatch(() => this.props.treeView.doc.treeViewOutlineMode && this.props.removeDoc?.(this.doc))} + OnTab={undoBatch((shift?: boolean) => { + shift ? this.props.outdentDocument?.() : this.props.indentDocument?.(); + setTimeout(() => Doc.SetInPlace(this.doc, "editTitle", `${this.props.treeView._uniqueId}`, false), 0); + })} + />) + + preTreeDrop = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => { + const dragData = de.complete.docDragData; + dragData && (dragData.dropAction = this.props.treeView.props.Document === dragData.treeViewDoc ? "same" : dragData.dropAction); + } + + @undoBatch + treeDrop = (e: Event, de: DragManager.DropEvent) => { + const pt = [de.x, de.y]; + const rect = this._header!.current!.getBoundingClientRect(); + const before = pt[1] < rect.top + rect.height / 2; + const inside = pt[0] > Math.min(rect.left + 75, rect.left + rect.width * .75) || (!before && this.treeViewOpen && this.childDocList.length); + if (de.complete.linkDragData) { + const sourceDoc = de.complete.linkDragData.linkSourceDocument; + const destDoc = this.doc; + DocUtils.MakeLink({ doc: sourceDoc }, { doc: destDoc }, "tree link", ""); + e.stopPropagation(); + } + const docDragData = de.complete.docDragData; + if (docDragData) { + e.stopPropagation(); + if (docDragData.draggedDocuments[0] === this.doc) return true; + const parentAddDoc = (doc: Doc | Doc[]) => this.props.addDocument(doc, undefined, before); + let addDoc = parentAddDoc; + if (inside) { + const localAdd = (doc: Doc) => { + const added = Doc.AddDocToList(this.dataDoc, this.fieldKey, doc); + added && (doc.context = this.doc.context); + return added; + }; + addDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce( + (flg: boolean, doc) => flg && localAdd(doc), true) || parentAddDoc(doc); + } + const move = (!docDragData.dropAction || docDragData.dropAction === "move" || docDragData.dropAction === "same") && docDragData.moveDocument; + return docDragData.droppedDocuments.reduce((added, d) => (move ? docDragData.moveDocument?.(d, undefined, addDoc) : addDoc(d)) || added, false); + } + return false; + } + + refTransform = (ref: HTMLDivElement) => { + const { scale, translateX, translateY } = Utils.GetScreenTransform(ref); + const outerXf = this.props.outerXf(); + const offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY); + return this.props.ScreenToLocalTransform().translate(offset[0], offset[1]); + } + docTransform = () => this.refTransform(this._dref.current!); + getTransform = () => this.refTransform(this._tref.current!); + docWidth = () => { + const layoutDoc = this.layoutDoc; + const aspect = NumCast(layoutDoc._nativeHeight, layoutDoc._fitWidth ? 0 : layoutDoc[HeightSym]()) / NumCast(layoutDoc._nativeWidth, layoutDoc._fitWidth ? 1 : layoutDoc[WidthSym]()); + if (aspect) return Math.min(layoutDoc[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT / aspect, this.props.panelWidth() - 20)); + return NumCast(layoutDoc._nativeWidth) ? Math.min(layoutDoc[WidthSym](), this.props.panelWidth() - 20) : this.props.panelWidth() - 20; + } + docHeight = () => { + const layoutDoc = this.layoutDoc; + const bounds = this.boundsOfCollectionDocument; + return Math.max(70, Math.min(this.MAX_EMBED_HEIGHT, (() => { + const aspect = NumCast(layoutDoc._nativeHeight, layoutDoc._fitWidth ? 0 : layoutDoc[HeightSym]()) / NumCast(layoutDoc._nativeWidth, layoutDoc._fitWidth ? 1 : layoutDoc[WidthSym]()); + if (aspect) return this.docWidth() * aspect; + if (bounds) return this.docWidth() * (bounds.b - bounds.y) / (bounds.r - bounds.x); + return layoutDoc._fitWidth ? (!this.doc._nativeHeight ? NumCast(this.props.containingCollection._height) : + Math.min(this.docWidth() * NumCast(layoutDoc.scrollHeight, NumCast(layoutDoc._nativeHeight)) / NumCast(layoutDoc._nativeWidth, + NumCast(this.props.containingCollection._height)))) : + NumCast(layoutDoc._height) ? NumCast(layoutDoc._height) : 50; + })())); + } + + @computed get expandedField() { + const ids: { [key: string]: string } = {}; + const rows: JSX.Element[] = []; + const doc = this.doc; + doc && Object.keys(doc).forEach(key => !(key in ids) && doc[key] !== ComputedField.undefined && (ids[key] = key)); + + for (const key of Object.keys(ids).slice().sort()) { + if (this.props.ignoreFields?.includes(key) || key === "title" || key === "treeViewOpen") continue; + const contents = doc[key]; + let contentElement: (JSX.Element | null)[] | JSX.Element = []; + + if (contents instanceof Doc || (Cast(contents, listSpec(Doc)) && (Cast(contents, listSpec(Doc))!.length && Cast(contents, listSpec(Doc))![0] instanceof Doc))) { + const remDoc = (doc: Doc | Doc[]) => this.remove(doc, key); + const localAdd = (doc: Doc, addBefore?: Doc, before?: boolean) => { + const added = Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false, true); + added && (doc.context = this.doc.context); + return added; + }; + const addDoc = (doc: Doc | Doc[], addBefore?: Doc, before?: boolean) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && localAdd(doc, addBefore, before), true); + contentElement = TreeView.GetChildElements(contents instanceof Doc ? [contents] : DocListCast(contents), + this.props.treeView, doc, undefined, key, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move, + this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, + this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.treeViewHideHeaderFields, this.props.treeViewPreventOpen, + [...this.props.renderedIds, doc[Id]], this.props.onCheckedClick, this.props.onChildClick, this.props.ignoreFields, false, this.props.whenActiveChanged, this.props.dontRegisterView); + } else { + contentElement = <EditableView key="editableView" + contents={contents !== undefined ? Field.toString(contents as Field) : "null"} + height={13} + fontSize={12} + GetValue={() => Field.toKeyValueString(doc, key)} + SetValue={(value: string) => KeyValueBox.SetField(doc, key, value, true)} />; + } + rows.push(<div style={{ display: "flex" }} key={key}> + <span style={{ fontWeight: "bold" }}>{key + ":"}</span> + + {contentElement} + </div>); + } + rows.push(<div style={{ display: "flex" }} key={"newKeyValue"}> + <EditableView + key="editableView" + contents={"+key:value"} + height={13} + fontSize={12} + GetValue={returnEmptyString} + SetValue={value => value.indexOf(":") !== -1 && KeyValueBox.SetField(doc, value.substring(0, value.indexOf(":")), value.substring(value.indexOf(":") + 1, value.length), true)} /> + </div>); + return rows; + } + + rtfWidth = () => Math.min(this.layoutDoc?.[WidthSym](), this.props.panelWidth() - 20); + rtfHeight = () => this.rtfWidth() <= this.layoutDoc?.[WidthSym]() ? Math.min(this.layoutDoc?.[HeightSym](), this.MAX_EMBED_HEIGHT) : this.MAX_EMBED_HEIGHT; + rtfOutlineHeight = () => Math.max(this.layoutDoc?.[HeightSym](), 20); + + @computed get renderContent() { + TraceMobx(); + const expandKey = this.treeViewExpandedView; + if (["links", "annotations", this.fieldKey].includes(expandKey)) { + const remDoc = (doc: Doc | Doc[]) => this.remove(doc, expandKey); + const localAdd = (doc: Doc, addBefore?: Doc, before?: boolean) => { + const added = Doc.AddDocToList(this.dataDoc, expandKey, doc, addBefore, before, false, true); + added && (doc.context = this.doc.context); + return added; + }; + const addDoc = (doc: Doc | Doc[], addBefore?: Doc, before?: boolean) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && localAdd(doc, addBefore, before), true); + const docs = expandKey === "links" ? this.childLinks : expandKey === "annotations" ? this.childAnnos : this.childDocs; + const sortKey = `${this.fieldKey}-sortAscending`; + return <ul key={expandKey + "more"} className={this.doc.treeViewHideTitle ? "no-indent" : ""} onClick={(e) => { + !this.outlineMode && (this.doc[sortKey] = (this.doc[sortKey] ? false : (this.doc[sortKey] === false ? undefined : true))); + e.stopPropagation(); + }}> + {!docs ? (null) : + TreeView.GetChildElements(docs, this.props.treeView, this.layoutDoc, + this.dataDoc, expandKey, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move, + StrCast(this.doc.childDropAction, this.props.dropAction) as dropActionType, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform, + this.props.outerXf, this.props.active, this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.treeViewHideHeaderFields, this.props.treeViewPreventOpen, + [...this.props.renderedIds, this.doc[Id]], this.props.onCheckedClick, this.props.onChildClick, this.props.ignoreFields, false, this.props.whenActiveChanged, this.props.dontRegisterView)} + </ul >; + } else if (this.treeViewExpandedView === "fields") { + return <ul key={this.doc[Id] + this.doc.title}><div ref={this._dref} style={{ display: "inline-block" }} > + {this.expandedField} + </div></ul>; + } else { + const layoutDoc = this.layoutDoc; + const panelHeight = StrCast(Doc.LayoutField(layoutDoc)).includes("FormattedTextBox") ? this.rtfHeight : this.docHeight; + const panelWidth = StrCast(Doc.LayoutField(layoutDoc)).includes("FormattedTextBox") ? this.rtfWidth : this.docWidth; + return <div ref={this._dref} style={{ display: "inline-block", height: panelHeight() }} key={this.doc[Id]}> + <ContentFittingDocumentView + Document={this.doc} + DataDoc={undefined} + LibraryPath={emptyPath} + dontRegisterView={this.props.dontRegisterView} + renderDepth={this.props.renderDepth + 1} + rootSelected={returnTrue} + treeViewDoc={undefined} + backgroundColor={this.props.backgroundColor} + fitToBox={this.boundsOfCollectionDocument !== undefined} + FreezeDimensions={true} + NativeWidth={layoutDoc.type === DocumentType.RTF ? this.rtfWidth : undefined} + NativeHeight={layoutDoc.type === DocumentType.RTF ? this.rtfHeight : undefined} + PanelWidth={panelWidth} + PanelHeight={panelHeight} + focus={returnFalse} + ScreenToLocalTransform={this.docTransform} + docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + ContainingCollectionDoc={this.props.containingCollection} + ContainingCollectionView={undefined} + addDocument={returnFalse} + moveDocument={this.move} + removeDocument={this.props.removeDoc} + parentActive={this.props.active} + whenActiveChanged={this.props.whenActiveChanged} + addDocTab={this.props.addDocTab} + pinToPres={this.props.pinToPres} + bringToFront={returnFalse} + ContentScaling={returnOne} + /> + </div>; + } + } + + get onCheckedClick() { return this.doc.type === DocumentType.COL ? undefined : this.props.onCheckedClick?.() ?? ScriptCast(this.doc.onCheckedClick); } + + @action + bulletClick = (e: React.MouseEvent) => { + if (this.onCheckedClick) { + this.onCheckedClick?.script.run({ + this: this.doc.isTemplateForField && this.props.dataDoc ? this.props.dataDoc : this.doc, + heading: this.props.containingCollection.title, + checked: this.doc.treeViewChecked === "check" ? "x" : this.doc.treeViewChecked === "x" ? undefined : "check", + containingTreeView: this.props.treeView.props.Document, + }, console.log); + } else { + this.treeViewOpen = !this.treeViewOpen; + } + e.stopPropagation(); + } + + @computed get renderBullet() { + TraceMobx(); + const checked = this.onCheckedClick ? (this.doc.treeViewChecked ?? "unchecked") : undefined; + return <div className={`bullet${this.outlineMode ? "-outline" : ""}`} title={this.childDocs?.length ? `click to see ${this.childDocs?.length} items` : "view fields"} + onClick={this.bulletClick} + style={this.outlineMode ? { opacity: NumCast(this.doc.opacity, 1) } : { + color: StrCast(this.doc.color, checked === "unchecked" ? "white" : "inherit"), + opacity: checked === "unchecked" ? undefined : 0.4 + }}> + {this.outlineMode && !(this.doc.text as RichTextField)?.Text ? (null) : + <FontAwesomeIcon icon={this.outlineMode ? [this.childDocs?.length && !this.treeViewOpen ? "fas" : "far", "circle"] : + checked === "check" ? "check" : + (checked === "x" ? "times" : checked === "unchecked" ? "square" : + !this.treeViewOpen ? (this.childDocs?.length ? "caret-square-right" : "caret-right") : + (this.childDocs?.length ? "caret-square-down" : "caret-down"))} />} + </div>; + } + @computed get showTitleEditorControl() { return ["*", this._uniqueId, this.props.treeView._uniqueId].includes(Doc.GetT(this.doc, "editTitle", "string", true) || ""); } + @computed get headerElements() { + return (Doc.IsSystem(this.doc) && Doc.UserDoc().noviceMode) || this.props.treeViewHideHeaderFields() ? (null) : + <> + <FontAwesomeIcon key="bars" icon="bars" size="sm" onClick={e => { this.showContextMenu(e); e.stopPropagation(); }} /> + <span className="collectionTreeView-keyHeader" key={this.treeViewExpandedView} + onPointerDown={action(() => { + if (this.treeViewOpen) { + this.doc.treeViewExpandedView = this.treeViewLockExpandedView ? this.doc.treeViewExpandedView : + this.treeViewExpandedView === this.fieldKey ? (Doc.UserDoc().noviceMode || this.outlineMode ? "layout" : "fields") : + this.treeViewExpandedView === "fields" && this.layoutDoc ? "layout" : + this.treeViewExpandedView === "layout" && DocListCast(this.doc.links).length ? "links" : + (this.treeViewExpandedView === "links" || this.treeViewExpandedView === "layout") && DocListCast(this.doc[this.fieldKey + "-annotations"]).length ? "annotations" : + this.childDocs ? this.fieldKey : (Doc.UserDoc().noviceMode || this.outlineMode ? "layout" : "fields"); + } + this.treeViewOpen = true; + })}> + {this.treeViewExpandedView} + </span> + </>; + } + + showContextMenu = (e: React.MouseEvent) => simulateMouseClick(this._docRef.current?.ContentDiv, e.clientX, e.clientY + 30, e.screenX, e.screenY + 30); + contextMenuItems = () => Doc.IsSystem(this.doc) ? [] : [{ script: ScriptField.MakeFunction(`openOnRight(self)`)!, label: "Open" }, { script: ScriptField.MakeFunction(`DocFocus(self)`)!, label: "Focus" }]; + truncateTitleWidth = () => NumCast(this.props.treeView.props.Document.treeViewTruncateTitleWidth, 0); + onChildClick = () => this.props.onChildClick?.() ?? (this._editTitleScript?.() || ScriptCast(this.doc.treeChildClick)); + onChildDoubleClick = () => (!this.outlineMode && this._openScript?.()) || ScriptCast(this.doc.treeChildDoubleClick); + /** + * Renders the EditableView title element for placement into the tree. + */ + @computed + get renderTitle() { + TraceMobx(); + const view = this.showTitleEditorControl ? this.editableView("title") : + <DocumentView + ref={this._docRef} + Document={this.doc} + DataDoc={undefined} + treeViewDoc={this.props.treeView.props.Document} + LibraryPath={emptyPath} + addDocument={undefined} + addDocTab={this.props.addDocTab} + rootSelected={returnTrue} + pinToPres={emptyFunction} + onClick={this.onChildClick} + onDoubleClick={this.onChildDoubleClick} + dropAction={this.props.dropAction} + moveDocument={this.move} + removeDocument={this.props.removeDoc} + ScreenToLocalTransform={this.getTransform} + ContentScaling={returnOne} + PanelWidth={this.truncateTitleWidth} + PanelHeight={returnZero} + contextMenuItems={this.contextMenuItems} + opacity={this.outlineMode ? undefined : returnOne} + renderDepth={1} + focus={returnTrue} + parentActive={returnTrue} + whenActiveChanged={this.props.whenActiveChanged} + bringToFront={emptyFunction} + dontRegisterView={BoolCast(this.props.treeView.props.Document.dontRegisterChildViews)} + docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + ContainingCollectionView={undefined} + ContainingCollectionDoc={this.props.containingCollection} + />; + return <> + <div className={`docContainer${Doc.IsSystem(this.props.document) ? "-system" : ""}`} ref={this._tref} title="click to edit title. Double Click or Drag to Open" + style={{ + fontWeight: Doc.IsSearchMatch(this.doc) !== undefined ? "bold" : undefined, + textDecoration: Doc.GetT(this.doc, "title", "string", true) ? "underline" : undefined, + outline: this.doc === CurrentUserUtils.ActiveDashboard ? "dashed 1px #06123232" : undefined, + pointerEvents: !this.props.active() && !SnappingManager.GetIsDragging() ? "none" : undefined + }} > + {view} + </div > + {this.headerElements} + </>; + } + + refocus = () => this.props.treeView.props.focus(this.props.treeView.props.Document); + + render() { + TraceMobx(); + if (this.props.renderedIds.indexOf(this.doc[Id]) !== -1) return null; + const sorting = this.doc[`${this.fieldKey}-sortAscending`]; + if (this.showTitleEditorControl) { // find containing CollectionTreeView and set our maximum width so the containing tree view won't have to scroll + let par: any = this._header?.current; + while (par && par.className !== "collectionTreeView-dropTarget") par = par.parentNode; + if (par) { + const par_rect = (par as HTMLElement).getBoundingClientRect(); + const my_recct = this._docRef.current?.ContentDiv?.getBoundingClientRect(); + this._editMaxWidth = Math.max(100, par_rect.right - (my_recct?.left || 0)); + } + } + else this._editMaxWidth = ""; + const selected = SelectionManager.IsSelected(DocumentManager.Instance.getFirstDocumentView(this.doc)); + return this.doc.treeViewHideHeader || this.outlineMode ? + !StrCast(Doc.LayoutField(this.doc)).includes("CollectionView") ? + this.renderContent + : <div className={`treeView-container${selected ? "-active" : ""}`} ref={this.createTreeDropTarget} onPointerDown={e => this.props.active(true) && SelectionManager.DeselectAll()} + onKeyDown={e => { + e.stopPropagation(); + e.preventDefault(); + switch (e.key) { + case "Backspace": return !(this.doc.text as RichTextField)?.Text && this.props.removeDoc?.(this.doc); + case "Enter": return UndoManager.RunInBatch(() => this.makeTextCollection(), "bullet"); + case "Tab": setTimeout(() => RichTextMenu.Instance.TextView?.EditorView?.focus(), 150); + return UndoManager.RunInBatch(() => e.shiftKey ? this.props.outdentDocument?.() : this.props.indentDocument?.(), "tab"); + } + }} > + <div className={`treeView-header` + (this._editMaxWidth ? "-editing" : "")} ref={this._header} style={{ alignItems: this.outlineMode ? "center" : undefined, maxWidth: this._editMaxWidth }} + onClick={e => { if (this.props.active(true)) { e.stopPropagation(); e.preventDefault(); } }} + onPointerDown={e => { if (this.props.active(true)) { e.stopPropagation(); e.preventDefault(); } }} + onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}> + {this.renderBullet} + <div ref={this._dref} style={{ display: "inline-block", height: this.rtfOutlineHeight() }} key={this.doc[Id]}> + <ContentFittingDocumentView + Document={this.doc} + DataDoc={undefined} + LayoutTemplateString={FormattedTextBox.LayoutString("text")} + LibraryPath={emptyPath} + renderDepth={this.props.renderDepth + 1} + rootSelected={returnTrue} + treeViewDoc={undefined} + backgroundColor={this.props.backgroundColor} + fitToBox={this.boundsOfCollectionDocument !== undefined} + PanelWidth={this.rtfWidth} + PanelHeight={this.rtfOutlineHeight} + focus={this.refocus} + ScreenToLocalTransform={this.docTransform} + docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + ContainingCollectionDoc={this.props.containingCollection} + ContainingCollectionView={undefined} + addDocument={this.props.addDocument} + moveDocument={this.move} + removeDocument={this.props.removeDoc} + parentActive={this.props.active} + whenActiveChanged={this.props.whenActiveChanged} + addDocTab={this.props.addDocTab} + pinToPres={this.props.pinToPres} + bringToFront={returnFalse} + ContentScaling={returnOne} + /> + </div> + </div> + + <div className={`treeView-border${this.outlineMode ? "outline" : ""}`} style={{ borderColor: sorting === undefined ? undefined : sorting ? "crimson" : "blue" }}> + {!this.treeViewOpen ? (null) : this.renderContent} + </div> + </div> : + <div className="treeView-container" ref={this.createTreeDropTarget} onPointerDown={e => this.props.active(true) && SelectionManager.DeselectAll()}> + <li className="collection-child"> + <div className={`treeView-header` + (this._editMaxWidth ? "-editing" : "")} ref={this._header} style={{ maxWidth: this._editMaxWidth }} onClick={e => { + if (this.props.active(true)) { + e.stopPropagation(); + e.preventDefault(); + SelectionManager.DeselectAll(); + } + }} + onPointerDown={e => { + if (this.props.active(true)) { + e.stopPropagation(); + e.preventDefault(); + } + }} + onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}> + {this.renderBullet} + {this.renderTitle} + </div> + <div className={`treeView-border${this.outlineMode ? "outline" : ""}`} style={{ borderColor: sorting === undefined ? undefined : sorting ? "crimson" : "blue" }}> + {!this.treeViewOpen ? (null) : this.renderContent} + </div> + </li> + </div>; + } + + + public static GetChildElements( + childDocs: Doc[], + treeView: CollectionTreeView, + containingCollection: Doc, + dataDoc: Doc | undefined, + key: string, + parentCollectionDoc: Doc | undefined, + parentPrevSibling: Doc | undefined, + add: (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => boolean, + remove: ((doc: Doc | Doc[]) => boolean), + move: DragManager.MoveFunction, + dropAction: dropActionType, + addDocTab: (doc: Doc, where: string) => boolean, + pinToPres: (document: Doc) => void, + backgroundColor: undefined | ((document: Opt<Doc>, renderDepth: number) => string | undefined), + screenToLocalXf: () => Transform, + outerXf: () => { translateX: number, translateY: number }, + active: (outsideReaction?: boolean) => boolean, + panelWidth: () => number, + ChromeHeight: undefined | (() => number), + renderDepth: number, + treeViewHideHeaderFields: () => boolean, + treeViewPreventOpen: boolean, + renderedIds: string[], + onCheckedClick: undefined | (() => ScriptField), + onChildClick: undefined | (() => ScriptField), + ignoreFields: string[] | undefined, + firstLevel: boolean, + whenActiveChanged: (isActive: boolean) => void, + dontRegisterView: boolean | undefined) { + const viewSpecScript = Cast(containingCollection.viewSpecScript, ScriptField); + if (viewSpecScript) { + childDocs = childDocs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result); + } + + const docs = childDocs.slice(); + const ascending = containingCollection?.[key + "-sortAscending"]; + if (ascending !== undefined) { + const sortAlphaNum = (a: string, b: string): 0 | 1 | -1 => { + const reN = /[0-9]*$/; + const aA = a.replace(reN, ""); // get rid of trailing numbers + const bA = b.replace(reN, ""); + if (aA === bA) { // if header string matches, then compare numbers numerically + const aN = parseInt(a.match(reN)![0], 10); + const bN = parseInt(b.match(reN)![0], 10); + return aN === bN ? 0 : aN > bN ? 1 : -1; + } else { + return aA > bA ? 1 : -1; + } + }; + docs.sort(function (a, b): 0 | 1 | -1 { + const first = (ascending ? b : a).title; + const second = (ascending ? a : b).title; + if (typeof first === 'number' && typeof second === 'number') return (first - second) > 0 ? 1 : -1; + if (typeof first === 'string' && typeof second === 'string') return sortAlphaNum(first, second); + return ascending ? 1 : -1; + }); + } + + const rowWidth = () => panelWidth() - 20; + return docs.filter(child => child instanceof Doc).map((child, i) => { + const pair = Doc.GetLayoutDataDocPair(containingCollection, dataDoc, child); + if (!pair.layout || pair.data instanceof Promise) { + return (null); + } + + const indent = i === 0 ? undefined : () => { + if (remove && StrCast(docs[i - 1].layout).indexOf('fieldKey') !== -1) { + const fieldKeysub = StrCast(docs[i - 1].layout).split('fieldKey')[1]; + const fieldKey = fieldKeysub.split("\'")[1]; + if (fieldKey && Cast(docs[i - 1][fieldKey], listSpec(Doc)) !== undefined) { + remove(child); + FormattedTextBox.SelectOnLoad = child[Id]; + Doc.AddDocToList(docs[i - 1], fieldKey, child); + docs[i - 1].treeViewOpen = true; + child.context = treeView.Document; + } + } + }; + const outdent = !parentCollectionDoc ? undefined : () => { + if (parentCollectionDoc._viewType === CollectionViewType.Tree && remove && StrCast(parentCollectionDoc.layout).indexOf('fieldKey') !== -1) { + const fieldKeysub = StrCast(parentCollectionDoc.layout).split('fieldKey')[1]; + const fieldKey = fieldKeysub.split("\'")[1]; + remove(child); + FormattedTextBox.SelectOnLoad = child[Id]; + Doc.AddDocToList(parentCollectionDoc, fieldKey, child, parentPrevSibling, false); + parentCollectionDoc.treeViewOpen = true; + child.context = treeView.Document; + } + }; + const addDocument = (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => add(doc, relativeTo ?? docs[i], before !== undefined ? before : false); + const childLayout = Doc.Layout(pair.layout); + const rowHeight = () => { + const aspect = NumCast(childLayout._nativeWidth, 0) / NumCast(childLayout._nativeHeight, 0); + return aspect ? Math.min(childLayout[WidthSym](), rowWidth()) / aspect : childLayout[HeightSym](); + }; + return <TreeView key={child[Id]} + document={pair.layout} + dataDoc={pair.data} + containingCollection={containingCollection} + prevSibling={docs[i]} + treeView={treeView} + indentDocument={indent} + outdentDocument={!parentCollectionDoc ? undefined : outdent} + onCheckedClick={onCheckedClick} + onChildClick={onChildClick} + renderDepth={renderDepth} + removeDoc={StrCast(containingCollection.freezeChildren).includes("remove") ? undefined : remove} + addDocument={addDocument} + backgroundColor={backgroundColor} + panelWidth={rowWidth} + panelHeight={rowHeight} + ChromeHeight={ChromeHeight} + dontRegisterView={dontRegisterView} + moveDocument={move} + dropAction={dropAction} + addDocTab={addDocTab} + pinToPres={pinToPres} + ScreenToLocalTransform={screenToLocalXf} + outerXf={outerXf} + parentKey={key} + active={active} + treeViewHideHeaderFields={treeViewHideHeaderFields} + treeViewPreventOpen={treeViewPreventOpen} + renderedIds={renderedIds} + ignoreFields={ignoreFields} + firstLevel={firstLevel} + whenActiveChanged={whenActiveChanged} />; + }); + } +}
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx index b00074cc6..bc2cb2d20 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx @@ -10,6 +10,7 @@ import { Id, ToString } from "../../../../fields/FieldSymbols"; import { ObjectField } from "../../../../fields/ObjectField"; import { RefField } from "../../../../fields/RefField"; import { listSpec } from "../../../../fields/Schema"; +import { CurrentUserUtils } from "../../../util/CurrentUserUtils"; export interface ViewDefBounds { type: string; @@ -359,7 +360,7 @@ export function computeTimelineLayout( groupNames.push({ type: "text", text: toLabel(Math.ceil(maxTime)), x: Math.ceil(maxTime - minTime) * scaling, y: 0, height: fontHeight, fontSize, payload: undefined }); } - const divider = { type: "div", color: Cast(Doc.UserDoc().activeWorkspace, Doc, null)?.darkScheme ? "dimGray" : "black", x: 0, y: 0, width: panelDim[0], height: -1, payload: undefined }; + const divider = { type: "div", color: CurrentUserUtils.ActiveDashboard?.darkScheme ? "dimGray" : "black", x: 0, y: 0, width: panelDim[0], height: -1, payload: undefined }; return normalizeResults(panelDim, fontHeight, docMap, poolData, viewDefsToJSX, groupNames, (maxTime - minTime) * scaling, [divider]); function layoutDocsAtTime(keyDocs: Doc[], key: number) { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index 3a2979696..9dcfde7f9 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -26,7 +26,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo this._anchorDisposer?.(); } @action - timeout = () => (Date.now() < this._start++ + 1000) && setTimeout(this.timeout, 25) + timeout = action(() => (Date.now() < this._start++ + 1000) && (this._timeout = setTimeout(this.timeout, 25))); componentDidMount() { this._anchorDisposer = reaction(() => [this.props.A.props.ScreenToLocalTransform(), this.props.B.props.ScreenToLocalTransform(), this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document), this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document)], action(() => { @@ -103,28 +103,67 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo // showLinks={action(() => { })} // />, { x: 300, y: 300 }); }); + + + } + + visibleY = (el: any) => { + let rect = el.getBoundingClientRect(); + const top = rect.top, height = rect.height; + var el = el.parentNode; + do { + rect = el.getBoundingClientRect(); + if (top <= rect.right === false && getComputedStyle(el).overflow === "hidden") return rect.bottom; + // Check if the element is out of view due to a container scrolling + if ((top + height) <= rect.top && getComputedStyle(el).overflow === "hidden") return rect.top; + el = el.parentNode; + } while (el !== document.body); + // Check its within the document viewport + return top; //top <= document.documentElement.clientHeight && getComputedStyle(document.documentElement).overflow === "hidden"; + } + visibleX = (el: any) => { + let rect = el.getBoundingClientRect(); + const left = rect.left, width = rect.width; + var el = el.parentNode; + do { + rect = el.getBoundingClientRect(); + if (left <= rect.right === false && getComputedStyle(el).overflow === "hidden") return rect.right; + // Check if the element is out of view due to a container scrolling + if ((left + width) <= rect.left && getComputedStyle(el).overflow === "hidden") return rect.left; + el = el.parentNode; + } while (el !== document.body); + // Check its within the document viewport + return left; //top <= document.documentElement.clientHeight && getComputedStyle(document.documentElement).overflow === "hidden"; } - @computed get renderData() { - this._start; - if (SnappingManager.GetIsDragging() || !this.props.A.ContentDiv || !this.props.B.ContentDiv || !this.props.LinkDocs.length) { + @computed.struct get renderData() { + this._start; SnappingManager.GetIsDragging(); + if (!this.props.A.ContentDiv || !this.props.B.ContentDiv || !this.props.LinkDocs.length) { return undefined; } this.props.A.props.ScreenToLocalTransform().transform(this.props.B.props.ScreenToLocalTransform()); const acont = this.props.A.ContentDiv.getElementsByClassName("linkAnchorBox-cont"); const bcont = this.props.B.ContentDiv.getElementsByClassName("linkAnchorBox-cont"); - const a = (acont.length ? acont[0] : this.props.A.ContentDiv).getBoundingClientRect(); - const b = (bcont.length ? bcont[0] : this.props.B.ContentDiv).getBoundingClientRect(); - const apt = Utils.closestPtBetweenRectangles(a.left, a.top, a.width, a.height, - b.left, b.top, b.width, b.height, + const adiv = (acont.length ? acont[0] : this.props.A.ContentDiv); + const bdiv = (bcont.length ? bcont[0] : this.props.B.ContentDiv); + for (let apdiv = adiv; apdiv; apdiv = apdiv.parentElement as any) if ((apdiv as any).hidden) return; + for (let apdiv = bdiv; apdiv; apdiv = apdiv.parentElement as any) if ((apdiv as any).hidden) return; + const a = adiv.getBoundingClientRect(); + const b = bdiv.getBoundingClientRect(); + const atop = this.visibleY(adiv); + const btop = this.visibleY(bdiv); + const aleft = this.visibleX(adiv); + const bleft = this.visibleX(bdiv); + const apt = Utils.closestPtBetweenRectangles(aleft, atop, a.width, a.height, + bleft, btop, b.width, b.height, a.left + a.width / 2, a.top + a.height / 2); - const bpt = Utils.closestPtBetweenRectangles(b.left, b.top, b.width, b.height, - a.left, a.top, a.width, a.height, + const bpt = Utils.closestPtBetweenRectangles(bleft, btop, b.width, b.height, + aleft, atop, a.width, a.height, apt.point.x, apt.point.y); const pt1 = [apt.point.x, apt.point.y]; const pt2 = [bpt.point.x, bpt.point.y]; - const pt1vec = [pt1[0] - (a.left + a.width / 2), pt1[1] - (a.top + a.height / 2)]; - const pt2vec = [pt2[0] - (b.left + b.width / 2), pt2[1] - (b.top + b.height / 2)]; + const pt1vec = [pt1[0] - (aleft + a.width / 2), pt1[1] - (atop + a.height / 2)]; + const pt2vec = [pt2[0] - (bleft + b.width / 2), pt2[1] - (btop + b.height / 2)]; const pt1len = Math.sqrt((pt1vec[0] * pt1vec[0]) + (pt1vec[1] * pt1vec[1])); const pt2len = Math.sqrt((pt2vec[0] * pt2vec[0]) + (pt2vec[1] * pt2vec[1])); const ptlen = Math.sqrt((pt1[0] - pt2[0]) * (pt1[0] - pt2[0]) + (pt1[1] - pt2[1]) * (pt1[1] - pt2[1])) / 2; @@ -132,6 +171,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo const pt2norm = [pt2vec[0] / pt2len * ptlen, pt2vec[1] / pt2len * ptlen]; const aActive = this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document); const bActive = this.props.B.isSelected() || Doc.IsBrushed(this.props.B.props.Document); + if (aleft !== a.left || atop !== a.top || bleft !== b.left || btop !== b.top) return { a, b, pt1norm: [0, 0], pt2norm: [0, 0], aActive, bActive, textX: undefined, textY: undefined, pt1, pt2 }; const textX = (Math.min(pt1[0], pt2[0]) + Math.max(pt1[0], pt2[0])) / 2 + NumCast(this.props.LinkDocs[0].linkOffsetX); const textY = (pt1[1] + pt2[1]) / 2 + NumCast(this.props.LinkDocs[0].linkOffsetY); @@ -144,9 +184,9 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo return !a.width || !b.width || ((!this.props.LinkDocs[0].linkDisplay) && !aActive && !bActive) ? (null) : (<> <path className="collectionfreeformlinkview-linkLine" style={{ opacity: this._opacity, strokeDasharray: "2 2" }} d={`M ${pt1[0]} ${pt1[1]} C ${pt1[0] + pt1norm[0]} ${pt1[1] + pt1norm[1]}, ${pt2[0] + pt2norm[0]} ${pt2[1] + pt2norm[1]}, ${pt2[0]} ${pt2[1]}`} /> - <text className="collectionfreeformlinkview-linkText" x={textX} y={textY} onPointerDown={this.pointerDown} > + {textX === undefined ? (null) : <text className="collectionfreeformlinkview-linkText" x={textX} y={textY} onPointerDown={this.pointerDown} > {StrCast(this.props.LinkDocs[0].description)} - </text> + </text>} </>); } }
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx index 1a2421bfd..6a1a41ca7 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx @@ -1,4 +1,4 @@ -import { computed } from "mobx"; +import { computed, trace } from "mobx"; import { observer } from "mobx-react"; import { Doc } from "../../../../fields/Doc"; import { Id } from "../../../../fields/FieldSymbols"; @@ -10,12 +10,10 @@ import React = require("react"); import { Utils, emptyFunction } from "../../../../Utils"; import { DocumentType } from "../../../documents/DocumentTypes"; import { SnappingManager } from "../../../util/SnappingManager"; -import { Cast } from "../../../../fields/Types"; @observer export class CollectionFreeFormLinksView extends React.Component { - @computed - get uniqueConnections() { + @computed get uniqueConnections() { const connections = DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => { if (!drawnPairs.reduce((found, drawnPair) => { const match1 = (connection.a === drawnPair.a && connection.b === drawnPair.b); @@ -37,7 +35,7 @@ export class CollectionFreeFormLinksView extends React.Component { } render() { - return SnappingManager.GetIsDragging() ? (null) : <div className="collectionfreeformlinksview-container"> + return <div className="collectionfreeformlinksview-container"> <svg className="collectionfreeformlinksview-svgCanvas"> {this.uniqueConnections} </svg> diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index 2b07c4efb..75cbc20ca 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -52,12 +52,16 @@ .pathOrder-frame { position: absolute; - width: 40; + width: 100%; + height: 100%; + min-height: 15px; text-align: center; - font-size: 30; background-color: #69a6db; + border-radius: 5px; + box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); font-family: Roboto; - font-weight: 300; + font-weight: 500; + color: white; } } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 46e30f616..2783011cf 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,7 +1,4 @@ -import { library } from "@fortawesome/fontawesome-svg-core"; -import { faEye } from "@fortawesome/free-regular-svg-icons"; -import { faBraille, faChalkboard, faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faFileUpload, faPaintBrush, faTable, faUpload } from "@fortawesome/free-solid-svg-icons"; -import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, trace } from "mobx"; +import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import { computedFn } from "mobx-utils"; import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../../fields/Doc"; @@ -15,7 +12,7 @@ import { ScriptField } from "../../../../fields/ScriptField"; import { BoolCast, Cast, FieldValue, NumCast, ScriptCast, StrCast } from "../../../../fields/Types"; import { TraceMobx } from "../../../../fields/util"; import { GestureUtils } from "../../../../pen-gestures/GestureUtils"; -import { aggregateBounds, intersectRect, returnFalse, returnOne, returnZero, Utils, setupMoveUpEvents } from "../../../../Utils"; +import { aggregateBounds, intersectRect, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils, returnVal } from "../../../../Utils"; import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; import { DocServer } from "../../../DocServer"; import { Docs, DocUtils } from "../../../documents/Documents"; @@ -24,10 +21,12 @@ import { DocumentManager } from "../../../util/DocumentManager"; import { DragManager, dropActionType } from "../../../util/DragManager"; import { HistoryUtil } from "../../../util/History"; import { InteractionUtils } from "../../../util/InteractionUtils"; +import { LinkManager } from "../../../util/LinkManager"; +import { SearchUtil } from "../../../util/SearchUtil"; import { SelectionManager } from "../../../util/SelectionManager"; import { SnappingManager } from "../../../util/SnappingManager"; import { Transform } from "../../../util/Transform"; -import { undoBatch, UndoManager } from "../../../util/UndoManager"; +import { undoBatch } from "../../../util/UndoManager"; import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss"; import { Timeline } from "../../animationtimeline/Timeline"; import { ContextMenu } from "../../ContextMenu"; @@ -37,29 +36,25 @@ import { DocumentLinksButton } from "../../nodes/DocumentLinksButton"; import { DocumentViewProps } from "../../nodes/DocumentView"; import { FormattedTextBox } from "../../nodes/formattedText/FormattedTextBox"; import { pageSchema } from "../../nodes/ImageBox"; +import { PresBox } from "../../nodes/PresBox"; import { CollectionDockingView } from "../CollectionDockingView"; import { CollectionSubView } from "../CollectionSubView"; import { CollectionViewType } from "../CollectionView"; import { computePivotLayout, computerPassLayout, computerStarburstLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult } from "./CollectionFreeFormLayoutEngines"; import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors"; import "./CollectionFreeFormView.scss"; -import MarqueeOptionsMenu from "./MarqueeOptionsMenu"; +import { MarqueeOptionsMenu } from "./MarqueeOptionsMenu"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); -import { PresBox } from "../../nodes/PresBox"; -import { SearchUtil } from "../../../util/SearchUtil"; -import { LinkManager } from "../../../util/LinkManager"; - -library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload); export const panZoomSchema = createSchema({ _panX: "number", _panY: "number", - currentTimecode: "number", + _currentTimecode: "number", displayTimecode: "number", - currentFrame: "number", + _currentFrame: "number", arrangeInit: ScriptField, - useClusters: "boolean", + _useClusters: "boolean", fitToBox: "boolean", _xPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set _yPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set @@ -109,28 +104,32 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @observable canPanY: boolean = true; @computed get fitToContentScaling() { return this.fitToContent ? NumCast(this.layoutDoc.fitToContentScaling, 1) : 1; } - @computed get fitToContent() { return (this.props.fitToBox || this.Document._fitToBox) && !this.isAnnotationOverlay; } - @computed get parentScaling() { return this.props.ContentScaling && this.fitToContent && !this.isAnnotationOverlay ? this.props.ContentScaling() : 1; } + @computed get fitToContent() { return this.props.fitToBox || (this.Document._fitToBox && !this.isAnnotationOverlay); } + @computed get parentScaling() { return this.props.ContentScaling && this.fitToContent ? this.props.ContentScaling() : 1; } @computed get contentBounds() { return aggregateBounds(this._layoutElements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!), NumCast(this.layoutDoc._xPadding, 10), NumCast(this.layoutDoc._yPadding, 10)); } - @computed get nativeWidth() { return this.fitToContent ? 0 : NumCast(this.Document._nativeWidth, this.props.NativeWidth()); } - @computed get nativeHeight() { return this.fitToContent ? 0 : NumCast(this.Document._nativeHeight, this.props.NativeHeight()); } + @computed get nativeWidth() { return this.fitToContent ? 0 : returnVal(this.props.NativeWidth?.(), NumCast(this.Document._nativeWidth)); } + @computed get nativeHeight() { return this.fitToContent ? 0 : returnVal(this.props.NativeHeight?.(), NumCast(this.Document._nativeHeight)); } private get isAnnotationOverlay() { return this.props.isAnnotationOverlay; } private get scaleFieldKey() { return this.props.scaleField || "_viewScale"; } private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; } - private panX = () => this.fitToContent ? (this.contentBounds.x + this.contentBounds.r) / 2 : this.Document._panX || 0; - private panY = () => this.fitToContent ? (this.contentBounds.y + this.contentBounds.b) / 2 : this.Document._panY || 0; - private zoomScaling = () => (this.fitToContentScaling / this.parentScaling) * (this.fitToContent ? - Math.min(this.props.PanelHeight() / (this.contentBounds.b - this.contentBounds.y), - this.props.PanelWidth() / (this.contentBounds.r - this.contentBounds.x)) : - NumCast(this.Document[this.scaleFieldKey], 1)) - + private panX = () => this.fitToContent && !this.props.isAnnotationOverlay ? (this.contentBounds.x + this.contentBounds.r) / 2 : this.Document._panX || 0; + private panY = () => this.fitToContent && !this.props.isAnnotationOverlay ? (this.contentBounds.y + this.contentBounds.b) / 2 : this.Document._panY || 0; + private zoomScaling = () => { + const mult = this.fitToContentScaling / this.parentScaling; + if (this.fitToContent) { + const zs = !this.childDocs.length ? 1 : + Math.min(this.props.PanelHeight() / (this.contentBounds.b - this.contentBounds.y), this.props.PanelWidth() / (this.contentBounds.r - this.contentBounds.x)); + return mult * (this.props.isAnnotationOverlay ? Math.min(zs, 1) : zs); + } + return mult * NumCast(this.Document[this.scaleFieldKey], 1); + } @computed get cachedCenteringShiftX(): number { const scaling = this.fitToContent || !this.contentScaling ? 1 : this.contentScaling; - return !this.isAnnotationOverlay ? this.props.PanelWidth() / 2 / this.parentScaling / scaling : 0; // shift so pan position is at center of window for non-overlay collections + return !this.props.isAnnotationOverlay ? this.props.PanelWidth() / 2 / this.parentScaling / scaling : 0; // shift so pan position is at center of window for non-overlay collections } @computed get cachedCenteringShiftY(): number { const scaling = this.fitToContent || !this.contentScaling ? 1 : this.contentScaling; - return !this.isAnnotationOverlay ? this.props.PanelHeight() / 2 / this.parentScaling / scaling : 0;// shift so pan position is at center of window for non-overlay collections + return !this.props.isAnnotationOverlay ? this.props.PanelHeight() / 2 / this.parentScaling / scaling : 0;// shift so pan position is at center of window for non-overlay collections } @computed get cachedGetLocalTransform(): Transform { return Transform.Identity().scale(1 / this.zoomScaling()).translate(this.panX(), this.panY()); @@ -178,8 +177,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P newBox.y = y; } } - if (this.Document.currentFrame !== undefined && !this.props.isAnnotationOverlay) { - CollectionFreeFormDocumentView.setupKeyframes(newBoxes, this.Document.currentFrame); + if (this.Document._currentFrame !== undefined && !this.props.isAnnotationOverlay) { + CollectionFreeFormDocumentView.setupKeyframes(newBoxes, this.Document._currentFrame, true); } } return retVal; @@ -189,7 +188,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P SelectionManager.DeselectAll(); docs.map(doc => DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView)).map(dv => dv && SelectionManager.SelectDoc(dv, true)); } - public isCurrent(doc: Doc) { return (Math.abs(NumCast(doc.displayTimecode, -1) - NumCast(this.Document.currentTimecode, -1)) < 1.5 || NumCast(doc.displayTimecode, -1) === -1); } + public isCurrent(doc: Doc) { return (Math.abs(NumCast(doc.displayTimecode, -1) - NumCast(this.Document._currentTimecode, -1)) < 1.5 || NumCast(doc.displayTimecode, -1) === -1); } public getActiveDocuments = () => { return this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => pair.layout); @@ -212,9 +211,9 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P for (let i = 0; i < docDragData.droppedDocuments.length; i++) { const d = docDragData.droppedDocuments[i]; const layoutDoc = Doc.Layout(d); - if (this.Document.currentFrame !== undefined) { + if (this.Document._currentFrame !== undefined) { const vals = CollectionFreeFormDocumentView.getValues(d, NumCast(d.activeFrame, 1000)); - CollectionFreeFormDocumentView.setValues(this.Document.currentFrame, d, x + vals.x - dropPos[0], y + vals.y - dropPos[1], vals.h, vals.w, vals.opacity); + CollectionFreeFormDocumentView.setValues(this.Document._currentFrame, d, x + vals.x - dropPos[0], y + vals.y - dropPos[1], vals.h, vals.w, this.Document.editScrollProgressivize ? vals.scroll : undefined, vals.opacity); } else { d.x = x + NumCast(d.x) - dropPos[0]; d.y = y + NumCast(d.y) - dropPos[1]; @@ -222,7 +221,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P const nd = [NumCast(layoutDoc._nativeWidth), NumCast(layoutDoc._nativeHeight)]; layoutDoc._width = NumCast(layoutDoc._width, 300); layoutDoc._height = NumCast(layoutDoc._height, nd[0] && nd[1] ? nd[1] / nd[0] * NumCast(layoutDoc._width) : 300); - d.isBackground === undefined && (d.zIndex = zsorted.length + 1 + i); // bringToFront + !d._isBackground && (d._raiseWhenDragged === undefined ? Doc.UserDoc()._raiseWhenDragged : d._raiseWhenDragged) && (d.zIndex = zsorted.length + 1 + i); // bringToFront } (docDragData.droppedDocuments.length === 1 || de.shiftKey) && this.updateClusterDocs(docDragData.droppedDocuments); @@ -261,7 +260,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @action onInternalDrop = (e: Event, de: DragManager.DropEvent) => { - // if (this.props.Document.isBackground) return false; + // if (this.props.Document._isBackground) return false; const [xp, yp] = this.getTransform().transformPoint(de.x, de.y); if (this.isAnnotationOverlay !== true && de.complete.linkDragData) { return this.internalLinkDrop(e, de, de.complete.linkDragData, xp, yp); @@ -306,8 +305,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @undoBatch @action - updateClusters(useClusters: boolean) { - this.props.Document.useClusters = useClusters; + updateClusters(_useClusters: boolean) { + this.props.Document._useClusters = _useClusters; this._clusterSets.length = 0; this.childLayoutPairs.map(pair => pair.layout).map(c => this.updateCluster(c)); } @@ -315,7 +314,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @action updateClusterDocs(docs: Doc[]) { const childLayouts = this.childLayoutPairs.map(pair => pair.layout); - if (this.props.Document.useClusters) { + if (this.props.Document._useClusters) { const docFirst = docs[0]; docs.map(doc => this._clusterSets.map(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1))); const preferredInd = NumCast(docFirst.cluster); @@ -325,7 +324,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P docFirst.cluster = i; } }))); - if (docFirst.cluster === -1 && preferredInd !== -1 && (!this._clusterSets[preferredInd] || !this._clusterSets[preferredInd].filter(member => Doc.IndexOf(member, childLayouts) !== -1).length)) { + if (docFirst.cluster === -1 && preferredInd !== -1 && this._clusterSets.length > preferredInd && (!this._clusterSets[preferredInd] || !this._clusterSets[preferredInd].filter(member => Doc.IndexOf(member, childLayouts) !== -1).length)) { docFirst.cluster = preferredInd; } this._clusterSets.map((set, i) => { @@ -338,7 +337,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P doc.cluster = this._clusterSets.length; this._clusterSets.push([doc]); }); - } else { + } else if (this._clusterSets.length) { for (let i = this._clusterSets.length; i <= NumCast(docFirst.cluster); i++) !this._clusterSets[i] && this._clusterSets.push([]); docs.map(doc => this._clusterSets[doc.cluster = NumCast(docFirst.cluster)].push(doc)); } @@ -350,7 +349,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @action updateCluster(doc: Doc) { const childLayouts = this.childLayoutPairs.map(pair => pair.layout); - if (this.props.Document.useClusters) { + if (this.props.Document._useClusters) { this._clusterSets.map(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1)); const preferredInd = NumCast(doc.cluster); doc.cluster = -1; @@ -359,7 +358,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P doc.cluster = i; } })); - if (doc.cluster === -1 && preferredInd !== -1 && (!this._clusterSets[preferredInd] || !this._clusterSets[preferredInd].filter(member => Doc.IndexOf(member, childLayouts) !== -1).length)) { + if (doc.cluster === -1 && preferredInd !== -1 && this._clusterSets.length > preferredInd && (!this._clusterSets[preferredInd] || !this._clusterSets[preferredInd].filter(member => Doc.IndexOf(member, childLayouts) !== -1).length)) { doc.cluster = preferredInd; } this._clusterSets.map((set, i) => { @@ -370,27 +369,27 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P if (doc.cluster === -1) { doc.cluster = this._clusterSets.length; this._clusterSets.push([doc]); - } else { + } else if (this._clusterSets.length) { for (let i = this._clusterSets.length; i <= doc.cluster; i++) !this._clusterSets[i] && this._clusterSets.push([]); this._clusterSets[doc.cluster].push(doc); } } } - getClusterColor = (doc: Doc) => { + getClusterColor = (doc: Opt<Doc>) => { let clusterColor = this.props.backgroundColor?.(doc, this.props.renderDepth + 1); - const cluster = NumCast(doc.cluster); - if (this.Document.useClusters) { + const cluster = NumCast(doc?.cluster); + if (this.Document._useClusters) { if (this._clusterSets.length <= cluster) { - setTimeout(() => this.updateCluster(doc), 0); + setTimeout(() => doc && this.updateCluster(doc), 0); } else { // choose a cluster color from a palette const colors = ["#da42429e", "#31ea318c", "rgba(197, 87, 20, 0.55)", "#4a7ae2c4", "rgba(216, 9, 255, 0.5)", "#ff7601", "#1dffff", "yellow", "rgba(27, 130, 49, 0.55)", "rgba(0, 0, 0, 0.268)"]; clusterColor = colors[cluster % colors.length]; const set = this._clusterSets[cluster]?.filter(s => s.backgroundColor); // override the cluster color with an explicitly set color on a non-background document. then override that with an explicitly set color on a background document - set && set.filter(s => !s.isBackground).map(s => clusterColor = StrCast(s.backgroundColor)); - set && set.filter(s => s.isBackground).map(s => clusterColor = StrCast(s.backgroundColor)); + set && set.filter(s => !s._isBackground).map(s => clusterColor = StrCast(s.backgroundColor)); + set && set.filter(s => s._isBackground).map(s => clusterColor = StrCast(s.backgroundColor)); } } return clusterColor; @@ -402,7 +401,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P if (e.nativeEvent.cancelBubble || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (Doc.GetSelectedTool() === InkTool.Highlighter || Doc.GetSelectedTool() === InkTool.Pen)) { return; } - this._hitCluster = this.props.Document.useClusters ? this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)) !== -1 : false; + this._hitCluster = this.props.Document._useClusters ? this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)) !== -1 : false; if (e.button === 0 && (!e.shiftKey || this._hitCluster) && !e.altKey && !e.ctrlKey && this.props.active(true)) { // if (!this.props.Document.aliasOf && !this.props.ContainingCollectionView) { @@ -591,7 +590,10 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P onClick = (e: React.MouseEvent) => { if (this.layoutDoc.targetScale && (Math.abs(e.pageX - this._downX) < 3 && Math.abs(e.pageY - this._downY) < 3)) { if (Date.now() - this._lastTap < 300) { - runInAction(() => DocumentLinksButton.StartLink = undefined); + runInAction(() => { + DocumentLinksButton.StartLink = undefined; + DocumentLinksButton.StartLinkView = undefined; + }); const docpt = this.getTransform().transformPoint(e.clientX, e.clientY); this.scaleAtPt(docpt, 1); e.stopPropagation(); @@ -605,7 +607,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P pan = (e: PointerEvent | React.Touch | { clientX: number, clientY: number }): void => { // bcz: theres should be a better way of doing these than referencing these static instances directly MarqueeOptionsMenu.Instance?.fadeOut(true);// I think it makes sense for the marquee menu to go away when panned. -syip2 - // PDFMenu.Instance.fadeOut(true); (commented out for mobile) const [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); this.setPan((this.Document._panX || 0) - dx, (this.Document._panY || 0) - dy, undefined, true); @@ -641,7 +642,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => { - // panning a workspace if (!e.cancelBubble) { const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true); const pt = myTouches[0]; @@ -780,9 +780,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @action zoom = (pointX: number, pointY: number, deltaY: number): void => { let deltaScale = deltaY > 0 ? (1 / 1.05) : 1.05; - if (deltaScale * this.zoomScaling() < 1 && this.isAnnotationOverlay) { - deltaScale = 1 / this.zoomScaling(); - } if (deltaScale < 0) deltaScale = -deltaScale; const [x, y] = this.getTransform().transformPoint(pointX, pointY); const localTransform = this.getLocalTransform().inverse().scaleAbout(deltaScale, x, y); @@ -796,7 +793,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @action onPointerWheel = (e: React.WheelEvent): void => { - if (this.layoutDoc._lockedTransform || this.props.Document.inOverlay) return; + if (this.layoutDoc._lockedTransform || this.props.Document.inOverlay || this.props.Document.treeViewOutlineMode) return; if (!e.ctrlKey && this.props.Document.scrollHeight !== undefined) { // things that can scroll vertically should do that instead of zooming e.stopPropagation(); } @@ -843,14 +840,14 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } bringToFront = action((doc: Doc, sendToBack?: boolean) => { - if (sendToBack || doc.isBackground) { + if (sendToBack || doc._isBackground) { doc.zIndex = 0; } else if (doc.isInkMask) { doc.zIndex = 5000; } else { const docs = this.childLayoutPairs.map(pair => pair.layout); docs.slice().sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex)); - let zlast = docs.length ? NumCast(docs[docs.length - 1].zIndex) : 1; + let zlast = docs.length ? Math.max(docs.length, NumCast(docs[docs.length - 1].zIndex)) : 1; if (zlast - docs.length > 100) { for (let i = 0; i < docs.length; i++) doc.zIndex = i + 1; zlast = docs.length + 1; @@ -908,19 +905,14 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P const savedState = { px: this.Document._panX, py: this.Document._panY, s: this.Document[this.scaleFieldKey], pt: this.Document._viewTransition }; - // if (!willZoom && DocumentView._focusHack.length) { - // Doc.BrushDoc(this.props.Document); - // !doc.z && NumCast(this.layoutDoc.scale) < 1 && this.scaleAtPt(DocumentView._focusHack, 1); // [NumCast(doc.x), NumCast(doc.y)], 1); - // } else { if (DocListCast(this.dataDoc[this.props.fieldKey]).includes(doc)) { // glr: freeform transform speed can be set by adjusting presTransition field - needs a way of knowing when presentation is not active... - if (!doc.z) this.setPan(newPanX, newPanY, doc.presTransition || doc.presTransition === 0 ? `transform ${doc.presTransition}ms` : "transform 500ms", true); // docs that are floating in their collection can't be panned to from their collection -- need to propagate the pan to a parent freeform somehow + if (!doc.z) this.setPan(newPanX, newPanY, doc.focusSpeed || doc.focusSpeed === 0 ? `transform ${doc.focusSpeed}ms` : "transform 500ms", true); // docs that are floating in their collection can't be panned to from their collection -- need to propagate the pan to a parent freeform somehow } Doc.BrushDoc(this.props.Document); this.props.focus(this.props.Document); willZoom && this.setScaleToZoom(layoutdoc, scale); Doc.linkFollowHighlight(doc); - //} afterFocus && setTimeout(() => { if (afterFocus?.()) { @@ -935,15 +927,17 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } setScaleToZoom = (doc: Doc, scale: number = 0.75) => { - this.Document[this.scaleFieldKey] = scale * Math.min(this.props.PanelWidth() / NumCast(doc._width), this.props.PanelHeight() / NumCast(doc._height)); + const pw = this.props.PanelWidth(); + const ph = this.props.PanelHeight(); + pw && ph && (this.Document[this.scaleFieldKey] = scale * Math.min(pw / NumCast(doc._width), ph / NumCast(doc._height))); } @computed get libraryPath() { return this.props.LibraryPath ? [...this.props.LibraryPath, this.props.Document] : []; } - @computed get backgroundActive() { return this.layoutDoc.isBackground && (this.props.ContainingCollectionView?.active() || this.props.active()); } + @computed get backgroundActive() { return this.layoutDoc._isBackground && (this.props.ContainingCollectionView?.active() || this.props.active()); } onChildClickHandler = () => this.props.childClickScript || ScriptCast(this.Document.onChildClick); onChildDoubleClickHandler = () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); - backgroundHalo = () => BoolCast(this.Document.useClusters); - parentActive = (outsideReaction: boolean) => this.props.active(outsideReaction) || this.backgroundActive ? true : false; + backgroundHalo = () => BoolCast(this.Document._useClusters); + parentActive = (outsideReaction: boolean) => this.props.active(outsideReaction) || this.backgroundActive || this.layoutDoc._viewType === CollectionViewType.Pile ? true : false; getChildDocumentViewProps(childLayout: Doc, childData?: Doc): DocumentViewProps { return { addDocument: this.props.addDocument, @@ -951,8 +945,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P moveDocument: this.props.moveDocument, pinToPres: this.props.pinToPres, whenActiveChanged: this.props.whenActiveChanged, - NativeHeight: returnZero, - NativeWidth: returnZero, fitToBox: false, DataDoc: childData, Document: childLayout, @@ -960,7 +952,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P LayoutTemplate: childLayout.z ? undefined : this.props.ChildLayoutTemplate, LayoutTemplateString: childLayout.z ? undefined : this.props.ChildLayoutString, FreezeDimensions: this.props.freezeChildDimensions, - layoutKey: undefined, + layoutKey: StrCast(this.props.Document.childLayoutKey), setupDragLines: this.setupDragLines, dontRegisterView: this.props.dontRegisterView, rootSelected: childData ? this.rootSelected : returnFalse, @@ -975,6 +967,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P ContainingCollectionView: this.props.CollectionView, ContainingCollectionDoc: this.props.Document, docFilters: this.docFilters, + docRangeFilters: this.docRangeFilters, + searchFilterDocs: this.searchFilterDocs, focus: this.focusDocument, backgroundColor: this.getClusterColor, backgroundHalo: this.backgroundHalo, @@ -1008,8 +1002,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P }); getCalculatedPositions(params: { pair: { layout: Doc, data?: Doc }, index: number, collection: Doc, docs: Doc[], state: any }): PoolData { const layoutDoc = Doc.Layout(params.pair.layout); - const { x, y, opacity } = this.Document.currentFrame === undefined ? params.pair.layout : - CollectionFreeFormDocumentView.getValues(params.pair.layout, this.Document.currentFrame || 0); + const { x, y, opacity } = this.Document._currentFrame === undefined ? params.pair.layout : + CollectionFreeFormDocumentView.getValues(params.pair.layout, this.Document._currentFrame || 0); const { z, color, zIndex } = params.pair.layout; return { x: NumCast(x), y: NumCast(y), z: Cast(z, "number"), color: StrCast(color), zIndex: Cast(zIndex, "number"), @@ -1045,7 +1039,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } else if (viewDef.type === "div") { return [x, y].some(val => val === undefined) ? undefined : { - ele: <div className="collectionFreeform-customDiv" title={viewDef.payload?.join(" ")} key={"div" + x + y + z} onClick={e => this.onViewDefDivClick(e, viewDef)} + ele: <div className="collectionFreeform-customDiv" title={viewDef.payload?.join(" ")} key={"div" + x + y + z + viewDef.payload} onClick={e => this.onViewDefDivClick(e, viewDef)} style={{ width, height, backgroundColor: color, transform }} />, bounds: viewDef }; @@ -1115,7 +1109,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P }); this._cachedPool.clear(); Array.from(newPool.entries()).forEach(k => this._cachedPool.set(k[0], k[1])); - const elements: ViewDefResult[] = computedElementData.slice(); + const elements = computedElementData.slice(); const engine = this.props.layoutEngine?.() || StrCast(this.props.Document._layoutEngine); Array.from(newPool.entries()).filter(entry => this.isCurrent(entry[1].pair.layout)).forEach(entry => elements.push({ @@ -1126,8 +1120,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P dataProvider={this.childDataProvider} sizeProvider={this.childSizeProvider} pointerEvents={this.backgroundActive || this.props.childPointerEvents ? - true : - (this.props.viewDefDivClick || (engine === "pass" && !this.props.isSelected(true))) ? false : undefined} + "all" : + (this.props.viewDefDivClick || (engine === "pass" && !this.props.isSelected(true))) ? "none" : undefined} jitterRotation={NumCast(this.props.Document._jitterRotation) || ((Doc.UserDoc().renderStyle === "comic" ? 10 : 0))} //fitToBox={this.props.fitToBox || BoolCast(this.props.freezeChildDimensions)} // bcz: check this fitToBox={BoolCast(this.props.freezeChildDimensions)} // bcz: check this @@ -1136,11 +1130,12 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P bounds: this.childDataProvider(entry[1].pair.layout, entry[1].replica) })); - if (this.props.isAnnotationOverlay) { - this.props.Document[this.scaleFieldKey] = Math.max(1, NumCast(this.props.Document[this.scaleFieldKey])); + if (this.props.isAnnotationOverlay) { // don't zoom out farther than 1-1 if it's a bounded item (image, video, pdf), otherwise don't allow zooming in closer than 1-1 if it's a text sidebar + if (this.props.scaleField) this.props.Document[this.scaleFieldKey] = Math.min(1, NumCast(this.props.Document[this.scaleFieldKey], 1)); + else this.props.Document[this.scaleFieldKey] = Math.max(1, NumCast(this.props.Document[this.scaleFieldKey])); } - this.Document.useClusters && !this._clusterSets.length && this.childDocs.length && this.updateClusters(true); + this.Document._useClusters && !this._clusterSets.length && this.childDocs.length && this.updateClusters(true); return elements; } @@ -1175,7 +1170,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P if ((e as any).handlePan || this.props.isAnnotationOverlay) return; (e as any).handlePan = true; - if (this._marqueeRef?.current) { + if (!this.props.Document._noAutoscroll && this._marqueeRef?.current) { const dragX = e.detail.clientX; const dragY = e.detail.clientY; const bounds = this._marqueeRef.current?.getBoundingClientRect(); @@ -1226,7 +1221,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @undoBatch @action toggleNativeDimensions = () => { - Doc.toggleNativeDimensions(this.layoutDoc, this.props.ContentScaling(), this.props.NativeWidth(), this.props.NativeHeight()); + Doc.toggleNativeDimensions(this.layoutDoc, this.props.ContentScaling(), this.props.NativeWidth?.() || 0, this.props.NativeHeight?.() || 0); } @undoBatch @@ -1238,11 +1233,12 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P private thumbIdentifier?: number; onContextMenu = (e: React.MouseEvent) => { - if (this.props.annotationsKey || !ContextMenu.Instance) return; + if (this.props.annotationsKey || this.props.Document.annotationOn || !ContextMenu.Instance) return; const appearance = ContextMenu.Instance.findByDescription("Appearance..."); const appearanceItems = appearance && "subitems" in appearance ? appearance.subitems : []; appearanceItems.push({ description: "Reset View", event: () => { this.props.Document._panX = this.props.Document._panY = 0; this.props.Document[this.scaleFieldKey] = 1; }, icon: "compress-arrows-alt" }); + !Doc.UserDoc().noviceMode && Doc.UserDoc().defaultTextLayout && appearanceItems.push({ description: "Reset default note style", event: () => Doc.UserDoc().defaultTextLayout = undefined, icon: "eye" }); appearanceItems.push({ description: `${this.fitToContent ? "Make Zoomable" : "Scale to Window"}`, event: () => this.Document._fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" }); !Doc.UserDoc().noviceMode ? appearanceItems.push({ description: "Arrange contents in grid", event: this.layoutDocsInGrid, icon: "table" }) : null; !appearance && ContextMenu.Instance.addItem({ description: "Appearance...", subitems: appearanceItems, icon: "eye" }); @@ -1252,17 +1248,17 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P !Doc.UserDoc().noviceMode ? viewCtrlItems.push({ description: (Doc.UserDoc().showSnapLines ? "Hide" : "Show") + " Snap Lines", event: () => Doc.UserDoc().showSnapLines = !Doc.UserDoc().showSnapLines, icon: "compress-arrows-alt" }) : null; - !Doc.UserDoc().noviceMode ? viewCtrlItems.push({ description: (this.Document.useClusters ? "Hide" : "Show") + " Clusters", event: () => this.updateClusters(!this.Document.useClusters), icon: "braille" }) : null; + !Doc.UserDoc().noviceMode ? viewCtrlItems.push({ description: (this.Document._useClusters ? "Hide" : "Show") + " Clusters", event: () => this.updateClusters(!this.Document._useClusters), icon: "braille" }) : null; !viewctrls && ContextMenu.Instance.addItem({ description: "UI Controls...", subitems: viewCtrlItems, icon: "eye" }); const options = ContextMenu.Instance.findByDescription("Options..."); const optionItems = options && "subitems" in options ? options.subitems : []; !this.props.isAnnotationOverlay && !Doc.UserDoc().noviceMode && - optionItems.push({ description: (this.showTimeline ? "Close" : "Open") + " Animation Timeline", event: action(() => this.showTimeline = !this.showTimeline), icon: faEye }); + optionItems.push({ description: (this.showTimeline ? "Close" : "Open") + " Animation Timeline", event: action(() => this.showTimeline = !this.showTimeline), icon: "eye" }); this.props.ContainingCollectionView && - optionItems.push({ description: "Undo Collection", event: this.promoteCollection, icon: "table" }); + optionItems.push({ description: "Move Items Out of Collection", event: this.promoteCollection, icon: "table" }); optionItems.push({ description: this.layoutDoc._lockedTransform ? "Unlock Transform" : "Lock Transform", event: this.toggleLockTransform, icon: this.layoutDoc._lockedTransform ? "unlock" : "lock" }); - optionItems.push({ description: "Use Background Color as Default", event: () => Cast(Doc.UserDoc().emptyCollection, Doc, null)._backgroundColor = StrCast(this.layoutDoc._backgroundColor), icon: "palette" }); + this.props.renderDepth && optionItems.push({ description: "Use Background Color as Default", event: () => Cast(Doc.UserDoc().emptyCollection, Doc, null)._backgroundColor = StrCast(this.layoutDoc._backgroundColor), icon: "palette" }); if (!Doc.UserDoc().noviceMode) { optionItems.push({ description: (!this.layoutDoc._nativeWidth || !this.layoutDoc._nativeHeight ? "Freeze" : "Unfreeze") + " Aspect", event: this.toggleNativeDimensions, icon: "snowflake" }); optionItems.push({ description: `${this.Document._freeformLOD ? "Enable LOD" : "Disable LOD"}`, event: () => this.Document._freeformLOD = !this.Document._freeformLOD, icon: "table" }); @@ -1271,7 +1267,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P !options && ContextMenu.Instance.addItem({ description: "Options...", subitems: optionItems, icon: "eye" }); const mores = ContextMenu.Instance.findByDescription("More..."); const moreItems = mores && "subitems" in mores ? mores.subitems : []; - moreItems.push({ description: "Import document", icon: "upload", event: ({ x, y }) => this.importDocument(x, y) }); + moreItems.push({ description: "Export collection", icon: "download", event: async () => Doc.Zip(this.props.Document) }); + moreItems.push({ description: "Import exported collection", icon: "upload", event: ({ x, y }) => this.importDocument(x, y) }); !mores && ContextMenu.Instance.addItem({ description: "More...", subitems: moreItems, icon: "eye" }); } @@ -1315,7 +1312,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } @action - setupDragLines = () => { + setupDragLines = (snapToDraggedDoc: boolean = false) => { const activeDocs = this.getActiveDocuments(); if (activeDocs.length > 50) { DragManager.SetSnapLines([], []); @@ -1331,13 +1328,13 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P }; const snappableDocs: Doc[] = []; // the set of documents in the visible viewport that we will try to snap to; const otherBounds = { left: this.panX(), top: this.panY(), width: Math.abs(size[0]), height: Math.abs(size[1]) }; - this.getActiveDocuments().filter(doc => !doc.isBackground && doc.z === undefined).map(doc => isDocInView(doc, selRect)); // first see if there are any foreground docs to snap to + this.getActiveDocuments().filter(doc => !doc._isBackground && doc.z === undefined).map(doc => isDocInView(doc, selRect)); // first see if there are any foreground docs to snap to !snappableDocs.length && this.getActiveDocuments().filter(doc => doc.z === undefined).map(doc => isDocInView(doc, selRect)); // if not, see if there are background docs to snap to !snappableDocs.length && this.getActiveDocuments().filter(doc => doc.z !== undefined).map(doc => isDocInView(doc, otherBounds)); // if not, then why not snap to floating docs const horizLines: number[] = []; const vertLines: number[] = []; - snappableDocs.filter(doc => !DragManager.docsBeingDragged.includes(Cast(doc.rootDocument, Doc, null) || doc)).forEach(doc => { + snappableDocs.filter(doc => snapToDraggedDoc || !DragManager.docsBeingDragged.includes(Cast(doc.rootDocument, Doc, null) || doc)).forEach(doc => { const { left, top, width, height } = docDims(doc); const topLeftInScreen = this.getTransform().inverse().transformPoint(left, top); const docSize = this.getTransform().inverse().transformDirection(width, height); @@ -1349,7 +1346,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } onPointerOver = (e: React.PointerEvent) => { if (SnappingManager.GetIsDragging()) { - this.setupDragLines(); + this.setupDragLines(e.ctrlKey || e.shiftKey); } e.stopPropagation(); } @@ -1379,7 +1376,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P _nudgeTime = 0; nudge = action((x: number, y: number) => { - if (this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Freeform) { // bcz: this isn't ideal, but want to try it out... + if (this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Freeform || + this.props.ContainingCollectionDoc._panX !== undefined) { // bcz: this isn't ideal, but want to try it out... this.setPan(NumCast(this.layoutDoc._panX) + this.props.PanelWidth() / 2 * x / this.zoomScaling(), NumCast(this.layoutDoc._panY) + this.props.PanelHeight() / 2 * (-y) / this.zoomScaling(), "transform 500ms", true); this._nudgeTime = Date.now(); @@ -1391,7 +1389,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @computed get marqueeView() { return <MarqueeView {...this.props} - nudge={this.nudge} + nudge={this.isAnnotationOverlay ? undefined : this.nudge} addDocTab={this.addDocTab} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} @@ -1406,25 +1404,26 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P centeringShiftY={this.centeringShiftY} presPaths={BoolCast(this.Document.presPathView)} progressivize={BoolCast(this.Document.editProgressivize)} - zoomProgressivize={BoolCast(this.Document.editZoomProgressivize)} + presPinView={BoolCast(this.Document.presPinView)} transition={Cast(this.layoutDoc._viewTransition, "string", null)} viewDefDivClick={this.props.viewDefDivClick} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}> {this.children} - </CollectionFreeFormViewPannableContents></div> + </CollectionFreeFormViewPannableContents> + </div> {this.showTimeline ? <Timeline ref={this._timelineRef} {...this.props} /> : (null)} </MarqueeView>; } @computed get contentScaling() { if (this.props.annotationsKey && !this.props.forceScaling) return 0; - const nw = NumCast(this.Document._nativeWidth, this.props.NativeWidth()); - const nh = NumCast(this.Document._nativeHeight, this.props.NativeHeight()); + const nw = returnVal(this.props.NativeWidth?.(), NumCast(this.Document._nativeWidth)); + const nh = returnVal(this.props.NativeHeight?.(), NumCast(this.Document._nativeHeight)); const hscale = nh ? this.props.PanelHeight() / nh : 1; const wscale = nw ? this.props.PanelWidth() / nw : 1; return wscale < hscale ? wscale : hscale; } - @computed get backgroundEvents() { return this.layoutDoc.isBackground && SnappingManager.GetIsDragging(); } + @computed get backgroundEvents() { return this.layoutDoc._isBackground && SnappingManager.GetIsDragging(); } render() { TraceMobx(); const clientRect = this._mainCont?.getBoundingClientRect(); @@ -1492,7 +1491,7 @@ interface CollectionFreeFormViewPannableContentsProps { transition?: string; presPaths?: boolean; progressivize?: boolean; - zoomProgressivize?: boolean; + presPinView?: boolean; } @observer @@ -1504,11 +1503,9 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF e.stopPropagation(); e.preventDefault(); const corner = e.target as any; - console.log(corner.id); if (corner) this._drag = corner.id; const rect = document.getElementById(this._drag); if (rect) { - console.log(this._drag); setupMoveUpEvents(e.target, e, this.onPointerMove, (e) => { }, (e) => { }); } } @@ -1559,48 +1556,25 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF doc.style.top = toNumber(top, e.movementY) + 'px'; doc.style.left = toNumber(left, e.movementX) + 'px'; } - this.updateAll(height, width, top, left); + // this.updateAll(height, width, top, left); return false; } return true; } - @action - updateAll = (width: number, height: number, top: number, left: number) => { - const activeItem = Cast(PresBox.Instance.childDocs[PresBox.Instance.itemIndex], Doc, null); - const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); - this.updateList(targetDoc, activeItem["viewfinder-width-indexed"], width); - this.updateList(targetDoc, activeItem["viewfinder-height-indexed"], height); - this.updateList(targetDoc, activeItem["viewfinder-top-indexed"], top); - this.updateList(targetDoc, activeItem["viewfinder-left-indexed"], left); - } - - @action - updateList = (doc: Doc, list: any, val: number) => { - const x: List<number> = list; - if (x && x.length >= NumCast(doc.currentFrame) + 1) { - x[NumCast(doc.currentFrame)] = val; - list = x; - } else if (doc && x) { - x.length = NumCast(doc.currentFrame) + 1; - x[NumCast(doc.currentFrame)] = val; - list = x; - } - } - // scale: NumCast(targetDoc._viewScale), @computed get zoomProgressivizeContainer() { - const activeItem = Cast(PresBox.Instance.childDocs[PresBox.Instance.itemIndex], Doc, null); - const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); - if (activeItem && activeItem.zoomProgressivize) { - const vfLeft: number = PresBox.Instance.checkList(targetDoc, activeItem["viewfinder-left-indexed"]); - const vfWidth: number = PresBox.Instance.checkList(targetDoc, activeItem["viewfinder-width-indexed"]); - const vfTop: number = PresBox.Instance.checkList(targetDoc, activeItem["viewfinder-top-indexed"]); - const vfHeight: number = PresBox.Instance.checkList(targetDoc, activeItem["viewfinder-height-indexed"]); + const activeItem = PresBox.Instance.activeItem; + // const targetDoc = PresBox.Instance.targetDoc; + if (activeItem && activeItem.presPinView && activeItem.id) { + const vfLeft: number = NumCast(activeItem.presPinViewX); + const vfTop: number = NumCast(activeItem.presPinViewY); + const vfWidth: number = 100; + const vfHeight: number = 100; return ( <> - {!activeItem.editZoomProgressivize ? (null) : <div id="resizable" className="resizable" onPointerDown={this.onPointerDown} style={{ width: vfWidth, height: vfHeight, top: vfTop, left: vfLeft, position: 'absolute' }}> - <div className='resizers'> + {!this.props.presPinView ? (null) : <div id="resizable" className="resizable" onPointerDown={this.onPointerDown} style={{ width: vfWidth, height: vfHeight, top: vfTop, left: vfLeft, position: 'absolute' }}> + <div className='resizers' key={'resizer' + activeItem.id}> <div id="resizer-tl" className='resizer top-left' onPointerDown={this.onPointerDown}></div> <div id="resizer-tr" className='resizer top-right' onPointerDown={this.onPointerDown}></div> <div id="resizer-bl" className='resizer bottom-left' onPointerDown={this.onPointerDown}></div> @@ -1613,7 +1587,7 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF } @computed get zoomProgressivize() { - return PresBox.Instance && this.props.zoomProgressivize ? this.zoomProgressivizeContainer : (null); + return PresBox.Instance && PresBox.Instance.activeItem && PresBox.Instance.activeItem.presPinView && PresBox.Instance.layoutDoc.presStatus === 'edit' ? this.zoomProgressivizeContainer : (null); } @computed get progressivize() { @@ -1622,30 +1596,26 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF @computed get presPaths() { const presPaths = "presPaths" + (this.props.presPaths ? "" : "-hidden"); - return !(PresBox.Instance) ? (null) : (<> - {!this.props.presPaths ? (null) : <><div>{PresBox.Instance.order}</div> - <svg className={presPaths}> - <defs> - <marker id="arrow" markerWidth="3" overflow="visible" markerHeight="3" refX="5" refY="5" orient="auto" markerUnits="strokeWidth"> - <path d="M0,0 L0,6 L9,3 z" fill="#69a6db" /> - </marker> - <marker id="square" markerWidth="3" markerHeight="3" overflow="visible" - refX="5" refY="5" orient="auto" markerUnits="strokeWidth"> - <path d="M 5,1 L 9,5 5,9 1,5 z" fill="#69a6db" /> - </marker> - <marker id="markerSquare" markerWidth="7" markerHeight="7" refX="4" refY="4" - orient="auto" overflow="visible"> - <rect x="1" y="1" width="5" height="5" fill="#69a6db" /> - </marker> - - <marker id="markerArrow" markerWidth="5" markerHeight="5" refX="2" refY="7" - orient="auto" overflow="visible"> - <path d="M2,2 L2,13 L8,7 L2,2" fill="#69a6db" /> - </marker> - </defs>; - {PresBox.Instance.paths} - </svg></>} - </>); + return !PresBox.Instance || !this.props.presPaths ? (null) : <> + <div key="presorder">{PresBox.Instance.order}</div> + <svg key="svg" className={presPaths}> + <defs> + <marker id="markerSquare" markerWidth="3" markerHeight="3" refX="1.5" refY="1.5" + orient="auto" overflow="visible"> + <rect x="0" y="0" width="3" height="3" stroke="#69a6db" strokeWidth="1" fill="white" fillOpacity="0.8" /> + </marker> + <marker id="markerSquareFilled" markerWidth="3" markerHeight="3" refX="1.5" refY="1.5" + orient="auto" overflow="visible"> + <rect x="0" y="0" width="3" height="3" stroke="#69a6db" strokeWidth="1" fill="#69a6db" /> + </marker> + <marker id="markerArrow" markerWidth="3" markerHeight="3" refX="2" refY="4" + orient="auto" overflow="visible"> + <path d="M2,2 L2,6 L6,4 L2,2 Z" stroke="#69a6db" strokeLinejoin="round" strokeWidth="1" fill="white" fillOpacity="0.8" /> + </marker> + </defs> + {PresBox.Instance.paths} + </svg> + </>; } render() { @@ -1657,9 +1627,16 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF const pany = -this.props.panY(); const zoom = this.props.zoomScaling(); return <div className={freeformclass} + onScroll={e => { + const target = e.target as any; + if (getComputedStyle(target)?.overflow === "visible") { // if collection is visible, then scrolling will mess things up since there are no scroll bars + target.scrollTop = target.scrollLeft = 0; + } + }} style={{ transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}) translate(${panx}px, ${pany}px)`, - transition: this.props.transition + transition: this.props.transition, + //willChange: "transform" }}> {this.props.children()} {this.presPaths} @@ -1667,4 +1644,4 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF {this.zoomProgressivize} </div>; } -} +}
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/FormatShapePane.scss b/src/client/views/collections/collectionFreeForm/FormatShapePane.scss deleted file mode 100644 index d49ab27fb..000000000 --- a/src/client/views/collections/collectionFreeForm/FormatShapePane.scss +++ /dev/null @@ -1,68 +0,0 @@ -.antimodeMenu-button { - width: 200px; - position: relative; - text-align: left; - - .color-previewI { - width: 100%; - height: 40%; - } - - .color-previewII { - width: 100%; - height: 100%; - } -} - -.antimenu-Buttonup { - position: absolute; - width: 20; - height: 10; - right: 0; - padding: 0; -} - -.formatShapePane-inputBtn { - width: inherit; - position: absolute; -} - -.btn-group-palette { - .sketch-picker { - background: #323232; - width: 160px !important; - height: 80% !important; - - .flexbox-fit { - background: #323232; - } - } -} - -.btn-group { - display: grid; - grid-template-columns: auto auto auto auto; - /* Make the buttons appear below each other */ -} - -.btn-group-palette { - display: block; - /* Make the buttons appear below each other */ -} - -.btn-draw { - display: inline; - /* Make the buttons appear below each other */ -} - -.btn2-group { - display: block; - background: #323232; - grid-template-columns: auto; - - /* Make the buttons appear below each other */ - .antimodeMenu-button { - background: #323232; - display: block; - } -}
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/FormatShapePane.tsx b/src/client/views/collections/collectionFreeForm/FormatShapePane.tsx deleted file mode 100644 index a7f44bbbf..000000000 --- a/src/client/views/collections/collectionFreeForm/FormatShapePane.tsx +++ /dev/null @@ -1,558 +0,0 @@ -import React = require("react"); -import { IconProp } from '@fortawesome/fontawesome-svg-core'; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, observable } from "mobx"; -import { observer } from "mobx-react"; -import { Doc, Field, Opt } from "../../../../fields/Doc"; -import { Document } from "../../../../fields/documentSchemas"; -import { InkField } from "../../../../fields/InkField"; -import { BoolCast, Cast, NumCast } from "../../../../fields/Types"; -import { DocumentType } from "../../../documents/DocumentTypes"; -import { SelectionManager } from "../../../util/SelectionManager"; -import AntimodeMenu, { AntimodeMenuProps } from "../../AntimodeMenu"; -import "./FormatShapePane.scss"; -import { undoBatch } from "../../../util/UndoManager"; -import { ColorState, SketchPicker } from 'react-color'; - -@observer -export default class FormatShapePane extends AntimodeMenu<AntimodeMenuProps> { - static Instance: FormatShapePane; - - private _lastFill = "#D0021B"; - private _lastLine = "#D0021B"; - private _lastDash = "2"; - private _mode = ["fill-drip", "ruler-combined"]; - - @observable private _subOpen = [false, false]; - @observable private _currMode = "fill-drip"; - @observable _lock = false; - @observable private _fillBtn = false; - @observable private _lineBtn = false; - @observable _controlBtn = false; - @observable private _controlPoints: { X: number, Y: number }[] = []; - @observable _currPoint = -1; - - getField(key: string) { - return this.selectedInk?.reduce((p, i) => - (p === undefined || (p && p === i.rootDoc[key])) && i.rootDoc[key] !== "0" ? Field.toString(i.rootDoc[key] as Field) : "", undefined as Opt<string>); - } - - @computed get selectedInk() { - const inks = SelectionManager.SelectedDocuments().filter(i => Document(i.rootDoc).type === DocumentType.INK); - return inks.length ? inks : undefined; - } - @computed get unFilled() { return this.selectedInk?.reduce((p, i) => p && !i.rootDoc.fillColor ? true : false, true) || false; } - @computed get unStrokd() { return this.selectedInk?.reduce((p, i) => p && !i.rootDoc.color ? true : false, true) || false; } - @computed get solidFil() { return this.selectedInk?.reduce((p, i) => p && i.rootDoc.fillColor ? true : false, true) || false; } - @computed get solidStk() { return this.selectedInk?.reduce((p, i) => p && i.rootDoc.color && (!i.rootDoc.strokeDash || i.rootDoc.strokeDash === "0") ? true : false, true) || false; } - @computed get dashdStk() { return !this.unStrokd && this.getField("strokeDash") || ""; } - @computed get colorFil() { const ccol = this.getField("fillColor") || ""; ccol && (this._lastFill = ccol); return ccol; } - @computed get colorStk() { const ccol = this.getField("color") || ""; ccol && (this._lastLine = ccol); return ccol; } - @computed get widthStk() { return this.getField("strokeWidth") || "1"; } - @computed get markHead() { return this.getField("strokeStartMarker") || ""; } - @computed get markTail() { return this.getField("strokeEndMarker") || ""; } - @computed get shapeHgt() { return this.getField("_height"); } - @computed get shapeWid() { return this.getField("_width"); } - @computed get shapeXps() { return this.getField("x"); } - @computed get shapeYps() { return this.getField("y"); } - @computed get shapeRot() { return this.getField("rotation"); } - set unFilled(value) { this.colorFil = value ? "" : this._lastFill; } - set solidFil(value) { this.unFilled = !value; } - set colorFil(value) { value && (this._lastFill = value); this.selectedInk?.forEach(i => i.rootDoc.fillColor = value ? value : undefined); } - set colorStk(value) { value && (this._lastLine = value); this.selectedInk?.forEach(i => i.rootDoc.color = value ? value : undefined); } - set markHead(value) { this.selectedInk?.forEach(i => i.rootDoc.strokeStartMarker = value); } - set markTail(value) { this.selectedInk?.forEach(i => i.rootDoc.strokeEndMarker = value); } - set unStrokd(value) { this.colorStk = value ? "" : this._lastLine; } - set solidStk(value) { this.dashdStk = ""; this.unStrokd = !value; } - set dashdStk(value) { - value && (this._lastDash = value) && (this.unStrokd = false); - this.selectedInk?.forEach(i => i.rootDoc.strokeDash = value ? this._lastDash : undefined); - } - set shapeXps(value) { this.selectedInk?.forEach(i => i.rootDoc.x = Number(value)); } - set shapeYps(value) { this.selectedInk?.forEach(i => i.rootDoc.y = Number(value)); } - set shapeRot(value) { this.selectedInk?.forEach(i => i.rootDoc.rotation = Number(value)); } - set widthStk(value) { this.selectedInk?.forEach(i => i.rootDoc.strokeWidth = Number(value)); } - set shapeWid(value) { - this.selectedInk?.filter(i => i.rootDoc._width && i.rootDoc._height).forEach(i => { - const oldWidth = NumCast(i.rootDoc._width); - i.rootDoc._width = Number(value); - this._lock && (i.rootDoc._height = (i.rootDoc._width * NumCast(i.rootDoc._height)) / oldWidth); - }); - } - set shapeHgt(value) { - this.selectedInk?.filter(i => i.rootDoc._width && i.rootDoc._height).forEach(i => { - const oldHeight = NumCast(i.rootDoc._height); - i.rootDoc._height = Number(value); - this._lock && (i.rootDoc._width = (i.rootDoc._height * NumCast(i.rootDoc._width)) / oldHeight); - }); - } - - constructor(props: Readonly<{}>) { - super(props); - FormatShapePane.Instance = this; - this._canFade = false; - this.Pinned = BoolCast(Doc.UserDoc()["menuFormatShape-pinned"]); - } - - @action - closePane = () => { - this.fadeOut(false); - this.Pinned = false; - } - - @action - upDownButtons = (dirs: string, field: string) => { - switch (field) { - case "rot": this.rotate((dirs === "up" ? .1 : -.1)); break; - // case "rot": this.selectedInk?.forEach(i => i.rootDoc.rotation = NumCast(i.rootDoc.rotation) + (dirs === "up" ? 0.1 : -0.1)); break; - case "Xps": this.selectedInk?.forEach(i => i.rootDoc.x = NumCast(i.rootDoc.x) + (dirs === "up" ? 10 : -10)); break; - case "Yps": this.selectedInk?.forEach(i => i.rootDoc.y = NumCast(i.rootDoc.y) + (dirs === "up" ? 10 : -10)); break; - case "stk": this.selectedInk?.forEach(i => i.rootDoc.strokeWidth = NumCast(i.rootDoc.strokeWidth) + (dirs === "up" ? .1 : -.1)); break; - case "wid": this.selectedInk?.filter(i => i.rootDoc._width && i.rootDoc._height).forEach(i => { - //redraw points - const oldWidth = NumCast(i.rootDoc._width); - const oldHeight = NumCast(i.rootDoc._height); - const oldX = NumCast(i.rootDoc.x); - const oldY = NumCast(i.rootDoc.y); - i.rootDoc._width = oldWidth + (dirs === "up" ? 10 : - 10); - this._lock && (i.rootDoc._height = (i.rootDoc._width / oldWidth * NumCast(i.rootDoc._height))); - const doc = Document(i.rootDoc); - if (doc.type === DocumentType.INK && doc.x && doc.y && doc._height && doc._width) { - console.log(doc.x, doc.y, doc._height, doc._width); - const ink = Cast(doc.data, InkField)?.inkData; - console.log(ink); - if (ink) { - const newPoints: { X: number, Y: number }[] = []; - ink.forEach(i => { - // (new x — oldx) + (oldxpoint * newWidt)/oldWidth - const newX = ((doc.x || 0) - oldX) + (i.X * (doc._width || 0)) / oldWidth; - const newY = ((doc.y || 0) - oldY) + (i.Y * (doc._height || 0)) / oldHeight; - newPoints.push({ X: newX, Y: newY }); - }); - doc.data = new InkField(newPoints); - } - } - }); - break; - case "hgt": this.selectedInk?.filter(i => i.rootDoc._width && i.rootDoc._height).forEach(i => { - const oldWidth = NumCast(i.rootDoc._width); - const oldHeight = NumCast(i.rootDoc._height); - const oldX = NumCast(i.rootDoc.x); - const oldY = NumCast(i.rootDoc.y); i.rootDoc._height = oldHeight + (dirs === "up" ? 10 : - 10); - this._lock && (i.rootDoc._width = (i.rootDoc._height / oldHeight * NumCast(i.rootDoc._width))); - const doc = Document(i.rootDoc); - if (doc.type === DocumentType.INK && doc.x && doc.y && doc._height && doc._width) { - console.log(doc.x, doc.y, doc._height, doc._width); - const ink = Cast(doc.data, InkField)?.inkData; - console.log(ink); - if (ink) { - const newPoints: { X: number, Y: number }[] = []; - ink.forEach(i => { - // (new x — oldx) + (oldxpoint * newWidt)/oldWidth - const newX = ((doc.x || 0) - oldX) + (i.X * (doc._width || 0)) / oldWidth; - const newY = ((doc.y || 0) - oldY) + (i.Y * (doc._height || 0)) / oldHeight; - newPoints.push({ X: newX, Y: newY }); - }); - doc.data = new InkField(newPoints); - } - } - }); - break; - } - } - - @undoBatch - @action - addPoints = (x: number, y: number, pts: { X: number, Y: number }[], index: number, control: { X: number, Y: number }[]) => { - this.selectedInk?.forEach(action(inkView => { - if (this.selectedInk?.length === 1) { - const doc = Document(inkView.rootDoc); - if (doc.type === DocumentType.INK) { - const ink = Cast(doc.data, InkField)?.inkData; - if (ink) { - const newPoints: { X: number, Y: number }[] = []; - var counter = 0; - for (var k = 0; k < index; k++) { - control.forEach(pt => (pts[k].X === pt.X && pts[k].Y === pt.Y) && counter++); - } - //decide where to put the new coordinate - const spNum = Math.floor(counter / 2) * 4 + 2; - - for (var i = 0; i < spNum; i++) { - newPoints.push({ X: ink[i].X, Y: ink[i].Y }); - } - for (var j = 0; j < 4; j++) { - newPoints.push({ X: x, Y: y }); - - } - for (var i = spNum; i < ink.length; i++) { - newPoints.push({ X: ink[i].X, Y: ink[i].Y }); - } - this._currPoint = -1; - doc.data = new InkField(newPoints); - } - } - } - })); - } - - @undoBatch - @action - deletePoints = () => { - this.selectedInk?.forEach(action(inkView => { - if (this.selectedInk?.length === 1 && this._currPoint !== -1) { - const doc = Document(inkView.rootDoc); - if (doc.type === DocumentType.INK) { - const ink = Cast(doc.data, InkField)?.inkData; - if (ink && ink.length > 4) { - const newPoints: { X: number, Y: number }[] = []; - - console.log(ink.length, this._currPoint, Math.floor((this._currPoint + 2) / 4)); - - const toRemove = Math.floor(((this._currPoint + 2) / 4)); - for (var i = 0; i < ink.length; i++) { - if (Math.floor((i + 2) / 4) !== toRemove) { - console.log(i, toRemove); - newPoints.push({ X: ink[i].X, Y: ink[i].Y }); - } - } - this._currPoint = -1; - doc.data = new InkField(newPoints); - if (newPoints.length === 4) { - const newerPoints: { X: number, Y: number }[] = []; - newerPoints.push({ X: newPoints[0].X, Y: newPoints[0].Y }); - newerPoints.push({ X: newPoints[0].X, Y: newPoints[0].Y }); - newerPoints.push({ X: newPoints[3].X, Y: newPoints[3].Y }); - newerPoints.push({ X: newPoints[3].X, Y: newPoints[3].Y }); - doc.data = new InkField(newerPoints); - - } - } - } - } - })); - } - - @undoBatch - @action - rotate = (angle: number) => { - const _centerPoints: { X: number, Y: number }[] = []; - SelectionManager.SelectedDocuments().forEach(action(inkView => { - const doc = Document(inkView.rootDoc); - if (doc.type === DocumentType.INK && doc.x && doc.y && doc._width && doc._height && doc.data) { - const ink = Cast(doc.data, InkField)?.inkData; - if (ink) { - const xs = ink.map(p => p.X); - const ys = ink.map(p => p.Y); - const left = Math.min(...xs); - const top = Math.min(...ys); - const right = Math.max(...xs); - const bottom = Math.max(...ys); - _centerPoints.push({ X: left, Y: top }); - } - } - })); - - var index = 0; - SelectionManager.SelectedDocuments().forEach(action(inkView => { - const doc = Document(inkView.rootDoc); - if (doc.type === DocumentType.INK && doc.x && doc.y && doc._width && doc._height && doc.data) { - doc.rotation = Number(doc.rotation) + Number(angle); - const ink = Cast(doc.data, InkField)?.inkData; - if (ink) { - - const newPoints: { X: number, Y: number }[] = []; - ink.forEach(i => { - const newX = Math.cos(angle) * (i.X - _centerPoints[index].X) - Math.sin(angle) * (i.Y - _centerPoints[index].Y) + _centerPoints[index].X; - const newY = Math.sin(angle) * (i.X - _centerPoints[index].X) + Math.cos(angle) * (i.Y - _centerPoints[index].Y) + _centerPoints[index].Y; - newPoints.push({ X: newX, Y: newY }); - }); - doc.data = new InkField(newPoints); - const xs = newPoints.map(p => p.X); - const ys = newPoints.map(p => p.Y); - const left = Math.min(...xs); - const top = Math.min(...ys); - const right = Math.max(...xs); - const bottom = Math.max(...ys); - - doc._height = (bottom - top); - doc._width = (right - left); - } - index++; - } - })); - } - - @undoBatch - @action - control = (xDiff: number, yDiff: number, controlNum: number) => { - this.selectedInk?.forEach(action(inkView => { - if (this.selectedInk?.length === 1) { - const doc = Document(inkView.rootDoc); - if (doc.type === DocumentType.INK && doc.x && doc.y && doc._width && doc._height && doc.data) { - const ink = Cast(doc.data, InkField)?.inkData; - if (ink) { - - const newPoints: { X: number, Y: number }[] = []; - const order = controlNum % 4; - for (var i = 0; i < ink.length; i++) { - if (controlNum === i || - (order === 0 && i === controlNum + 1) || - (order === 0 && controlNum !== 0 && i === controlNum - 2) || - (order === 0 && controlNum !== 0 && i === controlNum - 1) || - (order === 3 && i === controlNum - 1) || - (order === 3 && controlNum !== ink.length - 1 && i === controlNum + 1) || - (order === 3 && controlNum !== ink.length - 1 && i === controlNum + 2) - || ((ink[0].X === ink[ink.length - 1].X) && (ink[0].Y === ink[ink.length - 1].Y) && (i === 0 || i === ink.length - 1) && (controlNum === 0 || controlNum === ink.length - 1)) - ) { - newPoints.push({ X: ink[i].X - (xDiff * inkView.props.ScreenToLocalTransform().Scale), Y: ink[i].Y - (yDiff * inkView.props.ScreenToLocalTransform().Scale) }); - } - else { - newPoints.push({ X: ink[i].X, Y: ink[i].Y }); - } - } - const oldx = doc.x; - const oldy = doc.y; - const xs = ink.map(p => p.X); - const ys = ink.map(p => p.Y); - const left = Math.min(...xs); - const top = Math.min(...ys); - doc.data = new InkField(newPoints); - const xs2 = newPoints.map(p => p.X); - const ys2 = newPoints.map(p => p.Y); - const left2 = Math.min(...xs2); - const top2 = Math.min(...ys2); - const right2 = Math.max(...xs2); - const bottom2 = Math.max(...ys2); - doc._height = (bottom2 - top2); - doc._width = (right2 - left2); - //if points move out of bounds - - doc.x = oldx - (left - left2); - doc.y = oldy - (top - top2); - - } - } - } - })); - } - - @undoBatch - @action - switchStk = (color: ColorState) => { - const val = String(color.hex); - this.colorStk = val; - return true; - } - - @undoBatch - @action - switchFil = (color: ColorState) => { - const val = String(color.hex); - this.colorFil = val; - return true; - } - - - colorPicker(setter: (color: string) => {}, type: string) { - return <div className="btn-group-palette" key="colorpicker" style={{ width: 160, margin: 10 }}> - <SketchPicker onChange={type === "stk" ? this.switchStk : this.switchFil} presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']} - color={type === "stk" ? this.colorStk : this.colorFil} /> - </div>; - } - inputBox = (key: string, value: any, setter: (val: string) => {}) => { - return <> - <input style={{ color: "black", width: 40, position: "absolute", right: 20 }} - type="text" value={value} - onChange={undoBatch(action((e) => setter(e.target.value)))} - autoFocus /> - <button className="antiMenu-Buttonup" key="up1" onPointerDown={undoBatch(action(() => this.upDownButtons("up", key)))}> - ˄ - </button> - <br /> - <button className="antiMenu-Buttonup" key="down1" onPointerDown={undoBatch(action(() => this.upDownButtons("down", key)))} style={{ marginTop: -8 }}> - ˅ - </button> - </>; - } - - inputBoxDuo = (key: string, value: any, setter: (val: string) => {}, title1: string, key2: string, value2: any, setter2: (val: string) => {}, title2: string) => { - return <> - {title1} - <p style={{ marginTop: -20, right: 70, position: "absolute" }}>{title2}</p> - - <input style={{ color: "black", width: 40, position: "absolute", right: 130 }} - type="text" value={value} - onChange={e => setter(e.target.value)} - autoFocus /> - <button className="antiMenu-Buttonup" key="up2" onPointerDown={undoBatch(action(() => this.upDownButtons("up", key)))} style={{ right: 110 }}> - ˄ - </button> - <button className="antiMenu-Buttonup" key="down2" onPointerDown={undoBatch(action(() => this.upDownButtons("down", key)))} style={{ marginTop: 12, right: 110 }}> - ˅ - </button> - {title2 === "" ? "" : <> - <input style={{ color: "black", width: 40, position: "absolute", right: 20 }} - type="text" value={value2} - onChange={e => setter2(e.target.value)} - autoFocus /> - <button className="antiMenu-Buttonup" key="up3" onPointerDown={undoBatch(action(() => this.upDownButtons("up", key2)))}> - ˄ - </button> - <br /> - <button className="antiMenu-Buttonup" key="down3" onPointerDown={undoBatch(action(() => this.upDownButtons("down", key2)))} style={{ marginTop: -8 }}> - ˅ - </button></>} - </>; - } - - - colorButton(value: string, setter: () => {}) { - return <> - <button className="antimodeMenu-button" key="color" onPointerDown={undoBatch(action(e => setter()))} style={{ position: "relative", marginTop: -5 }}> - <div className="color-previewII" style={{ backgroundColor: value ?? "121212" }} /> - {value === "" || value === "transparent" ? <p style={{ fontSize: 25, color: "red", marginTop: -23, position: "fixed" }}>☒</p> : ""} - </button> - </>; - } - - controlPointsButton() { - return <> - <button className="antimodeMenu-button" title="Edit points" key="bezier" onPointerDown={action(() => this._controlBtn = this._controlBtn ? false : true)} style={{ position: "relative", marginTop: 10, backgroundColor: this._controlBtn ? "black" : "" }}> - <FontAwesomeIcon icon="bezier-curve" size="lg" /> - </button> - <button className="antimodeMenu-button" title="Lock ratio" key="ratio" onPointerDown={action(() => this._lock = this._lock ? false : true)} style={{ position: "relative", marginTop: 10, backgroundColor: this._lock ? "black" : "" }}> - <FontAwesomeIcon icon="lock" size="lg" /> - - </button> - <button className="antimodeMenu-button" key="rotate" title="Rotate 90˚" onPointerDown={action(() => this.rotate(Math.PI / 2))} style={{ position: "relative", marginTop: 10, fontSize: 15 }}> - ⟲ - </button> - <br /> <br /> - </>; - } - - lockRatioButton() { - return <> - <button className="antimodeMenu-button" key="lock" onPointerDown={action(() => this._lock = this._lock ? false : true)} style={{ position: "absolute", right: 80, backgroundColor: this._lock ? "black" : "" }}> - {/* <FontAwesomeIcon icon="bezier-curve" size="lg" /> */} - <FontAwesomeIcon icon="lock" size="lg" /> - - </button> - <br /> <br /> - </>; - } - - rotate90Button() { - return <> - <button className="antimodeMenu-button" key="rot" onPointerDown={action(() => this.rotate(Math.PI / 2))} style={{ position: "absolute", right: 80, }}> - {/* <FontAwesomeIcon icon="bezier-curve" size="lg" /> */} - ⟲ - - </button> - <br /> <br /> - </>; - } - @computed get fillButton() { return this.colorButton(this.colorFil, () => { this._fillBtn = !this._fillBtn; this._lineBtn = false; return true; }); } - @computed get lineButton() { return this.colorButton(this.colorStk, () => { this._lineBtn = !this._lineBtn; this._fillBtn = false; return true; }); } - - @computed get fillPicker() { return this.colorPicker((color: string) => this.colorFil = color, "fil"); } - @computed get linePicker() { return this.colorPicker((color: string) => this.colorStk = color, "stk"); } - - @computed get stkInput() { return this.inputBox("stk", this.widthStk, (val: string) => this.widthStk = val); } - @computed get dashInput() { return this.inputBox("dsh", this.widthStk, (val: string) => this.widthStk = val); } - - @computed get hgtInput() { return this.inputBoxDuo("hgt", this.shapeHgt, (val: string) => this.shapeHgt = val, "H:", "wid", this.shapeWid, (val: string) => this.shapeWid = val, "W:"); } - @computed get widInput() { return this.inputBox("wid", this.shapeWid, (val: string) => this.shapeWid = val); } - @computed get rotInput() { return this.inputBoxDuo("rot", this.shapeRot, (val: string) => { this.rotate(Number(val) - Number(this.shapeRot)); this.shapeRot = val; return true; }, "∠:", "rot", this.shapeRot, (val: string) => this.shapeRot = val, ""); } - - @computed get YpsInput() { return this.inputBox("Yps", this.shapeYps, (val: string) => this.shapeYps = val); } - - @computed get controlPoints() { return this.controlPointsButton(); } - @computed get lockRatio() { return this.lockRatioButton(); } - @computed get rotate90() { return this.rotate90Button(); } - @computed get XpsInput() { return this.inputBoxDuo("Xps", this.shapeXps, (val: string) => this.shapeXps = val, "X:", "Yps", this.shapeYps, (val: string) => this.shapeYps = val, "Y:"); } - - - @computed get propertyGroupItems() { - const fillCheck = <div key="fill" style={{ display: (this._subOpen[0] && this.selectedInk && this.selectedInk.length >= 1) ? "" : "none", width: "inherit", backgroundColor: "#323232", color: "white", }}> - Fill: - {this.fillButton} - <div style={{ float: "left", width: 100 }} > - Stroke: - {this.lineButton} - </div> - - {this._fillBtn ? this.fillPicker : ""} - {this._lineBtn ? this.linePicker : ""} - {this._fillBtn || this._lineBtn ? "" : <br />} - {(this.solidStk || this.dashdStk) ? "Width" : ""} - {(this.solidStk || this.dashdStk) ? this.stkInput : ""} - - - {(this.solidStk || this.dashdStk) ? <input type="range" defaultValue={Number(this.widthStk)} min={1} max={100} onChange={undoBatch(action((e) => this.widthStk = e.target.value))} /> : (null)} - <br /> - {(this.solidStk || this.dashdStk) ? <> - <p style={{ position: "absolute", fontSize: 12 }}>Arrow Head</p> - <input key="markHead" className="formatShapePane-inputBtn" type="checkbox" checked={this.markHead !== ""} onChange={undoBatch(action(() => this.markHead = this.markHead ? "" : "arrow"))} style={{ position: "absolute", right: 110, width: 20 }} /> - <p style={{ position: "absolute", fontSize: 12, right: 30 }}>Arrow End</p> - <input key="markTail" className="formatShapePane-inputBtn" type="checkbox" checked={this.markTail !== ""} onChange={undoBatch(action(() => this.markTail = this.markTail ? "" : "arrow"))} style={{ position: "absolute", right: 0, width: 20 }} /> - <br /> - </> : ""} - Dash: <input key="markHead" className="formatShapePane-inputBtn" type="checkbox" checked={this.dashdStk === "2"} onChange={undoBatch(action(() => this.dashdStk = this.dashdStk === "2" ? "0" : "2"))} style={{ position: "absolute", right: 110, width: 20 }} /> - - - - </div>; - - - - const sizeCheck = - - <div key="sizeCheck" style={{ display: (this._subOpen[1] && this.selectedInk && this.selectedInk.length >= 1) ? "" : "none", width: "inherit", backgroundColor: "#323232", color: "white", }}> - {this.controlPoints} - {this.hgtInput} - {this.XpsInput} - {this.rotInput} - - </div>; - - - const subMenus = this._currMode === "fill-drip" ? [`Appearance`, 'Transform'] : []; - const menuItems = this._currMode === "fill-drip" ? [fillCheck, sizeCheck] : []; - const indexOffset = 0; - - return <div className="antimodeMenu-sub" key="submenu" style={{ position: "absolute", width: "inherit", top: 60 }}> - {subMenus.map((subMenu, i) => - <div key={subMenu} style={{ width: "inherit" }}> - <button className="antimodeMenu-button" onPointerDown={action(() => this._subOpen[i + indexOffset] = !this._subOpen[i + indexOffset])} - style={{ backgroundColor: "121212", position: "relative", width: "inherit" }}> - {this._subOpen[i + indexOffset] ? "▼" : "▶︎"} - {subMenu} - </button> - {menuItems[i]} - </div>)} - </div>; - } - - @computed get closeBtn() { - return <button className="antimodeMenu-button" key="close" onPointerDown={action(() => this.closePane())} style={{ position: "absolute", right: 0 }}> - X - </button>; - } - - @computed get propertyGroupBtn() { - return <div className="antimodeMenu-button-tab" key="modes"> - {this._mode.map(mode => - <button className="antimodeMenu-button" key={mode} onPointerDown={action(() => this._currMode = mode)} - style={{ backgroundColor: this._currMode === mode ? "121212" : "", position: "relative", top: 30 }}> - <FontAwesomeIcon icon={mode as IconProp} size="lg" /> - </button>)} - </div>; - } - - render() { - return this.getElementVert([this.closeBtn, - this.propertyGroupItems]); - } -}
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx index 2cfe0183c..fd4fa0c7e 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx @@ -1,13 +1,12 @@ import React = require("react"); -import AntimodeMenu, { AntimodeMenuProps } from "../../AntimodeMenu"; -import { observer } from "mobx-react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { unimplementedFunction } from "../../../../Utils"; -import { undoBatch } from "../../../util/UndoManager"; import { Tooltip } from "@material-ui/core"; +import { observer } from "mobx-react"; +import { unimplementedFunction } from "../../../../Utils"; +import { AntimodeMenu, AntimodeMenuProps } from "../../AntimodeMenu"; @observer -export default class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> { +export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> { static Instance: MarqueeOptionsMenu; public createCollection: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; @@ -16,6 +15,7 @@ export default class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> public inkToText: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; public showMarquee: () => void = unimplementedFunction; public hideMarquee: () => void = unimplementedFunction; + public pinWithView: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; constructor(props: Readonly<{}>) { super(props); @@ -24,6 +24,7 @@ export default class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> } render() { + const presPinWithViewIcon = <img src="/assets/pinWithView.png" style={{ margin: "auto", width: 19, transform: 'translate(-2px, -2px)' }} />; const buttons = [ <Tooltip key="group" title={<><div className="dash-tooltip">Create a Collection</div></>} placement="bottom"> <button @@ -53,6 +54,13 @@ export default class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> <FontAwesomeIcon icon="font" size="lg" /> </button> </Tooltip>, + <Tooltip key="pinWithView" title={<><div className="dash-tooltip">Pin with selected view</div></>} placement="bottom"> + <button + className="antimodeMenu-button" + onPointerDown={this.pinWithView}> + <>{presPinWithViewIcon}</> + </button> + </Tooltip>, ]; return this.getElement(buttons); } diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index c0b19fcd2..7c64fd429 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -1,27 +1,31 @@ import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; -import { Doc, Opt, DocListCast, DataSym, AclEdit, AclAddonly, AclAdmin } from "../../../../fields/Doc"; -import { GetEffectiveAcl } from "../../../../fields/util"; +import { AclAddonly, AclAdmin, AclEdit, DataSym, Doc, DocListCast, Opt } from "../../../../fields/Doc"; import { InkData, InkField, InkTool } from "../../../../fields/InkField"; import { List } from "../../../../fields/List"; import { RichTextField } from "../../../../fields/RichTextField"; import { SchemaHeaderField } from "../../../../fields/SchemaHeaderField"; import { Cast, FieldValue, NumCast, StrCast } from "../../../../fields/Types"; +import { GetEffectiveAcl } from "../../../../fields/util"; import { Utils } from "../../../../Utils"; import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; import { Docs, DocumentOptions, DocUtils } from "../../../documents/Documents"; +import { DocumentManager } from "../../../util/DocumentManager"; import { SelectionManager } from "../../../util/SelectionManager"; import { Transform } from "../../../util/Transform"; -import { undoBatch } from "../../../util/UndoManager"; +import { undoBatch, UndoManager } from "../../../util/UndoManager"; import { ContextMenu } from "../../ContextMenu"; import { FormattedTextBox } from "../../nodes/formattedText/FormattedTextBox"; import { PreviewCursor } from "../../PreviewCursor"; +import { CollectionDockingView } from "../CollectionDockingView"; import { SubCollectionViewProps } from "../CollectionSubView"; -import { CollectionView } from "../CollectionView"; -import MarqueeOptionsMenu from "./MarqueeOptionsMenu"; +import { CollectionView, CollectionViewType } from "../CollectionView"; +import { MarqueeOptionsMenu } from "./MarqueeOptionsMenu"; import "./MarqueeView.scss"; import React = require("react"); -import { ContextMenuItem } from "../../ContextMenuItem"; +import { Id } from "../../../../fields/FieldSymbols"; +import { CurrentUserUtils } from "../../../util/CurrentUserUtils"; +import { PresMovement } from "../../nodes/PresBox"; interface MarqueeViewProps { getContainerTransform: () => Transform; @@ -30,7 +34,7 @@ interface MarqueeViewProps { selectDocuments: (docs: Doc[]) => void; addLiveTextDocument: (doc: Doc) => void; isSelected: () => boolean; - nudge: (x: number, y: number) => boolean; + nudge?: (x: number, y: number) => boolean; setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void; } @@ -75,7 +79,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque const [x, y] = this.props.getTransform().transformPoint(this._downX, this._downY); if (e.key === "?") { cm.setDefaultItem("?", (str: string) => this.props.addDocTab( - Docs.Create.WebDocument(`https://bing.com/search?q=${str}`, { _width: 200, x, y, _nativeHeight: 962, _nativeWidth: 850, isAnnotating: false, title: "bing", UseCors: true }), "onRight")); + Docs.Create.WebDocument(`https://bing.com/search?q=${str}`, { _fitWidth: true, _width: 400, x, y, _height: 512, _nativeWidth: 850, isAnnotating: false, title: "bing", useCors: true }), "add:right")); cm.displayMenu(this._downX, this._downY); e.stopPropagation(); @@ -115,30 +119,29 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque })(); e.stopPropagation(); } else if (e.key === "b" && e.ctrlKey) { + // e.preventDefault(); + // navigator.clipboard.readText().then(text => { + // const ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== ""); + // if (ns.length === 1 && text.startsWith("http")) { + // this.props.addDocument(Docs.Create.ImageDocument(text, { _nativeWidth: 300, _width: 300, x: x, y: y }));// paste an image from its URL in the paste buffer + // } else { + // this.pasteTable(ns, x, y); + // } + // }); + // e.stopPropagation(); + e.preventDefault(); - navigator.clipboard.readText().then(text => { - const ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== ""); - if (ns.length === 1 && text.startsWith("http")) { - this.props.addDocument(Docs.Create.ImageDocument(text, { _nativeWidth: 300, _width: 300, x: x, y: y }));// paste an image from its URL in the paste buffer - } else { - this.pasteTable(ns, x, y); - } - }); + const slide = Doc.copyDragFactory(Doc.UserDoc().emptySlide as Doc)!; + slide.x = x; + slide.y = y; + FormattedTextBox.SelectOnLoad = slide[Id]; + this.props.addDocument(slide); + //setTimeout(() => SelectionManager.SelectDoc(DocumentManager.Instance.getDocumentView(slide)!, false)); e.stopPropagation(); } else if (!e.ctrlKey && !e.metaKey) { - FormattedTextBox.SelectOnLoadChar = FormattedTextBox.DefaultLayout ? e.key : ""; - const tbox = Docs.Create.TextDocument("", { - _width: 200, _height: 100, x: x, y: y, _autoHeight: true, _fontSize: StrCast(Doc.UserDoc().fontSize), - _fontFamily: StrCast(Doc.UserDoc().fontFamily), - title: "-typed text-" - }); - const template = FormattedTextBox.DefaultLayout; - if (template instanceof Doc) { - tbox._width = NumCast(template._width); - tbox.layoutKey = "layout_" + StrCast(template.title); - Doc.GetProto(tbox)[StrCast(tbox.layoutKey)] = template; - } - this.props.addLiveTextDocument(tbox); + FormattedTextBox.SelectOnLoadChar = FormattedTextBox.DefaultLayout && !this.props.ChildLayoutString ? e.key : ""; + FormattedTextBox.LiveTextUndo = UndoManager.StartBatch("live text batch"); + this.props.addLiveTextDocument(CurrentUserUtils.GetNewTextDoc("-typed text-", x, y, 200, 100, this.props.xMargin === 0)); e.stopPropagation(); } } @@ -241,6 +244,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this.hideMarquee(); MarqueeOptionsMenu.Instance.fadeOut(true); document.removeEventListener("pointerdown", hideMarquee); + document.removeEventListener("wheel", hideMarquee); }; if (!this._commandExecuted && (Math.abs(this.Bounds.height * this.Bounds.width) > 100)) { MarqueeOptionsMenu.Instance.createCollection = this.collection; @@ -250,7 +254,9 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque MarqueeOptionsMenu.Instance.showMarquee = this.showMarquee; MarqueeOptionsMenu.Instance.hideMarquee = this.hideMarquee; MarqueeOptionsMenu.Instance.jumpTo(e.clientX, e.clientY); + MarqueeOptionsMenu.Instance.pinWithView = this.pinWithView; document.addEventListener("pointerdown", hideMarquee); + document.addEventListener("wheel", hideMarquee); } else { this.hideMarquee(); } @@ -349,7 +355,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this.hideMarquee(); } - getCollection = action((selected: Doc[], creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, isBackground?: boolean) => { + getCollection = action((selected: Doc[], creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, _isBackground?: boolean) => { const newCollection = creator ? creator(selected, { title: "nested stack", }) : ((doc: Doc) => { Doc.GetProto(doc).data = new List<Doc>(selected); Doc.GetProto(doc).title = "nested freeform"; @@ -357,8 +363,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque return doc; })(Doc.MakeCopy(Doc.UserDoc().emptyCollection as Doc, true)); newCollection.system = undefined; - newCollection.isBackground = isBackground; - newCollection.backgroundColor = this.props.isAnnotationOverlay ? "#00000015" : isBackground ? "cyan" : undefined; + newCollection._isBackground = _isBackground; + newCollection.backgroundColor = this.props.isAnnotationOverlay ? "#00000015" : _isBackground ? "cyan" : undefined; newCollection._width = this.Bounds.width; newCollection._height = this.Bounds.height; newCollection.x = this.Bounds.left; @@ -381,6 +387,38 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque } @undoBatch @action + pinWithView = (e: KeyboardEvent | React.PointerEvent | undefined) => { + const doc = this.props.Document; + const bounds = this.Bounds; + const selected = this.marqueeSelect(false); + const curPres = Cast(Doc.UserDoc().activePresentation, Doc) as Doc; + if (curPres) { + const pinDoc = Doc.MakeAlias(doc); + pinDoc.presentationTargetDoc = doc; + pinDoc.presMovement = PresMovement.Zoom; + pinDoc.context = curPres; + Doc.AddDocToList(curPres, "data", pinDoc); + if (curPres.expandBoolean) pinDoc.presExpandInlineButton = true; + if (!DocumentManager.Instance.getDocumentView(curPres)) { + CollectionDockingView.AddSplit(curPres, "right"); + } + if (e instanceof KeyboardEvent ? e.key === "c" : true) { + const x = this.Bounds.left + this.Bounds.width / 2; + const y = this.Bounds.top + this.Bounds.height / 2; + const panelWidth: number = this.props.PanelWidth(); + const panelHeight: number = this.props.PanelHeight(); + const scale = Math.min(Number(panelWidth) / this.Bounds.width, Number(panelHeight) / this.Bounds.height); + pinDoc.presPinView = true; + pinDoc.presPinViewX = x; + pinDoc.presPinViewY = y; + pinDoc.presPinViewScale = scale; + } + } + MarqueeOptionsMenu.Instance.fadeOut(true); + this.hideMarquee(); + } + + @undoBatch @action collection = (e: KeyboardEvent | React.PointerEvent | undefined) => { const bounds = this.Bounds; const selected = this.marqueeSelect(false); @@ -487,12 +525,12 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque const selected = this.marqueeSelect(false); selected.map(d => { this.props.removeDocument(d); - d.x = NumCast(d.x) - bounds.left - bounds.width / 2; - d.y = NumCast(d.y) - bounds.top - bounds.height / 2; + d.x = NumCast(d.x) - bounds.left; + d.y = NumCast(d.y) - bounds.top; d.page = -1; return d; }); - const summary = Docs.Create.TextDocument("", { x: bounds.left + bounds.width / 2, y: bounds.top + bounds.height / 2, _width: 200, _height: 200, _fitToBox: true, _showSidebar: true, title: "overview" }); + const summary = Docs.Create.TextDocument("", { x: bounds.left, y: bounds.top, _width: 200, _height: 200, _fitToBox: true, _showSidebar: true, title: "overview" }); const portal = Doc.MakeAlias(summary); Doc.GetProto(summary)[Doc.LayoutFieldKey(summary) + "-annotations"] = new List<Doc>(selected); Doc.GetProto(summary).layout_portal = CollectionView.LayoutString(Doc.LayoutFieldKey(summary) + "-annotations"); @@ -644,7 +682,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque marqueeSelect(selectBackgrounds: boolean = true) { const selRect = this.Bounds; const selection: Doc[] = []; - this.props.activeDocuments().filter(doc => !doc.isBackground && !doc.z).map(doc => { + this.props.activeDocuments().filter(doc => !doc._isBackground && !doc.z).map(doc => { const layoutDoc = Doc.Layout(doc); const x = NumCast(doc.x); const y = NumCast(doc.y); @@ -719,14 +757,12 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque </div>; } else { - //subtracted 250 for offset var str: string = ""; for (var i = 0; i < this._pointsX.length; i++) { - var x = 0; - x = this._pointsX[i] - 250; - str += x.toString(); + const pt = this.props.getContainerTransform().transformPoint(this._pointsX[i], this._pointsY[i]); + str += pt[0].toString(); str += ","; - str += this._pointsY[i].toString(); + str += pt[1].toString(); str += (" "); } @@ -747,7 +783,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque render() { return <div className="marqueeView" - style={{ overflow: StrCast(this.props.Document._overflow), cursor: MarqueeView.DragMarquee && this ? "crosshair" : "hand" }} + style={{ overflow: !this.props.ContainingCollectionView && this.props.annotationsKey ? "visible" : StrCast(this.props.Document._overflow), cursor: MarqueeView.DragMarquee && this ? "crosshair" : "hand" }} onDragOver={e => e.preventDefault()} onScroll={(e) => e.currentTarget.scrollTop = e.currentTarget.scrollLeft = 0} onClick={this.onClick} onPointerDown={this.onPointerDown}> {this._visible ? this.marqueeDiv : null} diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.scss b/src/client/views/collections/collectionGrid/CollectionGridView.scss index 4d8473be9..a6171af51 100644 --- a/src/client/views/collections/collectionGrid/CollectionGridView.scss +++ b/src/client/views/collections/collectionGrid/CollectionGridView.scss @@ -13,13 +13,6 @@ display: flex; flex-direction: row; - .imageBox-cont img { - height: auto; - width: auto; - max-height: 100%; - max-width: 100%; - } - .react-grid-layout { width: 100%; } @@ -134,17 +127,6 @@ } -// .documentDecorations-container .documentDecorations-resizer { -// pointer-events: none; -// } - -// #documentDecorations-bottomRightResizer, -// #documentDecorations-bottomLeftResizer, -// #documentDecorations-topRightResizer, -// #documentDecorations-topLeftResizer { -// visibility: collapse; -// } - /* Chrome, Safari, Edge, Opera */ input::-webkit-outer-spin-button, diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.tsx b/src/client/views/collections/collectionGrid/CollectionGridView.tsx index e6ac7021a..d31427829 100644 --- a/src/client/views/collections/collectionGrid/CollectionGridView.tsx +++ b/src/client/views/collections/collectionGrid/CollectionGridView.tsx @@ -6,7 +6,7 @@ import { documentSchema } from '../../../../fields/documentSchemas'; import { Id } from '../../../../fields/FieldSymbols'; import { makeInterface } from '../../../../fields/Schema'; import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; -import { emptyFunction, returnFalse, returnZero, setupMoveUpEvents } from '../../../../Utils'; +import { emptyFunction, returnFalse, returnZero, setupMoveUpEvents, OmitKeys } from '../../../../Utils'; import { Docs } from '../../../documents/Documents'; import { DragManager } from '../../../util/DragManager'; import { SnappingManager } from '../../../util/SnappingManager'; @@ -94,8 +94,8 @@ export class CollectionGridView extends CollectionSubView(GridSchema) { */ unflexedPosition(index: number): Omit<Layout, "i"> { return { - x: (index % Math.floor(this.numCols / this.defaultW)) * this.defaultW, - y: Math.floor(index / Math.floor(this.numCols / this.defaultH)) * this.defaultH, + x: (index % (Math.floor(this.numCols / this.defaultW) || 1)) * this.defaultW, + y: Math.floor(index / (Math.floor(this.numCols / this.defaultH) || 1)) * this.defaultH, w: this.defaultW, h: this.defaultH, static: true @@ -162,11 +162,9 @@ export class CollectionGridView extends CollectionSubView(GridSchema) { */ getDisplayDoc(layout: Doc, dxf: () => Transform, width: () => number, height: () => number) { return <ContentFittingDocumentView - {...this.props} + {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} Document={layout} DataDoc={layout.resolvedDataDoc as Doc} - NativeHeight={returnZero} - NativeWidth={returnZero} backgroundColor={this.props.backgroundColor} ContainingCollectionDoc={this.props.Document} PanelWidth={width} @@ -175,7 +173,7 @@ export class CollectionGridView extends CollectionSubView(GridSchema) { onClick={this.onChildClickHandler} renderDepth={this.props.renderDepth + 1} parentActive={this.props.active} - display={StrCast(this.props.Document.display, "contents")} // sets the css display type of the ContentFittingDocumentView component + dontCenter={true} />; } diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx index 21d283547..b99bef15e 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx @@ -213,9 +213,10 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu return this.props.addDocTab(doc, where); } getDisplayDoc(layout: Doc, dxf: () => Transform, width: () => number, height: () => number) { + const layoutTemp = this.props.DataDoc ? true : undefined; return <ContentFittingDocumentView Document={layout} - DataDoc={layout.resolvedDataDoc as Doc} + DataDoc={layout.resolvedDataDoc as Doc || (layoutTemp ? layout : undefined)} backgroundColor={this.props.backgroundColor} LayoutTemplate={this.props.ChildLayoutTemplate} LayoutTemplateString={this.props.ChildLayoutString} @@ -224,8 +225,6 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu renderDepth={this.props.renderDepth + 1} PanelWidth={width} PanelHeight={height} - NativeHeight={returnZero} - NativeWidth={returnZero} fitToBox={false} rootSelected={this.rootSelected} dropAction={StrCast(this.props.Document.childDropAction) as dropActionType} @@ -234,6 +233,8 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu ScreenToLocalTransform={dxf} focus={this.props.focus} docFilters={this.docFilters} + docRangeFilters={this.docRangeFilters} + searchFilterDocs={this.searchFilterDocs} ContainingCollectionDoc={this.props.CollectionView?.props.Document} ContainingCollectionView={this.props.CollectionView} addDocument={this.props.addDocument} diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx index d02088a6c..c5e1816d9 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx @@ -224,8 +224,6 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument) renderDepth={this.props.renderDepth + 1} PanelWidth={width} PanelHeight={height} - NativeHeight={returnZero} - NativeWidth={returnZero} fitToBox={false} rootSelected={this.rootSelected} dropAction={StrCast(this.props.Document.childDropAction) as dropActionType} @@ -234,6 +232,8 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument) ScreenToLocalTransform={dxf} focus={this.props.focus} docFilters={this.docFilters} + docRangeFilters={this.docRangeFilters} + searchFilterDocs={this.searchFilterDocs} ContainingCollectionDoc={this.props.CollectionView?.props.Document} ContainingCollectionView={this.props.CollectionView} addDocument={this.props.addDocument} diff --git a/src/client/views/globalCssVariables.scss b/src/client/views/globalCssVariables.scss index 7eb07ff55..1539d1399 100644 --- a/src/client/views/globalCssVariables.scss +++ b/src/client/views/globalCssVariables.scss @@ -36,6 +36,8 @@ $remoteCursors-zindex: 997; // ... not sure what level the remote cursors should $COLLECTION_BORDER_WIDTH: 1; $MINIMIZED_ICON_SIZE:25; $MAX_ROW_HEIGHT: 44px; +$DFLT_IMAGE_NATIVE_DIM: 900px; +$MENU_PANEL_WIDTH: 60px; :export { contextMenuZindex: $contextMenu-zindex; @@ -45,4 +47,6 @@ $MAX_ROW_HEIGHT: 44px; SEARCH_THUMBNAIL_SIZE: $search-thumnail-size; ANTIMODEMENU_HEIGHT: $antimodemenu-height; SEARCH_PANEL_HEIGHT: $searchpanel-height; + DFLT_IMAGE_NATIVE_DIM: $DFLT_IMAGE_NATIVE_DIM; + MENU_PANEL_WIDTH: $MENU_PANEL_WIDTH; }
\ No newline at end of file diff --git a/src/client/views/globalCssVariables.scss.d.ts b/src/client/views/globalCssVariables.scss.d.ts index fd1f60f13..fb3a327d0 100644 --- a/src/client/views/globalCssVariables.scss.d.ts +++ b/src/client/views/globalCssVariables.scss.d.ts @@ -7,6 +7,8 @@ interface IGlobalScss { SEARCH_THUMBNAIL_SIZE: string; ANTIMODEMENU_HEIGHT: string; SEARCH_PANEL_HEIGHT: string; + DFLT_IMAGE_NATIVE_DIM: string; + MENU_PANEL_WIDTH: string; } declare const globalCssVariables: IGlobalScss; diff --git a/src/client/views/linking/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx index ed64bde32..3713a1026 100644 --- a/src/client/views/linking/LinkEditor.tsx +++ b/src/client/views/linking/LinkEditor.tsx @@ -1,5 +1,3 @@ -import { library } from "@fortawesome/fontawesome-svg-core"; -import { faArrowLeft, faCog, faEllipsisV, faExchangeAlt, faPlus, faTable, faTimes, faTrash } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Tooltip } from "@material-ui/core"; import { action, computed, observable } from "mobx"; @@ -12,9 +10,6 @@ import { undoBatch } from "../../util/UndoManager"; import './LinkEditor.scss'; import React = require("react"); -library.add(faArrowLeft, faEllipsisV, faTable, faTrash, faCog, faExchangeAlt, faTimes, faPlus); - - interface GroupTypesDropdownProps { groupType: string; setGroupType: (group: string) => void; @@ -369,7 +364,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> { <div className="linkEditor-followingDropdown-dropdown"> <div className="linkEditor-followingDropdown-header" onPointerDown={this.changeDropdown}> - {StrCast(this.props.linkDoc.followLinkLocation, "Default")} + {StrCast(this.props.linkDoc.followLinkLocation, "default")} <FontAwesomeIcon className="linkEditor-followingDropdown-icon" icon={this.openDropdown ? "chevron-up" : "chevron-down"} size={"lg"} /> @@ -377,17 +372,37 @@ export class LinkEditor extends React.Component<LinkEditorProps> { <div className="linkEditor-followingDropdown-optionsList" style={{ display: this.openDropdown ? "" : "none" }}> <div className="linkEditor-followingDropdown-option" - onPointerDown={() => this.changeFollowBehavior("Default")}> + onPointerDown={() => this.changeFollowBehavior("default")}> Default </div> <div className="linkEditor-followingDropdown-option" - onPointerDown={() => this.changeFollowBehavior("onRight")}> - Always open in a new pane + onPointerDown={() => this.changeFollowBehavior("add:left")}> + Always open in new left pane + </div> + <div className="linkEditor-followingDropdown-option" + onPointerDown={() => this.changeFollowBehavior("add:right")}> + Always open in new right pane + </div> + <div className="linkEditor-followingDropdown-option" + onPointerDown={() => this.changeFollowBehavior("replace:right")}> + Always replace right tab </div> <div className="linkEditor-followingDropdown-option" - onPointerDown={() => this.changeFollowBehavior("inTab")}> + onPointerDown={() => this.changeFollowBehavior("replace:left")}> + Always replace left tab + </div> + <div className="linkEditor-followingDropdown-option" + onPointerDown={() => this.changeFollowBehavior("fullScreen")}> + Always open full screen + </div> + <div className="linkEditor-followingDropdown-option" + onPointerDown={() => this.changeFollowBehavior("add")}> Always open in a new tab </div> + <div className="linkEditor-followingDropdown-option" + onPointerDown={() => this.changeFollowBehavior("replace")}> + Replace Tab + </div> {this.props.linkDoc.linksToAnnotation ? <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior("openExternal")}> @@ -405,7 +420,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> { } render() { - const destination = LinkManager.Instance.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc); + const destination = LinkManager.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc); const groups = [this.props.linkDoc].map(groupDoc => { return <LinkGroupEditor key={"gred-" + StrCast(groupDoc.linkRelationship)} linkDoc={this.props.linkDoc} diff --git a/src/client/views/linking/LinkMenu.scss b/src/client/views/linking/LinkMenu.scss index 4dc25031d..0e03b46db 100644 --- a/src/client/views/linking/LinkMenu.scss +++ b/src/client/views/linking/LinkMenu.scss @@ -4,8 +4,6 @@ width: auto; height: auto; position: absolute; - top: 0; - left: 0; z-index: 999; .linkMenu-list { diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx index 8ecde959f..3b4912d3d 100644 --- a/src/client/views/linking/LinkMenu.tsx +++ b/src/client/views/linking/LinkMenu.tsx @@ -1,18 +1,14 @@ -import { action, observable, computed } from "mobx"; +import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; -import { DocumentView } from "../nodes/DocumentView"; -import { LinkEditor } from "./LinkEditor"; -import './LinkMenu.scss'; -import React = require("react"); import { Doc } from "../../../fields/Doc"; import { LinkManager } from "../../util/LinkManager"; -import { LinkMenuGroup } from "./LinkMenuGroup"; -import { faTrash } from '@fortawesome/free-solid-svg-icons'; -import { library } from "@fortawesome/fontawesome-svg-core"; import { DocumentLinksButton } from "../nodes/DocumentLinksButton"; +import { DocumentView } from "../nodes/DocumentView"; import { LinkDocPreview } from "../nodes/LinkDocPreview"; - -library.add(faTrash); +import { LinkEditor } from "./LinkEditor"; +import './LinkMenu.scss'; +import { LinkMenuGroup } from "./LinkMenuGroup"; +import React = require("react"); interface Props { docView: DocumentView; @@ -93,15 +89,14 @@ export class LinkMenu extends React.Component<Props> { } render() { - console.log("computed", this.position.x, this.position.b); const sourceDoc = this.props.docView.props.Document; const groups: Map<string, Doc[]> = LinkManager.Instance.getRelatedGroupedLinks(sourceDoc); - return <div className="linkMenu" ref={this._linkMenuRef} > + return <div className="linkMenu" style={{ left: this.position.x, top: this.props.docView.topMost ? undefined : this.position.b + 15, bottom: this.props.docView.topMost ? 20 : undefined }} ref={this._linkMenuRef} > {!this._editingLink ? - <div className="linkMenu-list" style={{ left: this.position.x, top: this.position.b + 15 }}> + <div className="linkMenu-list" > {this.renderAllGroups(groups)} </div> : - <div className="linkMenu-listEditor" style={{ left: this.position.x, top: this.position.b + 15 }}> + <div className="linkMenu-listEditor"> <LinkEditor sourceDoc={this.props.docView.props.Document} linkDoc={this._editingLink} showLinks={action(() => this._editingLink = undefined)} /> </div> diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx index 2ae87ac13..29e1d921c 100644 --- a/src/client/views/linking/LinkMenuGroup.tsx +++ b/src/client/views/linking/LinkMenuGroup.tsx @@ -48,7 +48,7 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> { document.removeEventListener("pointermove", this.onLinkButtonMoved); document.removeEventListener("pointerup", this.onLinkButtonUp); - const targets = this.props.group.map(l => LinkManager.Instance.getOppositeAnchor(l, this.props.sourceDoc)).filter(d => d) as Doc[]; + const targets = this.props.group.map(l => LinkManager.getOppositeAnchor(l, this.props.sourceDoc)).filter(d => d) as Doc[]; StartLinkTargetsDrag(this._drag.current, this.props.docView, e.x, e.y, this.props.sourceDoc, targets); } e.stopPropagation(); @@ -66,11 +66,12 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> { } render() { - const groupItems = this.props.group.map(linkDoc => { - const destination = LinkManager.Instance.getOppositeAnchor(linkDoc, this.props.sourceDoc) || - LinkManager.Instance.getOppositeAnchor(linkDoc, Cast(linkDoc.anchor2, Doc, null).annotationOn === this.props.sourceDoc ? Cast(linkDoc.anchor2, Doc, null) : Cast(linkDoc.anchor1, Doc, null)); + const set = new Set<Doc>(this.props.group); + const groupItems = Array.from(set.keys()).map(linkDoc => { + const destination = LinkManager.getOppositeAnchor(linkDoc, this.props.sourceDoc) || + LinkManager.getOppositeAnchor(linkDoc, Cast(linkDoc.anchor2, Doc, null).annotationOn === this.props.sourceDoc ? Cast(linkDoc.anchor2, Doc, null) : Cast(linkDoc.anchor1, Doc, null)); if (destination && this.props.sourceDoc) { - return <LinkMenuItem key={destination[Id] + this.props.sourceDoc[Id]} + return <LinkMenuItem key={linkDoc[Id]} groupType={this.props.groupType} addDocTab={this.props.addDocTab} docView={this.props.docView} diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx index 21c666a4d..3a8d41fef 100644 --- a/src/client/views/linking/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -1,27 +1,23 @@ -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faArrowRight, faChevronDown, faChevronUp, faEdit, faEye, faTimes, faPencilAlt, faEyeSlash } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon, FontAwesomeIconProps } from '@fortawesome/react-fontawesome'; +import { Tooltip } from '@material-ui/core'; import { action, observable, runInAction } from 'mobx'; import { observer } from "mobx-react"; -import { Doc, DocListCast, Opt } from '../../../fields/Doc'; +import { Doc, DocListCast } from '../../../fields/Doc'; import { Cast, StrCast } from '../../../fields/Types'; +import { WebField } from '../../../fields/URLField'; +import { emptyFunction, setupMoveUpEvents } from '../../../Utils'; +import { DocumentType } from '../../documents/DocumentTypes'; +import { DocumentManager } from '../../util/DocumentManager'; import { DragManager } from '../../util/DragManager'; +import { Hypothesis } from '../../util/HypothesisUtils'; import { LinkManager } from '../../util/LinkManager'; +import { undoBatch } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; -import './LinkMenuItem.scss'; -import React = require("react"); -import { DocumentManager } from '../../util/DocumentManager'; -import { setupMoveUpEvents, emptyFunction, Utils, simulateMouseClick } from '../../../Utils'; -import { DocumentView } from '../nodes/DocumentView'; import { DocumentLinksButton } from '../nodes/DocumentLinksButton'; +import { DocumentView } from '../nodes/DocumentView'; import { LinkDocPreview } from '../nodes/LinkDocPreview'; -import { Hypothesis } from '../../util/HypothesisUtils'; -import { Id } from '../../../fields/FieldSymbols'; -import { Tooltip } from '@material-ui/core'; -import { DocumentType } from '../../documents/DocumentTypes'; -import { undoBatch } from '../../util/UndoManager'; -import { WebField } from '../../../fields/URLField'; -library.add(faEye, faEdit, faTimes, faArrowRight, faChevronDown, faChevronUp, faPencilAlt, faEyeSlash); +import './LinkMenuItem.scss'; +import React = require("react"); interface LinkMenuItemProps { @@ -37,7 +33,7 @@ interface LinkMenuItemProps { // drag links and drop link targets (aliasing them if needed) export async function StartLinkTargetsDrag(dragEle: HTMLElement, docView: DocumentView, downX: number, downY: number, sourceDoc: Doc, specificLinks?: Doc[]) { - const draggedDocs = (specificLinks ? specificLinks : DocListCast(sourceDoc.links)).map(link => LinkManager.Instance.getOppositeAnchor(link, sourceDoc)).filter(l => l) as Doc[]; + const draggedDocs = (specificLinks ? specificLinks : DocListCast(sourceDoc.links)).map(link => LinkManager.getOppositeAnchor(link, sourceDoc)).filter(l => l) as Doc[]; if (draggedDocs.length) { const moddrag: Doc[] = []; @@ -146,35 +142,34 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { DocumentLinksButton.EditLink = undefined; LinkDocPreview.LinkInfo = undefined; e.preventDefault(); - ContextMenu.Instance.addItem({ description: "Follow Default Link", event: () => this.followDefault(), icon: "arrow-right" }); + ContextMenu.Instance.addItem({ description: "Follow Default Link", event: () => LinkMenuItem.followDefault(this.props.linkDoc, this.props.sourceDoc, this.props.destinationDoc, this.props.addDocTab), icon: "arrow-right" }); ContextMenu.Instance.displayMenu(e.clientX, e.clientY); } - @action.bound - followDefault() { + + @action + public static followDefault(linkDoc: Doc, sourceDoc: Doc, destinationDoc: Doc, addDocTab: (doc: Doc, where: string) => void) { DocumentLinksButton.EditLink = undefined; LinkDocPreview.LinkInfo = undefined; - const linkDoc = this.props.linkDoc; - if (linkDoc.followLinkLocation === "openExternal" && this.props.destinationDoc.type === DocumentType.WEB) { + if (linkDoc.followLinkLocation === "openExternal" && destinationDoc.type === DocumentType.WEB) { window.open(`${StrCast(linkDoc.annotationUri)}#annotations:${StrCast(linkDoc.annotationId)}`, '_blank'); - return; } - if (linkDoc.followLinkLocation && linkDoc.followLinkLocation !== "Default") { - const annotationOn = this.props.destinationDoc.annotationOn as Doc; - this.props.addDocTab(annotationOn instanceof Doc ? annotationOn : this.props.destinationDoc, StrCast(linkDoc.followLinkLocation)); + if (linkDoc.followLinkLocation && linkDoc.followLinkLocation !== "default") { + const annotationOn = destinationDoc.annotationOn as Doc; + addDocTab(annotationOn instanceof Doc ? annotationOn : destinationDoc, StrCast(linkDoc.followLinkLocation)); if (annotationOn) { setTimeout(() => { - const dv = DocumentManager.Instance.getFirstDocumentView(this.props.destinationDoc); - dv?.props.focus(this.props.destinationDoc, false); + const dv = DocumentManager.Instance.getFirstDocumentView(destinationDoc); + dv?.props.focus(destinationDoc, false); }); } } else { - DocumentManager.Instance.FollowLink(this.props.linkDoc, this.props.sourceDoc, doc => this.props.addDocTab(doc, "onRight"), false); + DocumentManager.Instance.FollowLink(linkDoc, sourceDoc, addDocTab, false); } - linkDoc.linksToAnnotation && Hypothesis.scrollToAnnotation(StrCast(this.props.linkDoc.annotationId), this.props.destinationDoc); + linkDoc.linksToAnnotation && Hypothesis.scrollToAnnotation(StrCast(linkDoc.annotationId), destinationDoc); } @undoBatch @@ -254,7 +249,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { <div className="destination-icon-wrapper" > <FontAwesomeIcon className="destination-icon" icon={destinationIcon} size="sm" /></div> <p className="linkMenu-destination-title" - onPointerDown={this.followDefault}> + onPointerDown={() => DocumentView.followLinkClick(this.props.linkDoc, this.props.sourceDoc, this.props.docView.props, false, false)}> {this.props.linkDoc.linksToAnnotation && Cast(this.props.destinationDoc.data, WebField)?.url.href === this.props.linkDoc.annotationUri ? "Annotation in" : ""} {title} </p> </div> diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index 5c8cb5e35..cba65f663 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -26,7 +26,7 @@ import { Scripting } from "../../util/Scripting"; import Waveform from "react-audio-waveform"; import axios from "axios"; import { SnappingManager } from "../../util/SnappingManager"; -const _global = (window /* browser */ || global /* node */) as any; +import { CurrentUserUtils } from "../../util/CurrentUserUtils"; declare class MediaRecorder { // whatever MediaRecorder has @@ -61,6 +61,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD _left: boolean = false; _first: boolean = false; _dragging = false; + _play: any = null; _count: Array<any> = []; _audioRef = React.createRef<HTMLDivElement>(); @@ -109,12 +110,14 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD this._linkPlayDisposer?.(); this._scrubbingDisposer?.(); } + + @action componentDidMount() { if (!this.dataDoc.markerAmount) { this.dataDoc.markerAmount = 0; } - runInAction(() => this.audioState = this.path ? "paused" : undefined); + this.audioState = this.path ? "paused" : undefined; this._linkPlayDisposer = reaction(() => this.layoutDoc.scrollToLinkID, scrollLinkId => { if (scrollLinkId) { @@ -130,7 +133,12 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD this._reactionDisposer = reaction(() => SelectionManager.SelectedDocuments(), selected => { const sel = selected.length ? selected[0].props.Document : undefined; - const link = sel && DocListCast(this.dataDoc.links).forEach(l => (l.anchor1 === sel || l.anchor2 === sel) && this.playLink(sel), false); + let link; + sel && DocListCast(this.dataDoc.links).forEach(l => { + if (l.anchor1 === sel || l.anchor2 === sel && !sel.audioStart) { + link = this.playLink(sel); + } + }); // for links created during recording if (!link) { this.layoutDoc.playOnSelect && this.recordingStart && sel && sel.creationDate && !Doc.AreProtosEqual(sel, this.props.Document) && this.playFromTime(DateCast(sel.creationDate).date.getTime()); @@ -155,10 +163,12 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD if (startTime) { link = true; - this.recordingStart && (endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime)); + this.layoutDoc.playOnSelect && this.recordingStart && (endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime)); } } }); + + this.layoutDoc.playOnSelect && this.recordingStart && Doc.AreProtosEqual(doc, this.props.Document) && this.pause(); return link; } @@ -169,11 +179,11 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD htmlEle.duration && htmlEle.duration !== Infinity && runInAction(() => this.dataDoc.duration = htmlEle.duration); DocListCast(this.dataDoc.links).map(l => { const { la1, linkTime } = this.getLinkData(l); - if (linkTime > NumCast(this.layoutDoc.currentTimecode) && linkTime < htmlEle.currentTime) { + if (linkTime > NumCast(this.layoutDoc._currentTimecode) && linkTime < htmlEle.currentTime) { Doc.linkFollowHighlight(la1); } }); - this.layoutDoc.currentTimecode = htmlEle.currentTime; + this.layoutDoc._currentTimecode = htmlEle.currentTime; } } @@ -191,8 +201,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD // play back the audio from time @action playFrom = (seekTimeInSeconds: number, endTime: number = this.audioDuration) => { - let play; - clearTimeout(play); + clearTimeout(this._play); this._duration = endTime - seekTimeInSeconds; if (Number.isNaN(this._ele?.duration)) { setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500); @@ -208,7 +217,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD this._ele.play(); runInAction(() => this.audioState = "playing"); if (endTime !== this.audioDuration) { - play = setTimeout(() => this.pause(), (this._duration) * 1000); // use setTimeout to play a specific duration + this._play = setTimeout(() => this.pause(), (this._duration) * 1000); // use setTimeout to play a specific duration } } else { this.pause(); @@ -223,7 +232,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD if (this._paused) { this._pausedTime += (new Date().getTime() - this._recordStart) / 1000; } else { - this.layoutDoc.currentTimecode = (new Date().getTime() - this._recordStart - this.pauseTime) / 1000; + this.layoutDoc._currentTimecode = (new Date().getTime() - this._recordStart - this.pauseTime) / 1000; } } } @@ -251,8 +260,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD specificContextMenu = (e: React.MouseEvent): void => { const funcs: ContextMenuProps[] = []; funcs.push({ description: (this.layoutDoc.playOnSelect ? "Don't play" : "Play") + " when link is selected", event: () => this.layoutDoc.playOnSelect = !this.layoutDoc.playOnSelect, icon: "expand-arrows-alt" }); - funcs.push({ description: (this.layoutDoc.hideMarkers ? "Don't hide" : "Hide") + " markers", event: () => this.layoutDoc.hideMarkers = !this.layoutDoc.hideMarkers, icon: "expand-arrows-alt" }); - funcs.push({ description: (this.layoutDoc.hideLabels ? "Don't hide" : "Hide") + " labels", event: () => this.layoutDoc.hideLabels = !this.layoutDoc.hideLabels, icon: "expand-arrows-alt" }); + funcs.push({ description: (this.layoutDoc.hideMarkers ? "Don't hide" : "Hide") + " range markers", event: () => this.layoutDoc.hideMarkers = !this.layoutDoc.hideMarkers, icon: "expand-arrows-alt" }); + funcs.push({ description: (this.layoutDoc.hideLabels ? "Don't hide" : "Hide") + " label markers", event: () => this.layoutDoc.hideLabels = !this.layoutDoc.hideLabels, icon: "expand-arrows-alt" }); funcs.push({ description: (this.layoutDoc.playOnClick ? "Don't play" : "Play") + " markers onClick", event: () => this.layoutDoc.playOnClick = !this.layoutDoc.playOnClick, icon: "expand-arrows-alt" }); ContextMenu.Instance?.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); } @@ -284,11 +293,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD // creates a text document for dictation onFile = (e: any) => { - const newDoc = Docs.Create.TextDocument("", { - title: "", _chromeStatus: "disabled", - x: NumCast(this.props.Document.x), y: NumCast(this.props.Document.y) + NumCast(this.props.Document._height) + 10, - _width: NumCast(this.props.Document._width), _height: 2 * NumCast(this.props.Document._height) - }); + const newDoc = CurrentUserUtils.GetNewTextDoc("", NumCast(this.props.Document.x), NumCast(this.props.Document.y) + NumCast(this.props.Document._height) + 10, + NumCast(this.props.Document._width), 2 * NumCast(this.props.Document._height)); Doc.GetProto(newDoc).recordingSource = this.dataDoc; Doc.GetProto(newDoc).recordingStart = ComputedField.MakeFunction(`self.recordingSource["${this.props.fieldKey}-recordingStart"]`); Doc.GetProto(newDoc).audioState = ComputedField.MakeFunction("self.recordingSource.audioState"); @@ -303,6 +309,11 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD this._ele = e; } + // ref for timeline + timelineRef = (timeline: HTMLDivElement) => { + this._timeline = timeline; + } + // returns the path of the audio file @computed get path() { const field = Cast(this.props.Document[this.props.fieldKey], AudioField); @@ -388,14 +399,19 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD onPointerDown = (e: React.PointerEvent, m: any, left: boolean): void => { this._currMarker = m; this._left = left; - const rect = (e.target as any).getBoundingClientRect(); - const toTimeline = (screen_delta: number) => screen_delta / rect.width * this.audioDuration; + this._timeline?.setPointerCapture(e.pointerId); + const toTimeline = (screen_delta: number, width: number) => screen_delta / width * this.audioDuration; setupMoveUpEvents(this, e, - () => { - this.changeMarker(this._currMarker, toTimeline(e.clientX - rect.x)); + (e: PointerEvent) => { + const rect = (e.target as any).getBoundingClientRect(); + this.changeMarker(this._currMarker, toTimeline(e.clientX - rect.x, rect.width)); return false; }, - () => this._ele!.currentTime = this.layoutDoc.currentTimecode = toTimeline(e.clientX - rect.x), + (e: PointerEvent) => { + const rect = (e.target as any).getBoundingClientRect(); + this._ele!.currentTime = this.layoutDoc._currentTimecode = toTimeline(e.clientX - rect.x, rect.width); + this._timeline?.releasePointerCapture(e.pointerId); + }, emptyFunction); } @@ -463,7 +479,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD color={"darkblue"} height={this._waveHeight} barWidth={0.1} - // pos={this.layoutDoc.currentTimecode} need to correctly resize parent to make this work (not very necessary for function) + // pos={this.layoutDoc._currentTimecode} need to correctly resize parent to make this work (not very necessary for function) pos={this.audioDuration} duration={this.audioDuration} peaks={this._buckets.length === 100 ? this._buckets : undefined} @@ -517,9 +533,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD return <DocumentView {...this.props} Document={mark} focus={() => this.playLink(mark)} - pointerEvents={true} - NativeHeight={returnZero} - NativeWidth={returnZero} + pointerEvents={"all"} rootSelected={returnFalse} LayoutTemplate={undefined} ContainingCollectionDoc={this.props.Document} @@ -544,7 +558,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD <div className="buttons" onClick={this._paused ? this.recordPlay : this.recordPause}> <FontAwesomeIcon style={{ width: "100%" }} icon={this._paused ? "play" : "pause"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /> </div> - <div className="time">{formatTime(Math.round(NumCast(this.layoutDoc.currentTimecode)))}</div> + <div className="time">{formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode)))}</div> </div> : <button className={`audiobox-record${interactive}`} style={{ backgroundColor: "black" }}> @@ -555,14 +569,14 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD <div className="audiobox-dictation"></div> <div className="audiobox-player" > <div className="audiobox-playhead" title={this.audioState === "paused" ? "play" : "pause"} onClick={this.onPlay}> <FontAwesomeIcon style={{ width: "100%", position: "absolute", left: "0px", top: "5px", borderWidth: "thin", borderColor: "white" }} icon={this.audioState === "paused" ? "play" : "pause"} size={"1x"} /></div> - <div className="audiobox-timeline" onClick={e => { e.stopPropagation(); e.preventDefault(); }} + <div className="audiobox-timeline" ref={this.timelineRef} onClick={e => { e.stopPropagation(); e.preventDefault(); }} onPointerDown={e => { if (e.button === 0 && !e.ctrlKey) { const rect = (e.target as any).getBoundingClientRect(); if (e.target !== this._audioRef.current) { const wasPaused = this.audioState === "paused"; - this._ele!.currentTime = this.layoutDoc.currentTimecode = (e.clientX - rect.x) / rect.width * this.audioDuration; + this._ele!.currentTime = this.layoutDoc._currentTimecode = (e.clientX - rect.x) / rect.width * this.audioDuration; wasPaused && this.pause(); } @@ -605,8 +619,6 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD <div className={`audiobox-marker-${this.props.PanelHeight() < 32 ? "mini" : ""}container`} key={l[Id]} style={{ left: `${startTime / this.audioDuration * 100}%` }} onClick={e => e.stopPropagation()}> <DocumentView {...this.props} Document={l} - NativeHeight={returnZero} - NativeWidth={returnZero} rootSelected={returnFalse} ContainingCollectionDoc={this.props.Document} parentActive={returnTrue} @@ -614,7 +626,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD backgroundColor={returnTransparent} ContentScaling={returnOne} forcedBackgroundColor={returnTransparent} - pointerEvents={false} + pointerEvents={"none"} LayoutTemplate={undefined} LayoutTemplateString={LinkAnchorBox.LayoutString(`anchor${Doc.LinkEndpoint(l, la2)}`)} /> @@ -624,11 +636,11 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD })} {this._visible ? this.selectionContainer : null} - <div className="audiobox-current" ref={this._audioRef} onClick={e => { e.stopPropagation(); e.preventDefault(); }} style={{ left: `${NumCast(this.layoutDoc.currentTimecode) / this.audioDuration * 100}%`, pointerEvents: "none" }} /> + <div className="audiobox-current" ref={this._audioRef} onClick={e => { e.stopPropagation(); e.preventDefault(); }} style={{ left: `${NumCast(this.layoutDoc._currentTimecode) / this.audioDuration * 100}%`, pointerEvents: "none" }} /> {this.audio} </div> <div className="current-time"> - {formatTime(Math.round(NumCast(this.layoutDoc.currentTimecode)))} + {formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode)))} </div> <div className="total-time"> {formatTime(Math.round(this.audioDuration))} diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 52f6a66c8..d6f8ce19c 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -11,7 +11,7 @@ import { Document } from "../../../fields/documentSchemas"; import { TraceMobx } from "../../../fields/util"; import { ContentFittingDocumentView } from "./ContentFittingDocumentView"; import { List } from "../../../fields/List"; -import { numberRange, smoothScroll } from "../../../Utils"; +import { numberRange, smoothScroll, returnVal } from "../../../Utils"; import { ComputedField } from "../../../fields/ScriptField"; import { listSpec } from "../../../fields/Schema"; import { DocumentType } from "../../documents/DocumentTypes"; @@ -55,23 +55,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF @computed get freezeDimensions() { return this.props.FreezeDimensions; } @computed get dataProvider() { return this.props.dataProvider?.(this.props.Document, this.props.replica); } @computed get sizeProvider() { return this.props.sizeProvider?.(this.props.Document, this.props.replica); } - @computed get nativeWidth() { return NumCast(this.layoutDoc._nativeWidth, this.props.NativeWidth() || (this.freezeDimensions ? this.layoutDoc[WidthSym]() : 0)); } - @computed get nativeHeight() { return NumCast(this.layoutDoc._nativeHeight, this.props.NativeHeight() || (this.freezeDimensions ? this.layoutDoc[HeightSym]() : 0)); } - - @computed get renderScriptDim() { - if (this.Document.renderScript) { - const someView = Cast(this.props.Document.someView, Doc); - const minimap = Cast(this.props.Document.minimap, Doc); - if (someView instanceof Doc && minimap instanceof Doc) { - const x = (NumCast(someView._panX) - NumCast(someView._width) / 2 / NumCast(someView._viewScale) - (NumCast(minimap.fitX) - NumCast(minimap.fitW) / 2)) / NumCast(minimap.fitW) * NumCast(minimap._width) - NumCast(minimap._width) / 2; - const y = (NumCast(someView._panY) - NumCast(someView._height) / 2 / NumCast(someView._viewScale) - (NumCast(minimap.fitY) - NumCast(minimap.fitH) / 2)) / NumCast(minimap.fitH) * NumCast(minimap._height) - NumCast(minimap._height) / 2; - const w = NumCast(someView._width) / NumCast(someView._viewScale) / NumCast(minimap.fitW) * NumCast(minimap.width); - const h = NumCast(someView._height) / NumCast(someView._viewScale) / NumCast(minimap.fitH) * NumCast(minimap.height); - return { x: x, y: y, width: w, height: h }; - } - } - return undefined; - } + @computed get nativeWidth() { return returnVal(this.props.NativeWidth?.(), NumCast(this.layoutDoc._nativeWidth, this.freezeDimensions ? this.layoutDoc[WidthSym]() : 0)); } + @computed get nativeHeight() { return returnVal(this.props.NativeHeight?.(), NumCast(this.layoutDoc._nativeHeight, this.freezeDimensions ? this.layoutDoc[HeightSym]() : 0)); } public static getValues(doc: Doc, time: number) { const timecode = Math.round(time); @@ -80,7 +65,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF w: Cast(doc["w-indexed"], listSpec("number"), [NumCast(doc._width)]).reduce((p, w, i) => (i <= timecode && w !== undefined) || p === undefined ? w : p, undefined as any as number), x: Cast(doc["x-indexed"], listSpec("number"), [NumCast(doc.x)]).reduce((p, x, i) => (i <= timecode && x !== undefined) || p === undefined ? x : p, undefined as any as number), y: Cast(doc["y-indexed"], listSpec("number"), [NumCast(doc.y)]).reduce((p, y, i) => (i <= timecode && y !== undefined) || p === undefined ? y : p, undefined as any as number), - scroll: Cast(doc["scroll-indexed"], listSpec("number"), [NumCast(doc._scrollTop, 0)]).reduce((p, s, i) => (i <= timecode && s !== undefined) || p === undefined ? s : p, undefined as any as number), + scroll: Cast(doc["scroll-indexed"], listSpec("number"), [NumCast(doc._scrollY, 0)]).reduce((p, s, i) => (i <= timecode && s !== undefined) || p === undefined ? s : p, undefined as any as number), opacity: Cast(doc["opacity-indexed"], listSpec("number"), [NumCast(doc.opacity, 1)]).reduce((p, o, i) => i <= timecode || p === undefined ? o : p, undefined as any as number), }); } @@ -98,7 +83,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF hindexed[timecode] = h as any as number; windexed[timecode] = w as any as number; oindexed[timecode] = opacity as any as number; - scrollIndexed[timecode] = scroll as any as number; + if (scroll) scrollIndexed[timecode] = scroll as any as number; d["x-indexed"] = new List<number>(xindexed); d["y-indexed"] = new List<number>(yindexed); d["h-indexed"] = new List<number>(hindexed); @@ -123,12 +108,12 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF setTimeout(() => doc.dataTransition = "inherit", 1010); } - public static setupScroll(doc: Doc, timecode: number, scrollProgressivize: boolean = false) { + public static setupScroll(doc: Doc, timecode: number) { const scrollList = new List<number>(); - scrollList[timecode] = NumCast(doc._scrollTop); + scrollList[timecode] = NumCast(doc._scrollY); doc["scroll-indexed"] = scrollList; - doc.activeFrame = ComputedField.MakeFunction("self.currentFrame"); - doc._scrollTop = ComputedField.MakeInterpolated("scroll", "activeFrame"); + doc.activeFrame = ComputedField.MakeFunction("self._currentFrame"); + doc._scrollY = ComputedField.MakeInterpolated("scroll", "activeFrame"); } @@ -165,7 +150,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF } - public static setupZoom(doc: Doc, targDoc: Doc, zoomProgressivize: boolean = false) { + public static setupZoom(doc: Doc, targDoc: Doc) { const width = new List<number>(); const height = new List<number>(); const top = new List<number>(); @@ -180,32 +165,25 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF doc["viewfinder-left-indexed"] = left; } - public static setupKeyframes(docs: Doc[], timecode: number, progressivize: boolean = false) { - docs.forEach((doc, i) => { - if (doc.appearFrame === undefined) doc.appearFrame = i; - const curTimecode = progressivize ? i : timecode; - const xlist = new List<number>(numberRange(timecode + 1).map(i => undefined) as any as number[]); - const ylist = new List<number>(numberRange(timecode + 1).map(i => undefined) as any as number[]); - const wlist = new List<number>(numberRange(timecode + 1).map(i => undefined) as any as number[]); - const hlist = new List<number>(numberRange(timecode + 1).map(i => undefined) as any as number[]); - const olist = new List<number>(numberRange(timecode + 1).map(t => progressivize && t < (doc.appearFrame ? doc.appearFrame : i) ? 0 : 1)); - const oarray = olist; - oarray.fill(0, 0, NumCast(doc.appearFrame) - 1); - oarray.fill(1, NumCast(doc.appearFrame), timecode); - // oarray.fill(0, 0, NumCast(doc.appearFrame) - 1); - // oarray.fill(1, NumCast(doc.appearFrame), timecode);\ + public static setupKeyframes(docs: Doc[], currTimecode: number, makeAppear: boolean = false) { + docs.forEach(doc => { + if (doc.appearFrame === undefined) doc.appearFrame = currTimecode; + const curTimecode = currTimecode; + const xlist = new List<number>(numberRange(currTimecode + 1).map(i => undefined) as any as number[]); + const ylist = new List<number>(numberRange(currTimecode + 1).map(i => undefined) as any as number[]); + const wlist = new List<number>(numberRange(currTimecode + 1).map(i => undefined) as any as number[]); + const hlist = new List<number>(numberRange(currTimecode + 1).map(i => undefined) as any as number[]); + const olist = new List<number>(numberRange(currTimecode + 1).map(t => !doc.z && makeAppear && t < NumCast(doc.appearFrame) ? 0 : 1)); wlist[curTimecode] = NumCast(doc._width); hlist[curTimecode] = NumCast(doc._height); xlist[curTimecode] = NumCast(doc.x); ylist[curTimecode] = NumCast(doc.y); - doc.xArray = xlist; - doc.yArray = ylist; doc["x-indexed"] = xlist; doc["y-indexed"] = ylist; doc["w-indexed"] = wlist; doc["h-indexed"] = hlist; - doc["opacity-indexed"] = oarray; - doc.activeFrame = ComputedField.MakeFunction("self.context?.currentFrame||0"); + doc["opacity-indexed"] = olist; + doc.activeFrame = ComputedField.MakeFunction("self.context?._currentFrame||0"); doc._height = ComputedField.MakeInterpolated("h", "activeFrame"); doc._width = ComputedField.MakeInterpolated("w", "activeFrame"); doc.x = ComputedField.MakeInterpolated("x", "activeFrame"); @@ -269,15 +247,16 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF render() { TraceMobx(); const backgroundColor = StrCast(this.layoutDoc._backgroundColor) || StrCast(this.layoutDoc.backgroundColor) || StrCast(this.Document.backgroundColor) || this.props.backgroundColor?.(this.Document, this.props.renderDepth); + const borderRounding = StrCast(Doc.Layout(this.layoutDoc).borderRounding) || StrCast(this.layoutDoc.borderRounding) || StrCast(this.Document.borderRounding) || undefined; return <div className="collectionFreeFormDocumentView-container" style={{ boxShadow: this.Opacity === 0 ? undefined : // if it's not visible, then no shadow this.layoutDoc.z ? `#9c9396 ${StrCast(this.layoutDoc.boxShadow, "10px 10px 0.9vw")}` : // if it's a floating doc, give it a big shadow - this.props.backgroundHalo?.() && this.props.Document.type !== DocumentType.INK ? (`${this.props.backgroundColor?.(this.props.Document, this.props.renderDepth)} ${StrCast(this.layoutDoc.boxShadow, `0vw 0vw ${(this.layoutDoc.isBackground ? 100 : 50) / this.props.ContentScaling()}px`)}`) : // if it's just in a cluster, make the shadown roughly match the cluster border extent - this.layoutDoc.isBackground ? undefined : // if it's a background & has a cluster color, make the shadow spread really big + this.props.backgroundHalo?.() && this.props.Document.type !== DocumentType.INK ? (`${this.props.backgroundColor?.(this.props.Document, this.props.renderDepth)} ${StrCast(this.layoutDoc.boxShadow, `0vw 0vw ${(this.layoutDoc._isBackground ? 100 : 50) / this.props.ContentScaling()}px`)}`) : // if it's just in a cluster, make the shadown roughly match the cluster border extent + this.layoutDoc._isBackground ? undefined : // if it's a background & has a cluster color, make the shadow spread really big StrCast(this.layoutDoc.boxShadow, ""), - borderRadius: StrCast(Doc.Layout(this.layoutDoc).borderRounding), + borderRadius: borderRounding, outline: this.Highlight ? "orange solid 2px" : "", transform: this.transform, transition: this.props.dataTransition ? this.props.dataTransition : this.dataProvider ? this.dataProvider.transition : StrCast(this.layoutDoc.dataTransition), @@ -286,7 +265,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF zIndex: this.ZInd, mixBlendMode: StrCast(this.layoutDoc.mixBlendMode) as any, display: this.ZInd === -99 ? "none" : undefined, - pointerEvents: this.props.Document.isBackground || this.Opacity === 0 || this.props.Document.type === DocumentType.INK || this.props.Document.isInkMask ? "none" : this.props.pointerEvents ? "all" : undefined + // @ts-ignore + pointerEvents: this.props.Document._isBackground || this.Opacity === 0 || this.props.Document.type === DocumentType.INK || this.props.Document.isInkMask ? "none" : this.props.pointerEvents }} > {Doc.UserDoc().renderStyle !== "comic" ? (null) : diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx index 090cf015a..fcc9e50f5 100644 --- a/src/client/views/nodes/ColorBox.tsx +++ b/src/client/views/nodes/ColorBox.tsx @@ -15,6 +15,7 @@ import { ActiveInkPen, ActiveInkWidth, ActiveInkBezierApprox, SetActiveInkColor, import "./ColorBox.scss"; import { FieldView, FieldViewProps } from './FieldView'; import { DocumentType } from "../../documents/DocumentTypes"; +import { RichTextMenu } from "./formattedText/RichTextMenu"; type ColorDocument = makeInterface<[typeof documentSchema]>; const ColorDocument = makeInterface(documentSchema); @@ -39,8 +40,8 @@ export class ColorBox extends ViewBoxBaseComponent<FieldViewProps, ColorDocument if (view.props.LayoutTemplate?.() || view.props.LayoutTemplateString) { // this situation typically occurs when you have a link dot targetDoc.backgroundColor = Doc.UserDoc().backgroundColor; // bcz: don't know how to change the color of an inline template... } - else if (StrCast(Doc.Layout(view.props.Document).layout).includes("FormattedTextBox") && window.getSelection()?.toString() !== "") { - Doc.Layout(view.props.Document)[Doc.LayoutFieldKey(view.props.Document) + "-color"] = Doc.UserDoc().backgroundColor; + else if (RichTextMenu.Instance?.TextViewFieldKey && window.getSelection()?.toString() !== "") { + Doc.Layout(view.props.Document)[RichTextMenu.Instance.TextViewFieldKey + "-color"] = Doc.UserDoc().backgroundColor; } else { Doc.Layout(view.props.Document)._backgroundColor = Doc.UserDoc().backgroundColor; // '_backgroundColor' is template specific. 'backgroundColor' would apply to all templates, but has no UI at the moment } @@ -55,7 +56,7 @@ export class ColorBox extends ViewBoxBaseComponent<FieldViewProps, ColorDocument render() { const selDoc = SelectionManager.SelectedDocuments()?.[0]?.rootDoc; return <div className={`colorBox-container${this.active() ? "-interactive" : ""}`} - onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()} + onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()} onClick={e => { (e.nativeEvent as any).stuff = true; e.stopPropagation(); }} style={{ transform: `scale(${this.props.ContentScaling()})`, width: `${100 / this.props.ContentScaling()}%`, height: `${100 / this.props.ContentScaling()}%` }} > <SketchPicker onChange={ColorBox.switchColor} presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']} diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index 616cddfcf..2af5b3182 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -74,7 +74,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps, C render() { const clipWidth = NumCast(this.layoutDoc._clipWidth) + "%"; - const childProps: DocumentViewProps = { ...this.props, pointerEvents: false, parentActive: this.props.active }; + const childProps: DocumentViewProps = { ...this.props, pointerEvents: "none", parentActive: this.props.active }; const clearButton = (which: string) => { return <div className={`clear-button ${which}`} onPointerDown={e => e.stopPropagation()} // prevent triggering slider movement in registerSliding diff --git a/src/client/views/nodes/ContentFittingDocumentView.scss b/src/client/views/nodes/ContentFittingDocumentView.scss index eb2d93b9a..679073d44 100644 --- a/src/client/views/nodes/ContentFittingDocumentView.scss +++ b/src/client/views/nodes/ContentFittingDocumentView.scss @@ -3,7 +3,8 @@ .contentFittingDocumentView { position: relative; display: flex; - align-items: center; + width: 100%; + height: 100%; .contentFittingDocumentView-previewDoc { position: relative; diff --git a/src/client/views/nodes/ContentFittingDocumentView.tsx b/src/client/views/nodes/ContentFittingDocumentView.tsx index 6081def5d..09051da78 100644 --- a/src/client/views/nodes/ContentFittingDocumentView.tsx +++ b/src/client/views/nodes/ContentFittingDocumentView.tsx @@ -1,51 +1,19 @@ import React = require("react"); import { computed } from "mobx"; import { observer } from "mobx-react"; -import { Transform } from "nodemailer/lib/xoauth2"; -import { Doc, HeightSym, Opt, WidthSym } from "../../../fields/Doc"; -import { ScriptField } from "../../../fields/ScriptField"; +import { Doc, HeightSym, WidthSym } from "../../../fields/Doc"; import { Cast, NumCast, StrCast } from "../../../fields/Types"; import { TraceMobx } from "../../../fields/util"; -import { emptyFunction } from "../../../Utils"; -import { dropActionType } from "../../util/DragManager"; -import { CollectionView } from "../collections/CollectionView"; +import { emptyFunction, OmitKeys, returnVal } from "../../../Utils"; import { DocumentView, DocumentViewProps } from "../nodes/DocumentView"; import "./ContentFittingDocumentView.scss"; interface ContentFittingDocumentViewProps { - Document: Doc; - DataDocument?: Doc; - LayoutDoc?: () => Opt<Doc>; - NativeWidth?: () => number; - NativeHeight?: () => number; - FreezeDimensions?: boolean; - LibraryPath: Doc[]; - renderDepth: number; - fitToBox?: boolean; - layoutKey?: string; - dropAction?: dropActionType; - PanelWidth: () => number; - PanelHeight: () => number; - focus?: (doc: Doc) => void; - CollectionView?: CollectionView; - CollectionDoc?: Doc; - onClick?: ScriptField; - backgroundColor?: (doc: Doc) => string | undefined; - getTransform: () => Transform; - addDocument?: (document: Doc) => boolean; - moveDocument?: (document: Doc, target: Doc | undefined, addDoc: ((doc: Doc) => boolean)) => boolean; - removeDocument?: (document: Doc) => boolean; - active: (outsideReaction: boolean) => boolean; - whenActiveChanged: (isActive: boolean) => void; - addDocTab: (document: Doc, where: string) => boolean; - pinToPres: (document: Doc) => void; - dontRegisterView?: boolean; - rootSelected: (outsideReaction?: boolean) => boolean; - Display?: string; + dontCenter?: boolean; } @observer -export class ContentFittingDocumentView extends React.Component<DocumentViewProps>{ +export class ContentFittingDocumentView extends React.Component<DocumentViewProps & ContentFittingDocumentViewProps> { public get displayName() { return "DocumentView(" + this.props.Document?.title + ")"; } // this makes mobx trace() statements more descriptive private get layoutDoc() { return this.props.LayoutTemplate?.() || @@ -53,12 +21,18 @@ export class ContentFittingDocumentView extends React.Component<DocumentViewProp Doc.Layout(this.props.Document); } @computed get freezeDimensions() { return this.props.FreezeDimensions; } - nativeWidth = () => NumCast(this.layoutDoc?._nativeWidth, this.props.NativeWidth?.() || (this.freezeDimensions && this.layoutDoc ? this.layoutDoc[WidthSym]() : this.props.PanelWidth())); - nativeHeight = () => NumCast(this.layoutDoc?._nativeHeight, this.props.NativeHeight?.() || (this.freezeDimensions && this.layoutDoc ? this.layoutDoc[HeightSym]() : this.props.PanelHeight())); + + nativeWidth = () => returnVal(this.props.NativeWidth?.(), + NumCast(this.layoutDoc?._nativeWidth || this.props.DataDoc?.[Doc.LayoutFieldKey(this.layoutDoc) + "-nativeWidth"], + (this.freezeDimensions && this.layoutDoc ? this.layoutDoc[WidthSym]() : this.props.PanelWidth()))) + nativeHeight = () => returnVal(this.props.NativeHeight?.(), + NumCast(this.layoutDoc?._nativeHeight || this.props.DataDoc?.[Doc.LayoutFieldKey(this.layoutDoc) + "-nativeHeight"], + (this.freezeDimensions && this.layoutDoc ? this.layoutDoc[HeightSym]() : this.props.PanelHeight()))) @computed get scaling() { const wscale = this.props.PanelWidth() / this.nativeWidth(); + const hscale = this.props.PanelHeight() / this.nativeHeight(); if (wscale * this.nativeHeight() > this.props.PanelHeight()) { - return (this.props.PanelHeight() / this.nativeHeight()) || 1; + return hscale || 1; } return wscale || 1; } @@ -67,38 +41,37 @@ export class ContentFittingDocumentView extends React.Component<DocumentViewProp private PanelWidth = () => this.panelWidth; private PanelHeight = () => this.panelHeight; - @computed get panelWidth() { return this.nativeWidth && !this.props.Document._fitWidth ? this.nativeWidth() * this.contentScaling() : this.props.PanelWidth(); } - @computed get panelHeight() { return this.nativeHeight && !this.props.Document._fitWidth ? this.nativeHeight() * this.contentScaling() : this.props.PanelHeight(); } + @computed get panelWidth() { return this.nativeWidth() && !this.props.Document._fitWidth ? this.nativeWidth() * this.contentScaling() : this.props.PanelWidth(); } + @computed get panelHeight() { return this.nativeHeight() && !this.props.Document._fitWidth ? this.nativeHeight() * this.contentScaling() : this.props.PanelHeight(); } - private getTransform = () => this.props.ScreenToLocalTransform().translate(-this.centeringOffset, -this.centeringYOffset).scale(1 / this.contentScaling()); - private get centeringOffset() { return this.nativeWidth() && !this.props.Document._fitWidth && this.props.display !== "contents" ? (this.props.PanelWidth() - this.nativeWidth() * this.contentScaling()) / 2 : 0; } - private get centeringYOffset() { return Math.abs(this.centeringOffset) < 0.001 && this.props.display !== "contents" ? (this.props.PanelHeight() - this.nativeHeight() * this.contentScaling()) / 2 : 0; } + @computed get childXf() { return this.props.DataDoc ? 1 : 1 / this.contentScaling(); } // this is intended to detect when a document is being rendered inside itself as part of a template, but not as a leaf node where nativeWidth & height would apply. + private getTransform = () => this.props.dontCenter ? + this.props.ScreenToLocalTransform().scale(this.childXf) : + this.props.ScreenToLocalTransform().translate(-this.centeringOffset, -this.centeringYOffset).scale(this.childXf) + private get centeringOffset() { return this.nativeWidth() && !this.props.Document._fitWidth ? (this.props.PanelWidth() - this.nativeWidth() * this.contentScaling()) / 2 : 0; } + private get centeringYOffset() { return Math.abs(this.centeringOffset) < 0.001 ? (this.props.PanelHeight() - this.nativeHeight() * this.contentScaling()) / 2 : 0; } @computed get borderRounding() { return StrCast(this.props.Document?.borderRounding); } render() { TraceMobx(); - return (<div className="contentFittingDocumentView" style={{ - width: Math.abs(this.centeringYOffset) > 0.001 ? "auto" : this.props.PanelWidth(), - height: Math.abs(this.centeringOffset) > 0.0001 ? "auto" : this.props.PanelHeight(), - display: this.props.display /* just added for grid */ - }}> + return (<div className="contentFittingDocumentView"> {!this.props.Document || !this.props.PanelWidth ? (null) : ( <div className="contentFittingDocumentView-previewDoc" style={{ - transform: `translate(${this.centeringOffset}px, 0px)`, + transform: !this.props.dontCenter ? `translate(${this.centeringOffset}px, ${this.centeringYOffset}px)` : undefined, borderRadius: this.borderRounding, height: Math.abs(this.centeringYOffset) > 0.001 ? `${100 * this.nativeHeight() / this.nativeWidth() * this.props.PanelWidth() / this.props.PanelHeight()}%` : this.props.PanelHeight(), width: Math.abs(this.centeringOffset) > 0.001 ? `${100 * (this.props.PanelWidth() - this.centeringOffset * 2) / this.props.PanelWidth()}%` : this.props.PanelWidth() }}> - <DocumentView {...this.props} + <DocumentView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} Document={this.props.Document} DataDoc={this.props.DataDoc} LayoutTemplate={this.props.LayoutTemplate} LayoutTemplateString={this.props.LayoutTemplateString} LibraryPath={this.props.LibraryPath} - NativeWidth={this.nativeWidth} - NativeHeight={this.nativeHeight} + // NativeWidth={this.nativeWidth} + // NativeHeight={this.nativeHeight} PanelWidth={this.PanelWidth} PanelHeight={this.PanelHeight} ContentScaling={this.contentScaling} diff --git a/src/client/views/nodes/DocHolderBox.tsx b/src/client/views/nodes/DocHolderBox.tsx index 88eb48f51..dd254ae93 100644 --- a/src/client/views/nodes/DocHolderBox.tsx +++ b/src/client/views/nodes/DocHolderBox.tsx @@ -120,6 +120,8 @@ export class DocHolderBox extends ViewBoxAnnotatableComponent<FieldViewProps, Do DataDoc={undefined} LibraryPath={emptyPath} docFilters={this.props.docFilters} + docRangeFilters={this.props.docRangeFilters} + searchFilterDocs={this.props.searchFilterDocs} ContainingCollectionView={this as any} // bcz: hack! need to pass a prop that can be used to select the container (ie, 'this') when the up selector in document decorations is clicked. currently, the up selector allows only a containing collection to be selected ContainingCollectionDoc={undefined} fitToBox={true} @@ -134,8 +136,6 @@ export class DocHolderBox extends ViewBoxAnnotatableComponent<FieldViewProps, Do pinToPres={this.props.pinToPres} ScreenToLocalTransform={this.getTransform} renderDepth={containedDoc.type !== DocumentType.DOCHOLDER && !this.props.renderDepth ? 0 : this.props.renderDepth + 1} - NativeHeight={returnZero} - NativeWidth={returnZero} PanelWidth={this.pwidth} PanelHeight={this.pheight} focus={this.props.focus} @@ -149,6 +149,8 @@ export class DocHolderBox extends ViewBoxAnnotatableComponent<FieldViewProps, Do DataDoc={undefined} LibraryPath={emptyPath} docFilters={this.props.docFilters} + docRangeFilters={this.props.docRangeFilters} + searchFilterDocs={this.props.searchFilterDocs} ContainingCollectionView={this as any} // bcz: hack! need to pass a prop that can be used to select the container (ie, 'this') when the up selector in document decorations is clicked. currently, the up selector allows only a containing collection to be selected ContainingCollectionDoc={undefined} fitToBox={true} @@ -164,8 +166,6 @@ export class DocHolderBox extends ViewBoxAnnotatableComponent<FieldViewProps, Do pinToPres={this.props.pinToPres} ScreenToLocalTransform={this.getTransform} renderDepth={containedDoc.type !== DocumentType.DOCHOLDER && !this.props.renderDepth ? 0 : this.props.renderDepth + 1} - NativeHeight={returnZero} - NativeWidth={returnZero} PanelWidth={this.pwidth} PanelHeight={this.pheight} focus={this.props.focus} diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 2408b3906..e8c3662aa 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -18,14 +18,14 @@ import { DocHolderBox } from "./DocHolderBox"; import { DocumentViewProps } from "./DocumentView"; import "./DocumentView.scss"; import { FontIconBox } from "./FontIconBox"; -import { MenuIconBox } from "./MenuIconBox"; import { FieldView, FieldViewProps } from "./FieldView"; -import { FormattedTextBox } from "./formattedText/FormattedTextBox"; +import { FormattedTextBox, FormattedTextBoxProps } from "./formattedText/FormattedTextBox"; import { ImageBox } from "./ImageBox"; import { KeyValueBox } from "./KeyValueBox"; import { PDFBox } from "./PDFBox"; import { PresBox } from "./PresBox"; import { SearchBox } from "../search/SearchBox"; +import { FilterBox } from "./FilterBox"; import { ColorBox } from "./ColorBox"; import { DashWebRTCVideo } from "../webcam/DashWebRTCVideo"; import { LinkAnchorBox } from "./LinkAnchorBox"; @@ -64,7 +64,19 @@ interface HTMLtagProps { onClick?: ScriptField; onInput?: ScriptField; } -//"<HTMLdiv borderRadius='100px' onClick={this.bannerColor=this.bannerColor==='red'?'green':'red'} width='100%' height='100%' transform='rotate({2*this.x+this.y}deg)'><ImageBox {...props} fieldKey={'data'}/><HTMLspan width='100%' marginTop='50%' height='10%' position='absolute' backgroundColor='{this.bannerColor===`green`?`dark`:`light`}grey'>{this.title}</HTMLspan></HTMLdiv>"@observer + +//"<HTMLdiv borderRadius='100px' onClick={this.bannerColor=this.bannerColor==='red'?'green':'red'} overflow='hidden' position='absolute' width='100%' height='100%' transform='rotate({2*this.x+this.y}deg)'> <ImageBox {...props} fieldKey={'data'}/> <HTMLspan width='200px' top='0' height='35px' textAlign='center' paddingTop='10px' transform='translate(-40px, 45px) rotate(-45deg)' position='absolute' color='{this.bannerColor===`green`?`light`:`dark`}blue' backgroundColor='{this.bannerColor===`green`?`dark`:`light`}blue'> {this.title}</HTMLspan></HTMLdiv>" +//"<HTMLdiv borderRadius='100px' overflow='hidden' position='absolute' width='100%' height='100%' +// transform='rotate({2*this.x+this.y}deg)' +// onClick = { this.bannerColor = this.bannerColor === 'red' ? 'green' : 'red' } > +// <ImageBox {...props} fieldKey={'data'}/> +// <HTMLspan width='200px' top='0' height='35px' textAlign='center' paddingTop='10px' +// transform='translate(-40px, 45px) rotate(-45deg)' position='absolute' +// color='{this.bannerColor===`green`?`light`:`dark`}blue' +// backgroundColor='{this.bannerColor===`green`?`dark`:`light`}blue'> +// {this.title} +// </HTMLspan> +// </HTMLdiv>" @observer export class HTMLtag extends React.Component<HTMLtagProps> { click = (e: React.MouseEvent) => { @@ -93,7 +105,7 @@ export class HTMLtag extends React.Component<HTMLtagProps> { } @observer -export class DocumentContentsView extends React.Component<DocumentViewProps & { +export class DocumentContentsView extends React.Component<DocumentViewProps & FormattedTextBoxProps & { isSelected: (outsideReaction: boolean) => boolean, select: (ctrl: boolean) => void, layoutKey: string, @@ -102,22 +114,13 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { }> { @computed get layout(): string { TraceMobx(); - if (!this.layoutDoc) return "<p>awaiting layout</p>"; - // const layout = Cast(this.layoutDoc[StrCast(this.layoutDoc.layoutKey, this.layoutDoc === this.props.Document ? this.props.layoutKey : "layout")], "string"); // bcz: replaced this with below... is it right? if (this.props.LayoutTemplateString) return this.props.LayoutTemplateString; + if (!this.layoutDoc) return "<p>awaiting layout</p>"; + if (this.props.layoutKey === "layout_keyValue") return StrCast(this.props.Document.layout_keyValue, KeyValueBox.LayoutString("data")); const layout = Cast(this.layoutDoc[this.layoutDoc === this.props.Document && this.props.layoutKey ? this.props.layoutKey : StrCast(this.layoutDoc.layoutKey, "layout")], "string"); - if (this.props.layoutKey === "layout_keyValue") { - return StrCast(this.props.Document.layout_keyValue, KeyValueBox.LayoutString("data")); - } else - if (layout === undefined) { - return this.props.Document.data ? - "<FieldView {...props} fieldKey='data' />" : - KeyValueBox.LayoutString(this.layoutDoc.proto ? "proto" : ""); - } else if (typeof layout === "string") { - return layout; - } else { - return "<p>Loading layout</p>"; - } + if (layout === undefined) return this.props.Document.data ? "<FieldView {...props} fieldKey='data' />" : KeyValueBox.LayoutString(this.layoutDoc.proto ? "proto" : ""); + if (typeof layout === "string") return layout; + return "<p>Loading layout</p>"; } get dataDoc() { @@ -136,7 +139,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { CreateBindings(onClick: Opt<ScriptField>, onInput: Opt<ScriptField>): JsxBindings { const list = { - ...OmitKeys(this.props, ['parentActive'], "", (obj: any) => obj.active = this.props.parentActive).omit, + ...OmitKeys(this.props, ['parentActive', 'NativeWidth', 'NativeHeight'], "", (obj: any) => obj.active = this.props.parentActive).omit, RootDoc: Cast(this.layoutDoc?.rootDocument, Doc, null) || this.layoutDoc, Document: this.layoutDoc, DataDoc: this.dataDoc, @@ -170,10 +173,10 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { // add onClick function to props const makeFuncProp = (func: string) => { - const splits = layoutFrame.split(`func=`); + const splits = layoutFrame.split(`${func}=`); if (splits.length > 1) { const code = XRegExp.matchRecursive(splits[1], "{", "}", "", { valueNames: ["between", "left", "match", "right", "between"] }); - layoutFrame = splits[0] + ` ${func}={props.onClick} ` + splits[1].substring(code[1].end + 1); + layoutFrame = splits[0] + ` ${func}={props.${func}} ` + splits[1].substring(code[1].end + 1); return ScriptField.MakeScript(code[1].value, { this: Doc.name, self: Doc.name, value: "string" }); } return undefined; @@ -190,9 +193,9 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { blacklistedAttrs={[]} renderInWrapper={false} components={{ - FormattedTextBox, ImageBox, DirectoryImportBox, FontIconBox, MenuIconBox, LabelBox, SliderBox, FieldView, + FormattedTextBox, ImageBox, DirectoryImportBox, FontIconBox, LabelBox, SliderBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox, - PDFBox, VideoBox, AudioBox, PresBox, YoutubeBox, PresElementBox, SearchBox, + PDFBox, VideoBox, AudioBox, PresBox, YoutubeBox, PresElementBox, SearchBox, FilterBox, ColorBox, DashWebRTCVideo, LinkAnchorBox, InkingStroke, DocHolderBox, LinkBox, ScriptingBox, ScreenshotBox, HTMLtag, ComparisonBox }} diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index cf8645e4c..ddfb3cc34 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -25,7 +25,7 @@ export const Flyout = higflyout.default; interface DocumentLinksButtonProps { View: DocumentView; - Offset?: number[]; + Offset?: (number | undefined)[]; AlwaysOn?: boolean; InMenu?: boolean; StartLink?: boolean; @@ -36,6 +36,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp private _linkButton = React.createRef<HTMLDivElement>(); @observable public static StartLink: Doc | undefined; + @observable public static StartLinkView: DocumentView | undefined; @observable public static AnnotationId: string | undefined; @observable public static AnnotationUri: string | undefined; @@ -79,8 +80,10 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp //action(() => Doc.BrushDoc(this.props.View.Document)); if (DocumentLinksButton.StartLink === this.props.View.props.Document) { DocumentLinksButton.StartLink = undefined; + DocumentLinksButton.StartLinkView = undefined; } else { DocumentLinksButton.StartLink = this.props.View.props.Document; + DocumentLinksButton.StartLinkView = this.props.View; } } else if (!this.props.InMenu) { DocumentLinksButton.EditLink = this.props.View; @@ -95,8 +98,10 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp DocumentLinksButton.AnnotationUri = undefined; if (DocumentLinksButton.StartLink === this.props.View.props.Document) { DocumentLinksButton.StartLink = undefined; + DocumentLinksButton.StartLinkView = undefined; } else { DocumentLinksButton.StartLink = this.props.View.props.Document; + DocumentLinksButton.StartLinkView = this.props.View; } //action(() => Doc.BrushDoc(this.props.View.Document)); @@ -110,6 +115,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp if (doubleTap && !this.props.StartLink) { if (DocumentLinksButton.StartLink === this.props.View.props.Document) { DocumentLinksButton.StartLink = undefined; + DocumentLinksButton.StartLinkView = undefined; DocumentLinksButton.AnnotationId = undefined; } else if (DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View.props.Document) { const sourceDoc = DocumentLinksButton.StartLink; @@ -129,9 +135,15 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp LinkDescriptionPopup.popupY = e.screenY - 100; LinkDescriptionPopup.descriptionPopup = true; - LinkDescriptionPopup.popupX = e.screenX; - LinkDescriptionPopup.popupY = e.screenY - 100; - LinkDescriptionPopup.descriptionPopup = true; + const rect = document.body.getBoundingClientRect(); + if (LinkDescriptionPopup.popupX + 200 > rect.width) { + LinkDescriptionPopup.popupX -= 190; + TaskCompletionBox.popupX -= 40; + } + if (LinkDescriptionPopup.popupY + 100 > rect.height) { + LinkDescriptionPopup.popupY -= 40; + TaskCompletionBox.popupY -= 40; + } setTimeout(action(() => TaskCompletionBox.taskCompleted = false), 2500); } @@ -144,15 +156,20 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp public static finishLinkClick = undoBatch(action((screenX: number, screenY: number, startLink: Doc, endLink: Doc, startIsAnnotation: boolean, endLinkView?: DocumentView,) => { if (startLink === endLink) { DocumentLinksButton.StartLink = undefined; + DocumentLinksButton.StartLinkView = undefined; DocumentLinksButton.AnnotationId = undefined; DocumentLinksButton.AnnotationUri = undefined; //!this.props.StartLink } else if (startLink !== endLink) { - const linkDoc = DocUtils.MakeLink({ doc: startLink }, { doc: endLink }, DocumentLinksButton.AnnotationId ? "hypothes.is annotation" : "long drag"); + const linkDoc = DocUtils.MakeLink({ doc: startLink }, { doc: endLink }, DocumentLinksButton.AnnotationId ? "hypothes.is annotation" : "long drag", undefined, undefined, true); // this notifies any of the subviews that a document is made so that they can make finer-grained hyperlinks (). see note above in onLInkButtonMoved if (endLinkView) { - startLink._link = endLinkView._link = linkDoc; - setTimeout(action(() => startLink._link = endLinkView._link = undefined), 0); + endLinkView._link = linkDoc; + DocumentLinksButton.StartLinkView && (DocumentLinksButton.StartLinkView._link = linkDoc); + setTimeout(action(() => { + DocumentLinksButton.StartLinkView && (DocumentLinksButton.StartLinkView._link = undefined); + endLinkView._link = undefined; + }), 0); } LinkManager.currentLink = linkDoc; @@ -176,6 +193,17 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp LinkDescriptionPopup.popupY = screenY - 100; LinkDescriptionPopup.descriptionPopup = true; } + + const rect = document.body.getBoundingClientRect(); + if (LinkDescriptionPopup.popupX + 200 > rect.width) { + LinkDescriptionPopup.popupX -= 190; + TaskCompletionBox.popupX -= 40; + } + if (LinkDescriptionPopup.popupY + 100 > rect.height) { + LinkDescriptionPopup.popupY -= 40; + TaskCompletionBox.popupY -= 40; + } + setTimeout(action(() => { TaskCompletionBox.taskCompleted = false; }), 2500); } } @@ -186,12 +214,13 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp @action clearLinks() { DocumentLinksButton.StartLink = undefined; + DocumentLinksButton.StartLinkView = undefined; } @computed get linkButton() { TraceMobx(); - const links = this.props.links; + const links = DocUtils.FilterDocs(Array.from(new Set<Doc>(this.props.links)), this.props.View.props.docFilters(), []); const menuTitle = this.props.StartLink ? "Drag or tap to start link" : "Tap to complete link"; const buttonTitle = "Tap to view links"; @@ -213,7 +242,10 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp id={"link-icon"} src={`/assets/${"link.png"}`} />; - const linkButton = <div ref={this._linkButton} style={{ minWidth: 20, minHeight: 20, position: "absolute", left: this.props.Offset?.[0] }}> + const linkButton = <div className="documentLinksButton-cont" ref={this._linkButton} style={{ + minWidth: 20, minHeight: 20, position: "absolute", + left: this.props.Offset?.[0], top: this.props.Offset?.[1], right: this.props.Offset?.[2], bottom: this.props.Offset?.[3] + }}> <div className={"documentLinksButton"} style={{ backgroundColor: this.props.InMenu ? "" : "#add8e6", color: this.props.InMenu ? "white" : "black", @@ -233,7 +265,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp <FontAwesomeIcon className="documentdecorations-icon" icon="hand-paper" size="sm" /> : links.length} */} {this.props.InMenu ? this.props.StartLink ? <FontAwesomeIcon className="documentdecorations-icon" icon="link" size="sm" /> : - link : links.length} + link : Array.from(links).length} </div> {this.props.InMenu && !this.props.StartLink && DocumentLinksButton.StartLink !== this.props.View.props.Document ? @@ -242,10 +274,11 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp width: this.props.InMenu ? "20px" : "30px", height: this.props.InMenu ? "20px" : "30px", backgroundColor: DocumentLinksButton.StartLink ? "" : "grey", opacity: DocumentLinksButton.StartLink ? "" : "50%", - border: DocumentLinksButton.StartLink ? "" : "none" + border: DocumentLinksButton.StartLink ? "" : "none", + cursor: DocumentLinksButton.StartLink ? "pointer" : "default" }} onPointerDown={DocumentLinksButton.StartLink ? this.completeLink : emptyFunction} - onClick={e => DocumentLinksButton.StartLink ? DocumentLinksButton.finishLinkClick(e.screenX, e.screenY, DocumentLinksButton.StartLink, this.props.View.props.Document, true, this.props.View) : emptyFunction} /> : (null) + onClick={e => DocumentLinksButton.StartLink ? DocumentLinksButton.finishLinkClick(e.clientX, e.clientY, DocumentLinksButton.StartLink, this.props.View.props.Document, true, this.props.View) : emptyFunction} /> : (null) } { DocumentLinksButton.StartLink === this.props.View.props.Document && this.props.InMenu && this.props.StartLink ? <div className={"documentLinksButton-startLink"} @@ -255,7 +288,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp } </div >; - return (!links.length) && !this.props.AlwaysOn ? (null) : + return (!Array.from(links).length) && !this.props.AlwaysOn ? (null) : this.props.InMenu && (DocumentLinksButton.StartLink || this.props.StartLink) ? <Tooltip title={<><div className="dash-tooltip">{title}</div></>}> {linkButton} diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index e6b8928d4..c31172e22 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -61,6 +61,7 @@ width: 100%; height: 100%; display: inline-block; + pointer-events: none; } .documentView-lock { @@ -109,7 +110,7 @@ transform-origin: top left; top: 0; width: 100%; - height: 25; + height: 14; background: rgba(0, 0, 0, .4); text-align: center; text-overflow: ellipsis; @@ -133,6 +134,8 @@ bottom: 0; width: 100%; transform-origin: bottom left; + opacity: 0.1; + transition: opacity 0.5s; } } @@ -144,4 +147,9 @@ display:inline-block; } } + > .documentView-styleWrapper { + > .documentView-captionWrapper { + opacity: 1; + } + } }
\ No newline at end of file diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index b5d210b4d..5f99f27b1 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,27 +1,29 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { AclAdmin, AclEdit, AclPrivate, DataSym, Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../fields/Doc"; +import { AclAdmin, AclEdit, AclPrivate, DataSym, Doc, DocListCast, Field, HeightSym, Opt, WidthSym } from "../../../fields/Doc"; import { Document } from '../../../fields/documentSchemas'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; +import { RichTextField } from '../../../fields/RichTextField'; import { listSpec } from "../../../fields/Schema"; import { ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from "../../../fields/Types"; import { GetEffectiveAcl, TraceMobx } from '../../../fields/util'; import { MobileInterface } from '../../../mobile/MobileInterface'; import { GestureUtils } from '../../../pen-gestures/GestureUtils'; -import { emptyFunction, OmitKeys, returnOne, returnTransparent, Utils } from "../../../Utils"; +import { emptyFunction, OmitKeys, returnOne, returnTransparent, returnVal, Utils } from "../../../Utils"; import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; import { Docs, DocUtils } from "../../documents/Documents"; import { DocumentType } from '../../documents/DocumentTypes'; +import { CurrentUserUtils } from '../../util/CurrentUserUtils'; import { DocumentManager } from "../../util/DocumentManager"; import { DragManager, dropActionType } from "../../util/DragManager"; import { InteractionUtils } from '../../util/InteractionUtils'; import { LinkManager } from '../../util/LinkManager'; import { Scripting } from '../../util/Scripting'; import { SelectionManager } from "../../util/SelectionManager"; -import SharingManager from '../../util/SharingManager'; +import { SharingManager } from '../../util/SharingManager'; import { SnappingManager } from '../../util/SnappingManager'; import { Transform } from "../../util/Transform"; import { undoBatch, UndoManager } from "../../util/UndoManager"; @@ -30,6 +32,7 @@ import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from '../ContextMenuItem'; import { DocComponent } from "../DocComponent"; import { EditableView } from '../EditableView'; +import { InkStrokeProperties } from '../InkStrokeProperties'; import { DocumentContentsView } from "./DocumentContentsView"; import { DocumentLinksButton } from './DocumentLinksButton'; import "./DocumentView.scss"; @@ -45,11 +48,14 @@ export interface DocumentViewProps { ContainingCollectionView: Opt<CollectionView>; ContainingCollectionDoc: Opt<Doc>; docFilters: () => string[]; + docRangeFilters: () => string[]; + searchFilterDocs: () => Doc[]; FreezeDimensions?: boolean; - NativeWidth: () => number; - NativeHeight: () => number; + NativeWidth?: () => number; + NativeHeight?: () => number; Document: Doc; DataDoc?: Doc; + getView?: (view: DocumentView) => any; LayoutTemplateString?: string; LayoutTemplate?: () => Opt<Doc>; LibraryPath: Doc[]; @@ -69,12 +75,13 @@ export interface DocumentViewProps { removeDocument?: (doc: Doc | Doc[]) => boolean; moveDocument?: (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean; ScreenToLocalTransform: () => Transform; - setupDragLines?: () => void; + setupDragLines?: (snapToDraggedDoc: boolean) => void; renderDepth: number; ContentScaling: () => number; PanelWidth: () => number; PanelHeight: () => number; - pointerEvents?: boolean; + pointerEvents?: string; + contentsPointerEvents?: string; focus: (doc: Doc, willZoom: boolean, scale?: number, afterFocus?: DocFocusFunc) => void; parentActive: (outsideReaction: boolean) => boolean; whenActiveChanged: (isActive: boolean) => void; @@ -82,7 +89,7 @@ export interface DocumentViewProps { addDocTab: (doc: Doc, where: string, libraryPath?: Doc[]) => boolean; pinToPres: (document: Doc) => void; backgroundHalo?: () => boolean; - backgroundColor?: (doc: Doc, renderDepth: number) => string | undefined; + backgroundColor?: (doc: Opt<Doc>, renderDepth: number) => string | undefined; forcedBackgroundColor?: (doc: Doc) => string | undefined; opacity?: () => number | undefined; ChromeHeight?: () => number; @@ -104,21 +111,26 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu private _lastTap: number = 0; private _doubleTap = false; private _mainCont = React.createRef<HTMLDivElement>(); - private _dropDisposer?: DragManager.DragDropDisposer; - private _showKPQuery: boolean = false; - private _queries: string = ""; private _titleRef = React.createRef<EditableView>(); + private _dropDisposer?: DragManager.DragDropDisposer; private _gestureEventDisposer?: GestureUtils.GestureEventDisposer; private _holdDisposer?: InteractionUtils.MultiTouchEventDisposer; protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; + private get active() { return SelectionManager.IsSelected(this, true) || this.props.parentActive(true); } public get displayName() { return "DocumentView(" + this.props.Document.title + ")"; } // this makes mobx trace() statements more descriptive public get ContentDiv() { return this._mainCont.current; } - private get active() { return SelectionManager.IsSelected(this, true) || this.props.parentActive(true); } + public get LayoutFieldKey() { return this.props.layoutKey || Doc.LayoutFieldKey(this.layoutDoc); } + @computed get ShowTitle() { + return StrCast(this.layoutDoc._showTitle, + !Doc.IsSystem(this.layoutDoc) && this.rootDoc.type === DocumentType.RTF && !this.props.treeViewDoc ? + (this.dataDoc.author === Doc.CurrentUserEmail ? StrCast(Doc.UserDoc().showTitle) : "author;creationDate") : + undefined); + } @computed get topMost() { return this.props.renderDepth === 0; } @computed get freezeDimensions() { return this.props.FreezeDimensions; } - @computed get nativeWidth() { return NumCast(this.layoutDoc._nativeWidth, this.props.NativeWidth() || (this.freezeDimensions ? this.layoutDoc[WidthSym]() : 0)); } - @computed get nativeHeight() { return NumCast(this.layoutDoc._nativeHeight, this.props.NativeHeight() || (this.freezeDimensions ? this.layoutDoc[HeightSym]() : 0)); } + @computed get nativeWidth() { return returnVal(this.props.NativeWidth?.(), NumCast(this.layoutDoc[(this.props.DataDoc ? this.LayoutFieldKey + "-" : "_") + "nativeWidth"], (this.freezeDimensions ? this.layoutDoc[WidthSym]() : 0))); } + @computed get nativeHeight() { return returnVal(this.props.NativeHeight?.(), NumCast(this.layoutDoc[(this.props.DataDoc ? this.LayoutFieldKey + "-" : "_") + "nativeHeight"], (this.freezeDimensions ? this.layoutDoc[HeightSym]() : 0))); } @computed get onClickHandler() { return this.props.onClick?.() ?? Cast(this.Document.onClick, ScriptField, Cast(this.layoutDoc.onClick, ScriptField, null)); } @computed get onDoubleClickHandler() { return this.props.onDoubleClick?.() ?? (Cast(this.layoutDoc.onDoubleClick, ScriptField, null) ?? this.Document.onDoubleClick); } @computed get onPointerDownHandler() { return this.props.onPointerDown?.() ?? ScriptCast(this.Document.onPointerDown); } @@ -128,6 +140,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu onClickFunc = () => this.onClickHandler; onDoubleClickFunc = () => this.onDoubleClickHandler; + constructor(props: any) { + super(props); + props.getView?.(this); + } + handle1PointerHoldStart = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): any => { this.removeMoveListeners(); this.removeEndListeners(); @@ -165,7 +182,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu me.touchEvent.preventDefault(); e.stopPropagation(); if (RadialMenu.Instance.used) { - this.onContextMenu(me.touches[0]); + this.onContextMenu(undefined, me.touches[0].pageX, me.touches[0].pageY); } } @@ -174,10 +191,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1]; RadialMenu.Instance.openMenu(pt.pageX - 15, pt.pageY - 15); - // RadialMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "onRight"), icon: "map-pin", selected: -1 }); + // RadialMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "add:right"), icon: "map-pin", selected: -1 }); const effectiveAcl = GetEffectiveAcl(this.props.Document[DataSym]); (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) && RadialMenu.Instance.addItem({ description: "Delete", event: () => { this.props.ContainingCollectionView?.removeDocument(this.props.Document), RadialMenu.Instance.closeMenu(); }, icon: "external-link-square-alt", selected: -1 }); - // RadialMenu.Instance.addItem({ description: "Open in a new tab", event: () => this.props.addDocTab(this.props.Document, "onRight"), icon: "trash", selected: -1 }); + // RadialMenu.Instance.addItem({ description: "Open in a new tab", event: () => this.props.addDocTab(this.props.Document, "add:right"), icon: "trash", selected: -1 }); RadialMenu.Instance.addItem({ description: "Pin", event: () => this.props.pinToPres(this.props.Document), icon: "map-pin", selected: -1 }); RadialMenu.Instance.addItem({ description: "Open", event: () => MobileInterface.Instance.handleClick(this.props.Document), icon: "trash", selected: -1 }); @@ -191,8 +208,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu this._mainCont.current && (this._multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(this._mainCont.current, this.onTouchStart.bind(this))); // this._mainCont.current && (this.holdDisposer = InteractionUtils.MakeHoldTouchTarget(this._mainCont.current, this.handle1PointerHoldStart.bind(this))); - if (!this.props.dontRegisterView) { - DocumentManager.Instance.DocumentViews.push(this); + if (!BoolCast(this.rootDoc.dontRegisterView, this.props.dontRegisterView)) { + DocumentManager.Instance.AddView(this); } } @@ -218,8 +235,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu this._holdDisposer?.(); Doc.UnBrushDoc(this.props.Document); if (!this.props.dontRegisterView) { - const index = DocumentManager.Instance.DocumentViews.indexOf(this); - index !== -1 && DocumentManager.Instance.DocumentViews.splice(index, 1); + DocumentManager.Instance.RemoveView(this); } } @@ -262,6 +278,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } onKeyDown = (e: React.KeyboardEvent) => { + if (this.rootDoc._singleLine && ((e.key === "Backspace" && this.dataDoc.text && !(this.dataDoc.text as RichTextField)?.Text) || ["Tab", "Enter"].includes(e.key))) { + return; + } if (e.altKey && !(e.nativeEvent as any).StopPropagationForReal) { (e.nativeEvent as any).StopPropagationForReal = true; // e.stopPropagation() doesn't seem to work... e.stopPropagation(); @@ -285,13 +304,19 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } } + _timeout: NodeJS.Timeout | undefined; + onClick = action((e: React.MouseEvent | React.PointerEvent) => { if (!e.nativeEvent.cancelBubble && !this.Document.ignoreClick && this.props.renderDepth >= 0 && (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) { let stopPropagate = true; let preventDefault = true; - !this.props.Document.isBackground && this.props.bringToFront(this.props.Document); - if (this._doubleTap && this.props.renderDepth) {// && !this.onClickHandler?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click + !this.props.Document._isBackground && (this.rootDoc._raiseWhenDragged === undefined ? Doc.UserDoc()._raiseWhenDragged : this.rootDoc._raiseWhenDragged) && this.props.bringToFront(this.rootDoc); + if (this._doubleTap && ((this.props.renderDepth && this.props.Document.type !== DocumentType.FONTICON) || this.onDoubleClickHandler)) {// && !this.onClickHandler?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click + if (this._timeout) { + clearTimeout(this._timeout); + this._timeout = undefined; + } if (!(e.nativeEvent as any).formattedHandled) { if (this.onDoubleClickHandler?.script && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes("ScriptingBox")) { // bcz: hack? don't execute script if you're clicking on a scripting box itself const func = () => this.onDoubleClickHandler.script.run({ @@ -301,40 +326,53 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu shiftKey: e.shiftKey }, console.log); func(); - } else { - UndoManager.RunInBatch(() => { - let fullScreenDoc = this.props.Document; - if (StrCast(this.props.Document.layoutKey) !== "layout_fullScreen" && this.props.Document.layout_fullScreen) { - fullScreenDoc = Doc.MakeAlias(this.props.Document); - fullScreenDoc.layoutKey = "layout_fullScreen"; - } - this.props.addDocTab(fullScreenDoc, "inTab"); - }, "double tap"); - SelectionManager.DeselectAll(); + } else if (!Doc.IsSystem(this.props.Document)) { + if (this.props.Document.type === DocumentType.INK) { + InkStrokeProperties.Instance && (InkStrokeProperties.Instance._controlBtn = true); + } else { + UndoManager.RunInBatch(() => { + let fullScreenDoc = this.props.Document; + if (StrCast(this.props.Document.layoutKey) !== "layout_fullScreen" && this.props.Document.layout_fullScreen) { + fullScreenDoc = Doc.MakeAlias(this.props.Document); + fullScreenDoc.layoutKey = "layout_fullScreen"; + } + this.props.addDocTab(fullScreenDoc, "add"); + }, "double tap"); + SelectionManager.DeselectAll(); + } Doc.UnBrushDoc(this.props.Document); } } } else if (this.onClickHandler?.script && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes("ScriptingBox")) { // bcz: hack? don't execute script if you're clicking on a scripting box itself + const shiftKey = e.shiftKey; const func = () => this.onClickHandler.script.run({ this: this.layoutDoc, self: this.rootDoc, scriptContext: this.props.scriptContext, thisContainer: this.props.ContainingCollectionDoc, documentView: this, - shiftKey: e.shiftKey + shiftKey }, console.log); - if (!Doc.AreProtosEqual(this.props.Document, Doc.UserDoc()["dockedBtn-undo"] as Doc) && !Doc.AreProtosEqual(this.props.Document, Doc.UserDoc()["dockedBtn-redo"] as Doc)) { - UndoManager.RunInBatch(func, "on click"); - } else func(); + const clickFunc = () => { + if (!Doc.AreProtosEqual(this.props.Document, Doc.UserDoc()["dockedBtn-undo"] as Doc) && + !Doc.AreProtosEqual(this.props.Document, Doc.UserDoc()["dockedBtn-redo"] as Doc) && + !this.onClickHandler.script.originalScript.includes("selectMainMenu")) { + UndoManager.RunInBatch(func, "on click"); + } else func(); + }; + if (this.onDoubleClickHandler) { + this._timeout = setTimeout(() => { this._timeout = undefined; clickFunc(); }, 500); + } else clickFunc(); } else if (this.Document["onClick-rawScript"] && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes("ScriptingBox")) {// bcz: hack? don't edit a script if you're clicking on a scripting box itself - this.props.addDocTab(DocUtils.makeCustomViewClicked(Doc.MakeAlias(this.props.Document), undefined, "onClick"), "onRight"); + this.props.addDocTab(DocUtils.makeCustomViewClicked(Doc.MakeAlias(this.props.Document), undefined, "onClick"), "add:right"); } else if (this.allLinks && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) { - this.allLinks.length && this.followLinkClick(e.altKey, e.ctrlKey, e.shiftKey); + this.allLinks.length && DocumentView.followLinkClick(undefined, this.props.Document, this.props, e.shiftKey, e.altKey); } else { if ((this.layoutDoc.onDragStart || this.props.Document.rootDocument) && !(e.ctrlKey || e.button > 0)) { // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTemplaetForField implies we're clicking on part of a template instance and we want to select the whole template, not the part stopPropagate = false; // don't stop propagation for field templates -- want the selection to propagate up to the root document of the template } else { - SelectionManager.SelectDoc(this, e.ctrlKey || e.shiftKey); + this.select(e.ctrlKey || e.shiftKey); + //SelectionManager.SelectDoc(this, e.ctrlKey || e.shiftKey); } preventDefault = false; } @@ -346,27 +384,35 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu // follows a link - if the target is on screen, it highlights/pans to it. // if the target isn't onscreen, then it will open up the target in a tab, on the right, or in place // depending on the followLinkLocation property of the source (or the link itself as a fallback); - followLinkClick = async (altKey: boolean, ctrlKey: boolean, shiftKey: boolean) => { + public static followLinkClick = async (linkDoc: Opt<Doc>, sourceDoc: Doc, docView: { + focus: (doc: Doc, willZoom: boolean, scale?: number, afterFocus?: DocFocusFunc) => void, + addDocTab: (doc: Doc, where: string, libraryPath?: Doc[]) => boolean, + ContainingCollectionDoc?: Doc + }, shiftKey: boolean, altKey: boolean) => { const batch = UndoManager.StartBatch("follow link click"); // open up target if it's not already in view ... const createViewFunc = (doc: Doc, followLoc: string, finished: Opt<() => void>) => { const targetFocusAfterDocFocus = () => { - const where = StrCast(this.Document.followLinkLocation) || followLoc; + const where = StrCast(sourceDoc.followLinkLocation) || followLoc; const hackToCallFinishAfterFocus = () => { finished && setTimeout(finished, 0); // finished() needs to be called right after hackToCallFinishAfterFocus(), but there's no callback for that so we use the hacky timeout. return false; // we must return false here so that the zoom to the document is not reversed. If it weren't for needing to call finished(), we wouldn't need this function at all since not having it is equivalent to returning false }; - this.props.addDocTab(doc, where) && this.props.focus(doc, BoolCast(this.Document.followLinkZoom, true), undefined, hackToCallFinishAfterFocus); // add the target and focus on it. - return where !== "inPlace"; // return true to reset the initial focus&zoom (return false for 'inPlace' since resetting the initial focus&zoom will negate the zoom into the target) + const addTab = docView.addDocTab(doc, where); + addTab && setTimeout(() => { + const targDocView = DocumentManager.Instance.getFirstDocumentView(doc); + targDocView?.props.focus(doc, BoolCast(sourceDoc.followLinkZoom, false), undefined, hackToCallFinishAfterFocus); + }); // add the target and focus on it. + return where !== "inPlace" || addTab; // return true to reset the initial focus&zoom (return false for 'inPlace' since resetting the initial focus&zoom will negate the zoom into the target) }; - if (!this.Document.followLinkZoom) { + if (!sourceDoc.followLinkZoom) { targetFocusAfterDocFocus(); } else { // first focus & zoom onto this (the clicked document). Then execute the function to focus on the target - this.props.focus(this.props.Document, BoolCast(this.Document.followLinkZoom, true), 1, targetFocusAfterDocFocus); + docView.focus(sourceDoc, BoolCast(sourceDoc.followLinkZoom, true), 1, targetFocusAfterDocFocus); } }; - await DocumentManager.Instance.FollowLink(undefined, this.props.Document, createViewFunc, shiftKey, this.props.ContainingCollectionDoc, batch.end, altKey ? true : undefined); + await DocumentManager.Instance.FollowLink(linkDoc, sourceDoc, createViewFunc, BoolCast(sourceDoc.followLinkZoom, false), docView.ContainingCollectionDoc, batch.end, altKey ? true : undefined); } handle1PointerDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => { @@ -564,17 +610,19 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @undoBatch @action deleteClicked = (): void => { - if (Doc.UserDoc().activeWorkspace === this.props.Document) { - alert("Can't delete the active workspace"); + if (CurrentUserUtils.ActiveDashboard === this.props.Document) { + alert("Can't delete the active dashboard"); } else { - const selected = SelectionManager.SelectedDocuments().slice(); - SelectionManager.DeselectAll(); - selected.map(dv => dv.props.removeDocument?.(dv.props.Document)); this.props.removeDocument?.(this.props.Document); } } @undoBatch @action + toggleRaiseWhenDragged = () => { + this.rootDoc._raiseWhenDragged = this.rootDoc._raiseWhenDragged === undefined ? false : undefined; + } + + @undoBatch @action toggleFollowLink = (location: Opt<string>, zoom: boolean, setPushpin: boolean): void => { this.Document.ignoreClick = false; this.Document.isLinkButton = !this.Document.isLinkButton; @@ -586,6 +634,27 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu this.Document.onClick = this.layoutDoc.onClick = undefined; } } + @undoBatch @action + toggleTargetOnClick = (): void => { + this.Document.ignoreClick = false; + this.Document.isLinkButton = true; + this.Document.isPushpin = true; + } + @undoBatch @action + followLinkOnClick = (location: Opt<string>, zoom: boolean,): void => { + this.Document.ignoreClick = false; + this.Document.isLinkButton = true; + this.Document.isPushpin = false; + this.Document.followLinkZoom = zoom; + this.Document.followLinkLocation = location; + } + @undoBatch @action + selectOnClick = (): void => { + this.Document.ignoreClick = false; + this.Document.isLinkButton = false; + this.Document.isPushpin = false; + this.Document.onClick = this.layoutDoc.onClick = undefined; + } @undoBatch @@ -601,8 +670,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @undoBatch @action drop = async (e: Event, de: DragManager.DropEvent) => { - if (this.props.Document === Doc.UserDoc().activeWorkspace) { - alert("linking to document tabs not yet supported. Drop link on document content."); + if (this.props.LayoutTemplateString) return; + if (this.props.Document === CurrentUserUtils.ActiveDashboard) { + if ((e.target as any)?.closest?.("*.lm_content")) { + alert("You can't perform this move most likely because you don't have permission to modify the destination."); + } + else alert("linking to document tabs not yet supported. Drop link on document content."); return; } const makeLink = action((linkDoc: Doc) => { @@ -617,15 +690,23 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu LinkDescriptionPopup.popupY = de.y; LinkDescriptionPopup.descriptionPopup = true; + const rect = document.body.getBoundingClientRect(); + if (LinkDescriptionPopup.popupX + 200 > rect.width) { + LinkDescriptionPopup.popupX -= 190; + TaskCompletionBox.popupX -= 40; + } + if (LinkDescriptionPopup.popupY + 100 > rect.height) { + LinkDescriptionPopup.popupY -= 40; + TaskCompletionBox.popupY -= 40; + } + setTimeout(action(() => TaskCompletionBox.taskCompleted = false), 2500); }); if (de.complete.annoDragData) { /// this whole section for handling PDF annotations looks weird. Need to rethink this to make it cleaner e.stopPropagation(); - de.complete.annoDragData.linkedToDoc = true; - - const linkDoc = DocUtils.MakeLink({ doc: de.complete.annoDragData.annotationDocument }, { doc: this.props.Document }, "link"); - linkDoc && makeLink(linkDoc); + de.complete.annoDragData.linkDocument = DocUtils.MakeLink({ doc: de.complete.annoDragData.annotationDocument }, { doc: this.props.Document }, "link"); + de.complete.annoDragData.linkDocument && makeLink(de.complete.annoDragData.linkDocument); } if (de.complete.linkDragData) { e.stopPropagation(); @@ -666,12 +747,14 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @undoBatch @action toggleBackground = () => { - this.Document.isBackground = (this.Document.isBackground ? undefined : true); - this.Document._overflow = this.Document.isBackground ? "visible" : undefined; - if (this.Document.isBackground) { + this.Document._isBackground = (this.Document._isBackground ? undefined : true); + this.Document._overflow = this.Document._isBackground ? "visible" : undefined; + if (this.Document._isBackground) { this.props.bringToFront(this.props.Document, true); - this.props.Document[DataSym][Doc.LayoutFieldKey(this.Document) + "-nativeWidth"] = this.Document[WidthSym](); - this.props.Document[DataSym][Doc.LayoutFieldKey(this.Document) + "-nativeHeight"] = this.Document[HeightSym](); + const wid = this.Document[WidthSym](); // change the nativewidth and height if the background is to be a collection that aggregates stuff that is added to it. + const hgt = this.Document[HeightSym](); + this.props.Document[DataSym][this.LayoutFieldKey + "-nativeWidth"] = wid; + this.props.Document[DataSym][this.LayoutFieldKey + "-nativeHeight"] = hgt; } } @@ -684,60 +767,82 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } @action - onContextMenu = async (e: React.MouseEvent | Touch): Promise<void> => { + onContextMenu = (e?: React.MouseEvent, pageX?: number, pageY?: number) => { + if (e && this.rootDoc._hideContextMenu && Doc.UserDoc().noviceMode) { + e.preventDefault(); + e.stopPropagation(); + !this.isSelected(true) && SelectionManager.SelectDoc(this, false); + } // the touch onContextMenu is button 0, the pointer onContextMenu is button 2 - if (!(e instanceof Touch)) { - if (e.button === 0 && !e.ctrlKey) { + if (e) { + if (e.button === 0 && !e.ctrlKey || e.isDefaultPrevented()) { e.preventDefault(); return; } - e.persist(); e.stopPropagation(); + e.persist(); - if (Math.abs(this._downX - e?.clientX) > 3 || Math.abs(this._downY - e?.clientY) > 3 || - e?.isDefaultPrevented()) { - e?.preventDefault(); - return; + if (!navigator.userAgent.includes("Mozilla")) { + if (Math.abs(this._downX - e?.clientX) > 3 || Math.abs(this._downY - e?.clientY) > 3) { + e?.preventDefault(); + return; + } } e.preventDefault(); } const cm = ContextMenu.Instance; - if (!cm) return; + if (!cm || (e as any)?.nativeEvent?.SchemaHandled) return; const customScripts = Cast(this.props.Document.contextMenuScripts, listSpec(ScriptField), []); Cast(this.props.Document.contextMenuLabels, listSpec("string"), []).forEach((label, i) => cm.addItem({ description: label, event: () => customScripts[i]?.script.run({ this: this.layoutDoc, self: this.rootDoc }), icon: "sticky-note" })); this.props.contextMenuItems?.().forEach(item => - cm.addItem({ description: item.label, event: () => item.script.script.run({ this: this.layoutDoc, self: this.rootDoc }), icon: "sticky-note" })); + item.label && cm.addItem({ description: item.label, event: () => item.script.script.run({ this: this.layoutDoc, self: this.rootDoc }), icon: "sticky-note" })); const templateDoc = Cast(this.props.Document[StrCast(this.props.Document.layoutKey)], Doc, null); const appearance = cm.findByDescription("UI Controls..."); const appearanceItems: ContextMenuProps[] = appearance && "subitems" in appearance ? appearance.subitems : []; - templateDoc && appearanceItems.push({ description: "Open Template ", event: () => this.props.addDocTab(templateDoc, "onRight"), icon: "eye" }); - //DocListCast(this.Document.links).length && appearanceItems.splice(0, 0, { description: `${this.layoutDoc.hideLinkButton ? "Show" : "Hide"} Link Button`, event: action(() => this.layoutDoc.hideLinkButton = !this.layoutDoc.hideLinkButton), icon: "eye" }); + !Doc.UserDoc().noviceMode && templateDoc && appearanceItems.push({ description: "Open Template ", event: () => this.props.addDocTab(templateDoc, "add:right"), icon: "eye" }); + DocListCast(this.Document.links).length && appearanceItems.splice(0, 0, { description: `${this.layoutDoc.hideLinkButton ? "Show" : "Hide"} Link Button`, event: action(() => this.layoutDoc.hideLinkButton = !this.layoutDoc.hideLinkButton), icon: "eye" }); !appearance && cm.addItem({ description: "UI Controls...", subitems: appearanceItems, icon: "compass" }); - const options = cm.findByDescription("Options..."); - const optionItems: ContextMenuProps[] = options && "subitems" in options ? options.subitems : []; - optionItems.push({ description: this.Document.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.Document.lockedPosition) ? "unlock" : "lock" }); - !options && cm.addItem({ description: "Options...", subitems: optionItems, icon: "compass" }); - - - const existingOnClick = cm.findByDescription("OnClick..."); - const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; - onClicks.push({ description: "Enter Portal", event: this.makeIntoPortal, icon: "window-restore" }); - onClicks.push({ description: "Toggle Detail", event: () => this.Document.onClick = ScriptField.MakeScript(`toggleDetail(self, "${this.Document.layoutKey}")`), icon: "concierge-bell" }); - onClicks.push({ description: this.Document.ignoreClick ? "Select" : "Do Nothing", event: () => this.Document.ignoreClick = !this.Document.ignoreClick, icon: this.Document.ignoreClick ? "unlock" : "lock" }); - onClicks.push({ description: this.Document.isLinkButton ? "Remove Follow Behavior" : "Follow Link in Place", event: () => this.toggleFollowLink("inPlace", true, false), icon: "link" }); - !this.Document.isLinkButton && onClicks.push({ description: "Follow Link on Right", event: () => this.toggleFollowLink("onRight", false, false), icon: "link" }); - onClicks.push({ description: this.Document.isLinkButton || this.onClickHandler ? "Remove Click Behavior" : "Follow Link", event: () => this.toggleFollowLink(undefined, false, false), icon: "link" }); - onClicks.push({ description: (this.Document.isPushpin ? "Remove" : "Make") + " Pushpin", event: () => this.toggleFollowLink(undefined, false, true), icon: "map-pin" }); - onClicks.push({ description: "Edit onClick Script", event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick"), icon: "terminal" }); - !existingOnClick && cm.addItem({ description: "OnClick...", noexpand: true, addDivider: true, subitems: onClicks, icon: "mouse-pointer" }); + if (!Doc.IsSystem(this.rootDoc) && this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Tree) { + const existingOnClick = cm.findByDescription("OnClick..."); + const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; + + const zorders = cm.findByDescription("ZOrder..."); + const zorderItems: ContextMenuProps[] = zorders && "subitems" in zorders ? zorders.subitems : []; + zorderItems.push({ description: "Bring to Front", event: () => this.props.bringToFront(this.rootDoc, false), icon: "expand-arrows-alt" }); + zorderItems.push({ description: "Send to Back", event: () => this.props.bringToFront(this.rootDoc, true), icon: "expand-arrows-alt" }); + zorderItems.push({ description: this.rootDoc._raiseWhenDragged !== false ? "Keep ZIndex when dragged" : "Allow ZIndex to change when dragged", event: this.toggleRaiseWhenDragged, icon: "expand-arrows-alt" }); + !zorders && cm.addItem({ description: "ZOrder...", subitems: zorderItems, icon: "compass" }); + + if (!this.Document.annotationOn) { + const options = cm.findByDescription("Options..."); + const optionItems: ContextMenuProps[] = options && "subitems" in options ? options.subitems : []; + !this.props.treeViewDoc && this.props.ContainingCollectionDoc?._viewType === CollectionViewType.Freeform && optionItems.push({ description: this.Document.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.Document.lockedPosition) ? "unlock" : "lock" }); + !options && cm.addItem({ description: "Options...", subitems: optionItems, icon: "compass" }); + + onClicks.push({ description: "Enter Portal", event: this.makeIntoPortal, icon: "window-restore" }); + onClicks.push({ description: "Toggle Detail", event: () => this.Document.onClick = ScriptField.MakeScript(`toggleDetail(self, "${this.Document.layoutKey}")`), icon: "concierge-bell" }); + onClicks.push({ description: this.Document.ignoreClick ? "Select" : "Do Nothing", event: () => this.Document.ignoreClick = !this.Document.ignoreClick, icon: this.Document.ignoreClick ? "unlock" : "lock" }); + onClicks.push({ description: this.Document.isLinkButton ? "Remove Follow Behavior" : "Follow Link in Place", event: () => this.toggleFollowLink("inPlace", true, false), icon: "link" }); + !this.Document.isLinkButton && onClicks.push({ description: "Follow Link on Right", event: () => this.toggleFollowLink("add:right", false, false), icon: "link" }); + onClicks.push({ description: this.Document.isLinkButton || this.onClickHandler ? "Remove Click Behavior" : "Follow Link", event: () => this.toggleFollowLink(undefined, false, false), icon: "link" }); + onClicks.push({ description: (this.Document.isPushpin ? "Remove" : "Make") + " Pushpin", event: () => this.toggleFollowLink(undefined, false, true), icon: "map-pin" }); + onClicks.push({ description: "Edit onClick Script", event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick"), icon: "terminal" }); + !existingOnClick && cm.addItem({ description: "OnClick...", addDivider: true, noexpand: true, subitems: onClicks, icon: "mouse-pointer" }); + } else if (DocListCast(this.Document.links).length) { + onClicks.push({ description: "Select on Click", event: () => this.selectOnClick(), icon: "link" }); + onClicks.push({ description: "Follow Link on Click", event: () => this.followLinkOnClick(undefined, false), icon: "link" }); + onClicks.push({ description: "Toggle Link Target on Click", event: () => this.toggleTargetOnClick(), icon: "map-pin" }); + !existingOnClick && cm.addItem({ description: "OnClick...", addDivider: true, subitems: onClicks, icon: "mouse-pointer" }); + } + } const funcs: ContextMenuProps[] = []; - if (this.layoutDoc.onDragStart) { + if (!Doc.UserDoc().noviceMode && this.layoutDoc.onDragStart) { funcs.push({ description: "Drag an Alias", icon: "edit", event: () => this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getAlias(this.dragFactory)')) }); funcs.push({ description: "Drag a Copy", icon: "edit", event: () => this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)')) }); funcs.push({ description: "Drag Document", icon: "edit", event: () => this.layoutDoc.onDragStart = undefined }); @@ -746,42 +851,43 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const more = cm.findByDescription("More..."); const moreItems = more && "subitems" in more ? more.subitems : []; - moreItems.push({ description: "Download document", icon: "download", event: async () => Doc.Zip(this.props.Document) }); - moreItems.push({ description: "Share", event: () => SharingManager.Instance.open(this), icon: "users" }); - //moreItems.push({ description: this.Document.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.Document.lockedPosition) ? "unlock" : "lock" }); - //moreItems.push({ description: "Create an Alias", event: () => this.onCopy(), icon: "copy" }); - if (!Doc.UserDoc().noviceMode) { - moreItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc), icon: "concierge-bell" }); - moreItems.push({ description: `${this.Document._chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.Document._chromeStatus = (this.Document._chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" }); - - if (Cast(Doc.GetProto(this.props.Document).data, listSpec(Doc))) { - moreItems.push({ description: "Export to Google Photos Album", event: () => GooglePhotos.Export.CollectionToAlbum({ collection: this.props.Document }).then(console.log), icon: "caret-square-right" }); - moreItems.push({ description: "Tag Child Images via Google Photos", event: () => GooglePhotos.Query.TagChildImages(this.props.Document), icon: "caret-square-right" }); - moreItems.push({ description: "Write Back Link to Album", event: () => GooglePhotos.Transactions.AddTextEnrichment(this.props.Document), icon: "caret-square-right" }); + if (!Doc.IsSystem(this.rootDoc)) { + (this.rootDoc._viewType !== CollectionViewType.Docking || !Doc.UserDoc().noviceMode) && moreItems.push({ description: "Share", event: () => SharingManager.Instance.open(this), icon: "users" }); + //moreItems.push({ description: this.Document.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.Document.lockedPosition) ? "unlock" : "lock" }); + if (!Doc.UserDoc().noviceMode) { + moreItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc), icon: "concierge-bell" }); + moreItems.push({ description: `${this.Document._chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.Document._chromeStatus = (this.Document._chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" }); + + if (Cast(Doc.GetProto(this.props.Document).data, listSpec(Doc))) { + moreItems.push({ description: "Export to Google Photos Album", event: () => GooglePhotos.Export.CollectionToAlbum({ collection: this.props.Document }).then(console.log), icon: "caret-square-right" }); + moreItems.push({ description: "Tag Child Images via Google Photos", event: () => GooglePhotos.Query.TagChildImages(this.props.Document), icon: "caret-square-right" }); + moreItems.push({ description: "Write Back Link to Album", event: () => GooglePhotos.Transactions.AddTextEnrichment(this.props.Document), icon: "caret-square-right" }); + } + moreItems.push({ description: "Copy ID", event: () => Utils.CopyText(Utils.prepend("/doc/" + this.props.Document[Id])), icon: "fingerprint" }); } - moreItems.push({ description: "Copy ID", event: () => Utils.CopyText(Utils.prepend("/doc/" + this.props.Document[Id])), icon: "fingerprint" }); - Doc.AreProtosEqual(this.props.Document, Doc.UserDoc()) && moreItems.push({ description: "Toggle Always Show Link End", event: () => Doc.UserDoc()["documentLinksButton-hideEnd"] = !Doc.UserDoc()["documentLinksButton-hideEnd"], icon: "eye" }); } const collectionAcl = GetEffectiveAcl(this.props.ContainingCollectionDoc?.[DataSym]); - if (collectionAcl === AclAdmin || collectionAcl === AclEdit) moreItems.push({ description: "Close", event: this.deleteClicked, icon: "times" }); + if (this.props.removeDocument && !this.props.Document._stayInCollection) { // need option to gray out menu items ... preferably with a '?' that explains why they're grayed out (eg., no permissions) + moreItems.push({ description: "Close", event: this.deleteClicked, icon: "times" }); + } !more && cm.addItem({ description: "More...", subitems: moreItems, icon: "hand-point-right" }); cm.moveAfter(cm.findByDescription("More...")!, cm.findByDescription("OnClick...")!); const help = cm.findByDescription("Help..."); const helpItems: ContextMenuProps[] = help && "subitems" in help ? help.subitems : []; - !Doc.UserDoc().novice && helpItems.push({ description: "Show Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "onRight"), icon: "layer-group" }); - helpItems.push({ description: "Text Shortcuts Ctrl+/", event: () => this.props.addDocTab(Docs.Create.PdfDocument(Utils.prepend("/assets/cheat-sheet.pdf"), { _width: 300, _height: 300 }), "onRight"), icon: "keyboard" }); - helpItems.push({ description: "Print Document in Console", event: () => console.log(this.props.Document), icon: "hand-point-right" }); + !Doc.UserDoc().novice && helpItems.push({ description: "Show Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "add:right"), icon: "layer-group" }); + helpItems.push({ description: "Text Shortcuts Ctrl+/", event: () => this.props.addDocTab(Docs.Create.PdfDocument(Utils.prepend("/assets/cheat-sheet.pdf"), { _width: 300, _height: 300 }), "add:right"), icon: "keyboard" }); + !Doc.UserDoc().novice && helpItems.push({ description: "Print Document in Console", event: () => console.log(this.props.Document), icon: "hand-point-right" }); cm.addItem({ description: "Help...", noexpand: true, subitems: helpItems, icon: "question" }); runInAction(() => { - if (!this.topMost && !(e instanceof Touch)) { - e.stopPropagation(); // DocumentViews should stop propagation of this event + if (!this.topMost) { + e?.stopPropagation(); // DocumentViews should stop propagation of this event } - cm.displayMenu(e.pageX - 15, e.pageY - 15); - !SelectionManager.IsSelected(this, true) && SelectionManager.SelectDoc(this, false); + cm.displayMenu((e?.pageX || pageX || 0) - 15, (e?.pageY || pageY || 0) - 15); + !this.isSelected(true) && SelectionManager.SelectDoc(this, false); }); } @@ -795,11 +901,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu isSelected = (outsideReaction?: boolean) => SelectionManager.IsSelected(this, outsideReaction); select = (ctrlPressed: boolean) => { SelectionManager.SelectDoc(this, ctrlPressed); }; - chromeHeight = () => { - const showTitle = StrCast(this.layoutDoc._showTitle); - const showTextTitle = showTitle && (StrCast(this.layoutDoc.layout).indexOf("PresBox") !== -1 || StrCast(this.layoutDoc.layout).indexOf("FormattedTextBox") !== -1) ? showTitle : undefined; - return showTextTitle ? 25 : 1; + @computed get showOverlappingTitle() { + const excluded = ["PresBox", /* "FormattedTextBox", */ "FontIconBox"]; // bcz: shifting the title for texst causes problems with collaborative use when some people see titles, and others don't + return !excluded.includes(StrCast(this.layoutDoc.layout)); } + chromeHeight = () => this.showOverlappingTitle ? 1 : 25; @computed get finalLayoutKey() { if (typeof this.props.layoutKey === "string") { @@ -812,13 +918,14 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu return this.isSelected(outsideReaction) || (this.props.Document.rootDocument && this.props.rootSelected?.(outsideReaction)) || false; } childScaling = () => (this.layoutDoc._fitWidth ? this.props.PanelWidth() / this.nativeWidth : this.props.ContentScaling()); - @computed.struct get linkOffset() { return [-15, 0]; } + @computed.struct get linkOffset() { return this.topMost ? [0, undefined, undefined, 10] : [-15, undefined, undefined, undefined]; } @computed get contents() { - const pos = this.props.relative ? "relative " : "absolute"; TraceMobx(); - return (<div className="documentView-contentsView" style={{ borderRadius: "inherit", width: "100%", height: "100%" }}> + return (<div className="documentView-contentsView" style={{ pointerEvents: this.props.contentsPointerEvents as any, borderRadius: "inherit", width: "100%", height: "100%" }}> <DocumentContentsView key={1} docFilters={this.props.docFilters} + docRangeFilters={this.props.docRangeFilters} + searchFilterDocs={this.props.searchFilterDocs} ContainingCollectionView={this.props.ContainingCollectionView} ContainingCollectionDoc={this.props.ContainingCollectionDoc} NativeWidth={this.NativeWidth} @@ -857,7 +964,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu layoutKey={this.finalLayoutKey} /> {this.layoutDoc.hideAllLinks ? (null) : this.allAnchors} {/* {this.allAnchors} */} - {this.props.forcedBackgroundColor?.(this.Document) === "transparent" || this.layoutDoc.isLinkButton || this.layoutDoc.hideLinkButton || this.props.dontRegisterView ? (null) : + {this.props.forcedBackgroundColor?.(this.Document) === "transparent" || (!this.isSelected() && (this.layoutDoc.isLinkButton || this.layoutDoc.hideLinkButton)) || this.props.dontRegisterView ? (null) : <DocumentLinksButton View={this} links={this.allLinks} Offset={this.linkOffset} />} </div> ); @@ -880,9 +987,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu anchorPanelWidth = () => this.props.PanelWidth() || 1; anchorPanelHeight = () => this.props.PanelHeight() || 1; - @computed.struct get directLinks() { return LinkManager.Instance.getAllDirectLinks(this.rootDoc); } - @computed.struct get allLinks() { return DocListCast(this.Document.links); } - @computed.struct get allAnchors() { + @computed get directLinks() { TraceMobx(); return LinkManager.Instance.getAllDirectLinks(this.rootDoc); } + @computed get allLinks() { TraceMobx(); return DocListCast(this.Document.links); } + @computed get allAnchors() { TraceMobx(); if (this.props.LayoutTemplateString?.includes("LinkAnchorBox")) return null; if ((this.props.treeViewDoc && this.props.LayoutTemplateString) || // render nothing for: tree view anchor dots @@ -904,7 +1011,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu dontRegisterView={false} forcedBackgroundColor={returnTransparent} removeDocument={this.hideLinkAnchor} - pointerEvents={false} + pointerEvents={"none"} LayoutTemplate={undefined} LayoutTemplateString={LinkAnchorBox.LayoutString(`anchor${Doc.LinkEndpoint(d, this.props.Document)}`)} /> </div >); @@ -922,14 +1029,15 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu </div>; } - const showTitle = StrCast(this.layoutDoc._showTitle); const showTitleHover = StrCast(this.layoutDoc._showTitleHover); const showCaption = StrCast(this.layoutDoc._showCaption); - const showTextTitle = showTitle && (StrCast(this.layoutDoc.layout).indexOf("FormattedTextBox") !== -1) ? showTitle : undefined; const captionView = (!showCaption ? (null) : <div className="documentView-captionWrapper" style={{ backgroundColor: StrCast(this.layoutDoc["caption-backgroundColor"]), color: StrCast(this.layoutDoc["caption-color"]) }}> <DocumentContentsView {...OmitKeys(this.props, ['children']).omit} + yMargin={10} + xMargin={10} hideOnLeave={true} + dontRegisterView={true} LayoutTemplateString={`<FormattedTextBox {...props} fieldKey={'${showCaption}'}/>`} ContentScaling={returnOne} ChromeHeight={this.chromeHeight} @@ -938,28 +1046,32 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu onClick={this.onClickFunc} layoutKey={this.finalLayoutKey} /> </div>); - const titleView = (!showTitle ? (null) : + const titleView = (!this.ShowTitle ? (null) : <div className={`documentView-titleWrapper${showTitleHover ? "-hover" : ""}`} key="title" style={{ - position: showTextTitle ? "relative" : "absolute", + position: this.showOverlappingTitle ? "absolute" : "relative", + background: SharingManager.Instance.users.find(users => users.user.email === this.dataDoc.author)?.userColor || (this.rootDoc.type === DocumentType.RTF ? StrCast(Doc.UserDoc().userColor) : "rgba(0,0,0,0.4)"), pointerEvents: this.onClickHandler || this.Document.ignoreClick ? "none" : undefined, }}> <EditableView ref={this._titleRef} - contents={(this.props.DataDoc || this.props.Document)[showTitle]?.toString()} - display={"block"} height={72} fontSize={12} - GetValue={() => (this.props.DataDoc || this.props.Document)[showTitle]?.toString()} - SetValue={undoBatch((value: string) => (Doc.GetProto(this.props.DataDoc || this.props.Document)[showTitle] = value) ? true : true)} + contents={this.ShowTitle === "title" ? StrCast((this.dataDoc || this.props.Document).title) : this.ShowTitle.split(";").map(field => field + ":" + (this.dataDoc || this.props.Document)[field]?.toString()).join(" ")} + display={"block"} + fontSize={10} + GetValue={() => Field.toString((this.dataDoc || this.props.Document)[this.ShowTitle.split(";")[0]] as any as Field)} + SetValue={undoBatch((value: string) => { + this.ShowTitle.includes("Date") ? true : (Doc.GetProto(this.dataDoc || this.props.Document)[this.ShowTitle] = value) ? true : true; + })} /> </div>); - return !showTitle && !showCaption ? + return !this.ShowTitle && !showCaption ? this.contents : <div className="documentView-styleWrapper" > - {this.Document.type !== DocumentType.RTF ? <> {this.contents} {titleView} </> : <> {titleView} {this.contents} </>} + {this.showOverlappingTitle ? <> {this.contents} {titleView} </> : <> {titleView} {this.contents} </>} {captionView} </div>; } @computed get ignorePointerEvents() { - return this.props.pointerEvents === false || - (this.Document.isBackground && !this.isSelected() && !SnappingManager.GetIsDragging()) || + return this.props.pointerEvents === "none" || + (this.Document._isBackground && !this.isSelected() && !SnappingManager.GetIsDragging()) || (this.Document.type === DocumentType.INK && Doc.GetSelectedTool() !== InkTool.None); } @undoBatch @@ -981,16 +1093,17 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu }); renderLock() { - return (this.Document.isBackground !== undefined || this.isSelected(false)) && + return (this.Document._isBackground !== undefined || this.isSelected(false)) && ((this.Document.type === DocumentType.COL && this.Document._viewType !== CollectionViewType.Pile) || this.Document.type === DocumentType.IMG) && this.props.renderDepth > 0 && !this.props.treeViewDoc ? <div className="documentView-lock" onClick={this.toggleBackground}> - <FontAwesomeIcon icon={this.Document.isBackground ? "unlock" : "lock"} style={{ color: this.Document.isBackground ? "red" : undefined }} size="lg" /> + <FontAwesomeIcon icon={this.Document._isBackground ? "unlock" : "lock"} style={{ color: this.Document._isBackground ? "red" : undefined }} size="lg" /> </div> : (null); } render() { + TraceMobx(); if (!(this.props.Document instanceof Doc)) return (null); if (GetEffectiveAcl(this.props.Document[DataSym]) === AclPrivate) return (null); if (this.props.Document.hidden) return (null); @@ -998,10 +1111,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const opacity = Cast(this.layoutDoc._opacity, "number", Cast(this.layoutDoc.opacity, "number", Cast(this.Document.opacity, "number", null))); const finalOpacity = this.props.opacity ? this.props.opacity() : opacity; const finalColor = this.layoutDoc.type === DocumentType.FONTICON || this.layoutDoc._viewType === CollectionViewType.Linear ? undefined : backgroundColor; - const fullDegree = Doc.isBrushedHighlightedDegree(this.props.Document); + const fullDegree = this.props.LayoutTemplateString ? (Doc.IsHighlighted(this.props.Document) ? 6 : 0) : Doc.isBrushedHighlightedDegree(this.props.Document); // bcz: Argh!! need to identify a tree view doc better than a LayoutTemlatString const borderRounding = this.layoutDoc.borderRounding; const localScale = fullDegree; - const highlightColors = Cast(Doc.UserDoc().activeWorkspace, Doc, null)?.darkScheme ? + const highlightColors = CurrentUserUtils.ActiveDashboard?.darkScheme ? ["transparent", "#65350c", "#65350c", "yellow", "magenta", "cyan", "orange"] : ["transparent", "maroon", "maroon", "yellow", "magenta", "cyan", "orange"]; const highlightStyles = ["solid", "dashed", "solid", "solid", "solid", "solid", "solid"]; @@ -1012,8 +1125,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu id={this.props.Document[Id]} ref={this._mainCont} onKeyDown={this.onKeyDown} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick} - onPointerEnter={action(() => { Doc.BrushDoc(this.props.Document); })} - onPointerLeave={action((e: React.PointerEvent<HTMLDivElement>) => { + onPointerEnter={action(e => !SnappingManager.GetIsDragging() && Doc.BrushDoc(this.props.Document))} + onPointerLeave={action(e => { let entered = false; const target = document.elementFromPoint(e.nativeEvent.x, e.nativeEvent.y); for (let child: any = target; child; child = child?.parentElement) { diff --git a/src/client/views/nodes/FaceRectangles.tsx b/src/client/views/nodes/FaceRectangles.tsx index 92ca276cb..0d1e063af 100644 --- a/src/client/views/nodes/FaceRectangles.tsx +++ b/src/client/views/nodes/FaceRectangles.tsx @@ -17,7 +17,7 @@ export interface RectangleTemplate { } @observer -export default class FaceRectangles extends React.Component<FaceRectanglesProps> { +export class FaceRectangles extends React.Component<FaceRectanglesProps> { render() { const faces = DocListCast(this.props.document.faces); diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 9d61ec6d1..d65b43704 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -29,6 +29,8 @@ export interface FieldViewProps { dropAction: dropActionType; backgroundHalo?: () => boolean; docFilters: () => string[]; + docRangeFilters: () => string[]; + searchFilterDocs: () => Doc[]; isSelected: (outsideReaction?: boolean) => boolean; select: (isCtrlPressed: boolean) => void; rootSelected: (outsideReaction?: boolean) => boolean; @@ -38,11 +40,12 @@ export interface FieldViewProps { pinToPres: (document: Doc) => void; removeDocument?: (document: Doc | Doc[]) => boolean; moveDocument?: (document: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean; - backgroundColor?: (document: Doc, renderDepth: number) => string | undefined; + backgroundColor?: (document: Opt<Doc>, renderDepth: number) => string | undefined; ScreenToLocalTransform: () => Transform; bringToFront: (doc: Doc, sendToBack?: boolean) => void; active: (outsideReaction?: boolean) => boolean; whenActiveChanged: (isActive: boolean) => void; + LayoutTemplateString?: string; dontRegisterView?: boolean; focus: (doc: Doc) => void; presMultiSelect?: (doc: Doc) => void; //added for selecting multiple documents in a presentation @@ -51,17 +54,15 @@ export interface FieldViewProps { PanelHeight: () => number; PanelPosition?: string; overflow?: boolean; - NativeHeight: () => number; - NativeWidth: () => number; + NativeHeight?: () => number; + NativeWidth?: () => number; setVideoBox?: (player: VideoBox) => void; ContentScaling: () => number; - ChromeHeight?: () => number; childLayoutTemplate?: () => Opt<Doc>; - highlighting?: string[]; - lines?: string[]; - doc?: Doc; // properties intended to be used from within layout strings (otherwise use the function equivalents that work more efficiently with React) + fontSize?: number; + pointerEvents?: string; height?: number; width?: number; background?: string; @@ -133,7 +134,7 @@ export class FieldView extends React.Component<FieldViewProps> { // ); } else if (field instanceof List) { - return <div> {field.map(f => Field.toString(f)).join(", ")} </div>; + return <div> {field.length ? field.map(f => Field.toString(f)).join(", ") : "[]"} </div>; } // bcz: this belongs here, but it doesn't render well so taking it out for now else if (field instanceof WebField) { diff --git a/src/client/views/nodes/FilterBox.scss b/src/client/views/nodes/FilterBox.scss new file mode 100644 index 000000000..d32cc0d2b --- /dev/null +++ b/src/client/views/nodes/FilterBox.scss @@ -0,0 +1,55 @@ + + +.filterBox-flyout { + width: 400px; + display: block; + text-align: left; + .filterBox-flyout-facet { + background-color: lightgray; + text-align: left; + display: inline-block; + position: relative; + width: 100%; + } +} +.filterBox-treeView { + display: flex; + flex-direction: column; + width: 200px; + height: 100%; + position: absolute; + right: 0; + top: 0; + border-left: solid 1px; + z-index: 1; + + .filterBox-addfacet { + display: inline-block; + width: 200px; + height: 30px; + background: darkGray; + text-align: left; + + .filterBox-addFacetButton { + display: flex; + margin: auto; + cursor: pointer; + + .filterBox-span { + margin-right: 15px; + } + } + + >div, + >div>div { + width: 100%; + height: 100%; + } + } + + .filterBox-tree { + display: inline-block; + width: 100%; + height: calc(100% - 30px); + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/FilterBox.tsx b/src/client/views/nodes/FilterBox.tsx new file mode 100644 index 000000000..d3c0bfba3 --- /dev/null +++ b/src/client/views/nodes/FilterBox.tsx @@ -0,0 +1,252 @@ +import React = require("react"); +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { computed } from "mobx"; +import { observer } from "mobx-react"; +import { DataSym, Doc, DocListCast, Field, Opt } from "../../../fields/Doc"; +import { documentSchema } from "../../../fields/documentSchemas"; +import { List } from "../../../fields/List"; +import { RichTextField } from "../../../fields/RichTextField"; +import { listSpec, makeInterface } from "../../../fields/Schema"; +import { ComputedField, ScriptField } from "../../../fields/ScriptField"; +import { Cast } from "../../../fields/Types"; +import { emptyFunction, emptyPath, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, returnZero } from "../../../Utils"; +import { Docs } from "../../documents/Documents"; +import { DocumentType } from "../../documents/DocumentTypes"; +import { CollectionDockingView } from "../collections/CollectionDockingView"; +import { CollectionTreeView } from "../collections/CollectionTreeView"; +import { ViewBoxBaseComponent } from "../DocComponent"; +import { SearchBox } from "../search/SearchBox"; +import { FieldView, FieldViewProps } from './FieldView'; +import './FilterBox.scss'; +import { Scripting } from "../../util/Scripting"; +import { values } from "lodash"; +const higflyout = require("@hig/flyout"); +export const { anchorPoints } = higflyout; +export const Flyout = higflyout.default; + +type FilterBoxDocument = makeInterface<[typeof documentSchema]>; +const FilterBoxDocument = makeInterface(documentSchema); + +@observer +export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDocument>(FilterBoxDocument) { + public static LayoutString(fieldKey: string) { return FieldView.LayoutString(FilterBox, fieldKey); } + + @computed get allDocs() { + const allDocs = new Set<Doc>(); + if (CollectionDockingView.Instance) { + const activeTabs = DocListCast(CollectionDockingView.Instance.props.Document.data); + SearchBox.foreachRecursiveDoc(activeTabs, (doc: Doc) => allDocs.add(doc)); + setTimeout(() => CollectionDockingView.Instance.props.Document.allDocuments = new List<Doc>(Array.from(allDocs))); + } + return allDocs; + } + + @computed get _allFacets() { + const noviceReqFields = ["author", "tags", "text", "type"]; + const noviceLayoutFields: string[] = [];//["_curPage"]; + const noviceFields = [...noviceReqFields, ...noviceLayoutFields]; + + const keys = new Set<string>(noviceFields); + this.allDocs.forEach(doc => SearchBox.documentKeys(doc).filter(key => keys.add(key))); + return Array.from(keys.keys()).filter(key => key[0]).filter(key => key[0] === "#" || key.indexOf("lastModified") !== -1 || (key[0] === key[0].toUpperCase() && !key.startsWith("_")) || noviceFields.includes(key) || !Doc.UserDoc().noviceMode).sort(); + } + + gatherFieldValues(dashboard: Doc, facetKey: string) { + const childDocs = DocListCast((dashboard.data as any)[0].data); + const valueSet = new Set<string>(); + let rtFields = 0; + childDocs.forEach((d) => { + const facetVal = d[facetKey]; + if (facetVal instanceof RichTextField) rtFields++; + valueSet.add(Field.toString(facetVal as Field)); + const fieldKey = Doc.LayoutFieldKey(d); + const annos = !Field.toString(Doc.LayoutField(d) as Field).includes("CollectionView"); + const data = d[annos ? fieldKey + "-annotations" : fieldKey]; + if (data !== undefined) { + let subDocs = DocListCast(data); + if (subDocs.length > 0) { + let newarray: Doc[] = []; + while (subDocs.length > 0) { + newarray = []; + subDocs.forEach((t) => { + const facetVal = t[facetKey]; + if (facetVal instanceof RichTextField) rtFields++; + valueSet.add(Field.toString(facetVal as Field)); + const fieldKey = Doc.LayoutFieldKey(t); + const annos = !Field.toString(Doc.LayoutField(t) as Field).includes("CollectionView"); + DocListCast(t[annos ? fieldKey + "-annotations" : fieldKey]).forEach((newdoc) => newarray.push(newdoc)); + }); + subDocs = newarray; + } + } + } + }); + return { strings: Array.from(valueSet.keys()), rtFields }; + } + /** + * Responds to clicking the check box in the flyout menu + */ + facetClick = (facetHeader: string) => { + const targetDoc = CollectionDockingView.Instance.props.Document; + const found = DocListCast(this.dataDoc[this.props.fieldKey]).findIndex(doc => doc.title === facetHeader); + if (found !== -1) { + (this.dataDoc[this.props.fieldKey] as List<Doc>).splice(found, 1); + const docFilter = Cast(targetDoc._docFilters, listSpec("string")); + if (docFilter) { + let index: number; + while ((index = docFilter.findIndex(item => item === facetHeader)) !== -1) { + docFilter.splice(index, 3); + } + } + const docRangeFilters = Cast(targetDoc._docRangeFilters, listSpec("string")); + if (docRangeFilters) { + let index: number; + while ((index = docRangeFilters.findIndex(item => item === facetHeader)) !== -1) { + docRangeFilters.splice(index, 3); + } + } + } else { + const allCollectionDocs = DocListCast((targetDoc.data as any)[0].data); + const facetValues = this.gatherFieldValues(targetDoc, facetHeader); + + let nonNumbers = 0; + let minVal = Number.MAX_VALUE, maxVal = -Number.MAX_VALUE; + facetValues.strings.map(val => { + const num = Number(val); + if (Number.isNaN(num)) { + nonNumbers++; + } else { + minVal = Math.min(num, minVal); + maxVal = Math.max(num, maxVal); + } + }); + let newFacet: Opt<Doc>; + if (facetHeader === "text" || facetValues.rtFields / allCollectionDocs.length > 0.1) { + newFacet = Docs.Create.TextDocument("", { _width: 100, _height: 25, _stayInCollection: true, _hideContextMenu: true, treeViewExpandedView: "layout", title: facetHeader, treeViewOpen: true, forceActive: true, ignoreClick: true }); + Doc.GetProto(newFacet).type = DocumentType.COL; // forces item to show an open/close button instead ofa checkbox + newFacet._textBoxPadding = 4; + const scriptText = `setDocFilter(this?.target, "${facetHeader}", text, "match")`; + newFacet.onTextChanged = ScriptField.MakeScript(scriptText, { this: Doc.name, text: "string" }); + } else if (facetHeader !== "tags" && nonNumbers / facetValues.strings.length < .1) { + newFacet = Docs.Create.SliderDocument({ title: facetHeader, _stayInCollection: true, _hideContextMenu: true, treeViewExpandedView: "layout", treeViewOpen: true }); + const newFacetField = Doc.LayoutFieldKey(newFacet); + const ranged = Doc.readDocRangeFilter(targetDoc, facetHeader); + Doc.GetProto(newFacet).type = DocumentType.COL; // forces item to show an open/close button instead ofa checkbox + const extendedMinVal = minVal - Math.min(1, Math.abs(maxVal - minVal) * .05); + const extendedMaxVal = maxVal + Math.min(1, Math.abs(maxVal - minVal) * .05); + newFacet[newFacetField + "-min"] = ranged === undefined ? extendedMinVal : ranged[0]; + newFacet[newFacetField + "-max"] = ranged === undefined ? extendedMaxVal : ranged[1]; + Doc.GetProto(newFacet)[newFacetField + "-minThumb"] = extendedMinVal; + Doc.GetProto(newFacet)[newFacetField + "-maxThumb"] = extendedMaxVal; + const scriptText = `setDocFilterRange(this?.target, "${facetHeader}", range)`; + newFacet.onThumbChanged = ScriptField.MakeScript(scriptText, { this: Doc.name, range: "number" }); + } else { + newFacet = new Doc(); + newFacet.sytem = true; + newFacet.title = facetHeader; + newFacet.treeViewOpen = true; + newFacet.type = DocumentType.COL; + const capturedVariables = { layoutDoc: targetDoc, _stayInCollection: true, _hideContextMenu: true, dataDoc: (targetDoc.data as any)[0][DataSym] }; + newFacet.data = ComputedField.MakeFunction(`readFacetData(layoutDoc, "${facetHeader}")`, {}, capturedVariables); + } + newFacet && Doc.AddDocToList(this.dataDoc, this.props.fieldKey, newFacet); + } + } + filterBackground = () => "rgba(105, 105, 105, 0.432)"; + get ignoreFields() { return ["_docFilters", "_docRangeFilters"]; } // this makes the tree view collection ignore these filters (otherwise, the filters would filter themselves) + @computed get scriptField() { + const scriptText = "setDocFilter(this?.target, heading, this.title, checked)"; + const script = ScriptField.MakeScript(scriptText, { this: Doc.name, heading: "string", checked: "string", containingTreeView: Doc.name }); + return script ? () => script : undefined; + } + + render() { + const facetCollection = this.props.Document.proto as Doc; + const flyout = <div className="filterBox-flyout" style={{ width: `100%`, height: this.props.PanelHeight() - 30 }} onWheel={e => e.stopPropagation()}> + {this._allFacets.map(facet => <label className="filterBox-flyout-facet" key={`${facet}`} onClick={e => this.facetClick(facet)}> + <input type="checkbox" onChange={e => { }} checked={DocListCast(this.props.Document[this.props.fieldKey]).some(d => d.title === facet)} /> + <span className="checkmark" /> + {facet} + </label>)} + </div>; + + return this.props.dontRegisterView ? (null) : <div className="filterBox-treeView" style={{ width: "100%" }}> + <div className="filterBox-addFacet" style={{ width: "100%" }} onPointerDown={e => e.stopPropagation()}> + <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout}> + <div className="filterBox-addFacetButton"> + <FontAwesomeIcon icon={"edit"} size={"lg"} /> + <span className="filterBox-span">Choose Facets</span> + </div> + </Flyout> + </div> + <div className="filterBox-tree" key="tree"> + <CollectionTreeView + PanelPosition={""} + Document={facetCollection} + DataDoc={Doc.GetProto(facetCollection)} + fieldKey={`${this.props.fieldKey}`} + CollectionView={undefined} + docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + ContainingCollectionDoc={this.props.ContainingCollectionDoc} + ContainingCollectionView={this.props.ContainingCollectionView} + PanelWidth={this.props.PanelWidth} + PanelHeight={this.props.PanelHeight} + LibraryPath={emptyPath} + rootSelected={this.props.rootSelected} + renderDepth={1} + dropAction={this.props.dropAction} + ScreenToLocalTransform={this.props.ScreenToLocalTransform} + addDocTab={returnFalse} + pinToPres={returnFalse} + isSelected={returnFalse} + select={returnFalse} + bringToFront={emptyFunction} + active={this.props.active} + whenActiveChanged={returnFalse} + treeViewHideTitle={true} + ContentScaling={returnOne} + focus={returnFalse} + treeViewHideHeaderFields={true} + onCheckedClick={this.scriptField} + ignoreFields={this.ignoreFields} + annotationsKey={""} + dontRegisterView={true} + backgroundColor={this.filterBackground} + moveDocument={returnFalse} + removeDocument={returnFalse} + addDocument={returnFalse} /> + </div> + </div>; + } +} + +Scripting.addGlobal(function determineCheckedState(layoutDoc: Doc, facetHeader: string, facetValue: string) { + const docFilters = Cast(layoutDoc._docFilters, listSpec("string"), []); + for (let i = 0; i < docFilters.length; i += 3) { + const [header, value, state] = docFilters.slice(i, i + 3); + if (header === facetHeader && value === facetValue) { + return state; + } + } + return undefined; +}); +Scripting.addGlobal(function readFacetData(layoutDoc: Doc, facetHeader: string) { + const allCollectionDocs = DocListCast(CollectionDockingView.Instance?.props.Document.allDocuments); + const set = new Set<string>(); + if (facetHeader === "tags") allCollectionDocs.forEach(child => Field.toString(child[facetHeader] as Field).split(":").forEach(key => set.add(key))); + else allCollectionDocs.forEach(child => set.add(Field.toString(child[facetHeader] as Field))); + const facetValues = Array.from(set).filter(v => v); + + let nonNumbers = 0; + facetValues.map(val => Number.isNaN(Number(val)) && nonNumbers++); + const facetValueDocSet = (nonNumbers / facetValues.length > .1 ? facetValues.sort() : facetValues.sort((n1: string, n2: string) => Number(n1) - Number(n2))).map(facetValue => { + const doc = new Doc(); + doc.system = true; + doc.title = facetValue.toString(); + doc.treeViewChecked = ComputedField.MakeFunction("determineCheckedState(layoutDoc, facetHeader, facetValue)", {}, { layoutDoc, facetHeader, facetValue }); + return doc; + }); + return new List<Doc>(facetValueDocSet); +});
\ No newline at end of file diff --git a/src/client/views/nodes/FontIconBox.scss b/src/client/views/nodes/FontIconBox.scss index 75bc90d7a..33ac85a0e 100644 --- a/src/client/views/nodes/FontIconBox.scss +++ b/src/client/views/nodes/FontIconBox.scss @@ -34,12 +34,14 @@ } } +.menuButton-circle, .menuButton-round { border-radius: 100%; background-color: black; + padding: 0; .fontIconBox-label { - margin-left: -10px; // button padding is 10px; + //margin-left: -10px; // button padding is 10px; bottom: 0; position: absolute; } @@ -52,7 +54,6 @@ .menuButton-square { padding-top: 3px; padding-bottom: 3px; - padding-left: 5px; .fontIconBox-label { border-radius: 0px; @@ -62,9 +63,11 @@ } .menuButton, +.menuButton-circle, .menuButton-round, .menuButton-square { - width: 100%; + margin-left: -5%; + width: 110%; height: 100%; pointer-events: all; touch-action: none; @@ -72,11 +75,7 @@ .menuButton-wrap { touch-action: none; border-radius: 8px; - - // &:hover { - // background: rgb(61, 61, 61); - // cursor: pointer; - // } + width: 100%; } .menuButton-icon-square { @@ -89,4 +88,13 @@ width: 95% !important; height: 95%; } +} +.menuButton-round { + width: 100%; + svg { + width: 50% !important; + height: 50%; + position: relative; + bottom: 2px; + } }
\ No newline at end of file diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx index fd71876b0..6e96513c7 100644 --- a/src/client/views/nodes/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox.tsx @@ -39,20 +39,18 @@ export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>( showTemplate = (): void => { const dragFactory = Cast(this.layoutDoc.dragFactory, Doc, null); - dragFactory && this.props.addDocTab(dragFactory, "onRight"); - } - dragAsTemplate = (): void => { - this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)'); - } - useAsPrototype = (): void => { - this.layoutDoc.onDragStart = ScriptField.MakeFunction('makeDelegate(this.dragFactory, true)'); + dragFactory && this.props.addDocTab(dragFactory, "add:right"); } + dragAsTemplate = (): void => { this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)'); }; + useAsPrototype = (): void => { this.layoutDoc.onDragStart = ScriptField.MakeFunction('makeDelegate(this.dragFactory, true)'); }; specificContextMenu = (): void => { - const cm = ContextMenu.Instance; - cm.addItem({ description: "Show Template", event: this.showTemplate, icon: "tag" }); - cm.addItem({ description: "Use as Render Template", event: this.dragAsTemplate, icon: "tag" }); - cm.addItem({ description: "Use as Prototype", event: this.useAsPrototype, icon: "tag" }); + if (!Doc.UserDoc().noviceMode) { + const cm = ContextMenu.Instance; + cm.addItem({ description: "Show Template", event: this.showTemplate, icon: "tag" }); + cm.addItem({ description: "Use as Render Template", event: this.dragAsTemplate, icon: "tag" }); + cm.addItem({ description: "Use as Prototype", event: this.useAsPrototype, icon: "tag" }); + } } componentWillUnmount() { @@ -63,16 +61,19 @@ export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>( const label = StrCast(this.rootDoc.label, StrCast(this.rootDoc.title)); const color = StrCast(this.layoutDoc.color, this._foregroundColor); const backgroundColor = StrCast(this.layoutDoc._backgroundColor, StrCast(this.rootDoc.backgroundColor, this.props.backgroundColor?.(this.rootDoc, this.props.renderDepth))); - const shape = StrCast(this.layoutDoc.iconShape, "round"); - + const shape = StrCast(this.layoutDoc.iconShape, label ? "round" : "circle"); + const icon = StrCast(this.dataDoc.icon, "user") as any; + const presSize = shape === 'round' ? 25 : 30; + const presTrailsIcon = <img src={`/assets/${"presTrails.png"}`} + style={{ width: presSize, height: presSize, filter: `invert(${color === "white" ? "100%" : "0%"})`, marginBottom: "5px" }} />; const button = <button className={`menuButton-${shape}`} ref={this._ref} onContextMenu={this.specificContextMenu} style={{ boxShadow: this.layoutDoc.ischecked ? `4px 4px 12px black` : undefined, backgroundColor: this.layoutDoc.iconShape === "square" ? backgroundColor : "", }}> <div className="menuButton-wrap"> - {<FontAwesomeIcon className={`menuButton-icon-${shape}`} icon={StrCast(this.dataDoc.icon, "user") as any} color={color} - size={this.layoutDoc.iconShape === "square" ? "sm" : "lg"} />} + {icon === 'pres-trail' ? presTrailsIcon : <FontAwesomeIcon className={`menuButton-icon-${shape}`} icon={icon} color={color} + size={this.layoutDoc.iconShape === "square" ? "sm" : "sm"} />} {!label ? (null) : <div className="fontIconBox-label" style={{ color, backgroundColor }}> {label} </div>} {this.props.Document.watchedDocuments ? <FontIconBadge collection={Cast(this.props.Document.watchedDocuments, Doc, null)} /> : (null)} </div> diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 216c5f39e..164456cfb 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,8 +1,5 @@ -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faEye } from '@fortawesome/free-regular-svg-icons'; -import { faAsterisk, faBrain, faFileAudio, faImage, faPaintBrush } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable, runInAction } from 'mobx'; +import { action, computed, observable, runInAction, reaction, IReactionDisposer } from 'mobx'; import { observer } from "mobx-react"; import { DataSym, Doc, DocListCast, HeightSym, WidthSym } from '../../../fields/Doc'; import { documentSchema } from '../../../fields/documentSchemas'; @@ -14,7 +11,8 @@ import { ComputedField } from '../../../fields/ScriptField'; import { Cast, NumCast, StrCast } from '../../../fields/Types'; import { AudioField, ImageField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, returnOne, Utils, returnZero } from '../../../Utils'; +import { emptyFunction, returnOne, returnZero, Utils, OmitKeys } from '../../../Utils'; +import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices'; import { Docs } from '../../documents/Documents'; import { Networking } from '../../Network'; @@ -24,22 +22,16 @@ import { ContextMenu } from "../../views/ContextMenu"; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { ContextMenuProps } from '../ContextMenuItem'; import { ViewBoxAnnotatableComponent } from '../DocComponent'; -import FaceRectangles from './FaceRectangles'; +import { FaceRectangles } from './FaceRectangles'; import { FieldView, FieldViewProps } from './FieldView'; import "./ImageBox.scss"; import React = require("react"); -import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; -const requestImageSize = require('../../util/request-image-size'); const path = require('path'); const { Howl } = require('howler'); -library.add(faImage, faEye as any, faPaintBrush, faBrain); -library.add(faFileAudio, faAsterisk); - - export const pageSchema = createSchema({ - curPage: "number", + _curPage: "number", fitWidth: "boolean", googlePhotosUrl: "string", googlePhotosTags: "string" @@ -70,15 +62,33 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ImageBox, fieldKey); } private _imgRef: React.RefObject<HTMLImageElement> = React.createRef(); private _dropDisposer?: DragManager.DragDropDisposer; + private _pathDisposer?: IReactionDisposer; @observable private _audioState = 0; @observable static _showControls: boolean; @observable uploadIcon = uploadIcons.idle; protected createDropTarget = (ele: HTMLDivElement) => { - this._dropDisposer && this._dropDisposer(); + this._dropDisposer?.(); ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.props.Document)); } + componentDidMount() { + this._pathDisposer = reaction(() => ({ nativeSize: this.nativeSize, width: this.layoutDoc[WidthSym]() }), + ({ nativeSize, width }) => { + if (this.props.NativeWidth?.() !== 0 || !this.layoutDoc._height) { + this.layoutDoc._nativeWidth = nativeSize.nativeWidth; + this.layoutDoc._nativeHeight = nativeSize.nativeHeight; + this.layoutDoc._nativeOrientation = nativeSize.nativeOrientation; + this.layoutDoc._height = width * nativeSize.nativeHeight / nativeSize.nativeWidth; + } + }, + { fireImmediately: true }); + } + + componentWillUnmount() { + this._pathDisposer?.(); + } + @undoBatch @action drop = (e: Event, de: DragManager.DropEvent) => { @@ -111,23 +121,17 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD }).then(function (stream) { gumStream = stream; recorder = new MediaRecorder(stream); - recorder.ondataavailable = async function (e: any) { - const formData = new FormData(); - formData.append("file", e.data); - const res = await fetch(Utils.prepend("/uploadFormData"), { - method: 'POST', - body: formData - }); - const files = await res.json(); - const url = Utils.prepend(files[0].path); - // upload to server with known URL - const audioDoc = Docs.Create.AudioDocument(url, { title: "audio test", _width: 200, _height: 32 }); - audioDoc.treeViewExpandedView = "layout"; - const audioAnnos = Cast(this.dataDoc[this.fieldKey + "-audioAnnotations"], listSpec(Doc)); - if (audioAnnos === undefined) { - this.dataDoc[this.fieldKey + "-audioAnnotations"] = new List([audioDoc]); - } else { - audioAnnos.push(audioDoc); + recorder.ondataavailable = async (e: any) => { + const [{ result }] = await Networking.UploadFilesToServer(e.data); + if (!(result instanceof Error)) { + const audioDoc = Docs.Create.AudioDocument(Utils.prepend(result.accessPaths.agnostic.client), { title: "audio test", _width: 200, _height: 32 }); + audioDoc.treeViewExpandedView = "layout"; + const audioAnnos = Cast(self.dataDoc[self.fieldKey + "-audioAnnotations"], listSpec(Doc)); + if (audioAnnos === undefined) { + self.dataDoc[self.fieldKey + "-audioAnnotations"] = new List([audioDoc]); + } else { + audioAnnos.push(audioDoc); + } } }; runInAction(() => self._audioState = 2); @@ -158,23 +162,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD if (field) { const funcs: ContextMenuProps[] = []; funcs.push({ description: "Rotate Clockwise 90", event: this.rotate, icon: "expand-arrows-alt" }); - funcs.push({ description: "Make Background", event: () => { this.layoutDoc.isBackground = true; this.props.bringToFront?.(this.rootDoc); }, icon: "expand-arrows-alt" }); + funcs.push({ description: "Make Background", event: () => { this.layoutDoc._isBackground = true; this.props.bringToFront?.(this.rootDoc); }, icon: "expand-arrows-alt" }); if (!Doc.UserDoc().noviceMode) { funcs.push({ description: "Export to Google Photos", event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: "caret-square-right" }); funcs.push({ description: "Copy path", event: () => Utils.CopyText(field.url.href), icon: "expand-arrows-alt" }); - // funcs.push({ - // description: "Reset Native Dimensions", event: action(async () => { - // const curNW = NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"]); - // const curNH = NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"]); - // if (this.props.PanelWidth() / this.props.PanelHeight() > curNW / curNH) { - // this.dataDoc[this.fieldKey + "-nativeWidth"] = this.props.PanelHeight() * curNW / curNH; - // this.dataDoc[this.fieldKey + "-nativeHeight"] = this.props.PanelHeight(); - // } else { - // this.dataDoc[this.fieldKey + "-nativeWidth"] = this.props.PanelWidth(); - // this.dataDoc[this.fieldKey + "-nativeHeight"] = this.props.PanelWidth() * curNH / curNW; - // } - // }), icon: "expand-arrows-alt" - // }); const existingAnalyze = ContextMenu.Instance?.findByDescription("Analyzers..."); const modes: ContextMenuProps[] = existingAnalyze && "subitems" in existingAnalyze ? existingAnalyze.subitems : []; @@ -219,15 +210,12 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD choosePath(url: URL) { const lower = url.href.toLowerCase(); - if (url.protocol === "data") { - return url.href; - } else if (url.href.indexOf(window.location.origin) === -1) { - return Utils.CorsProxy(url.href); - } else if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower)) { - return url.href;//Why is this here - } + if (url.protocol === "data") return url.href; + if (url.href.indexOf(window.location.origin) === -1) return Utils.CorsProxy(url.href); + if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower)) return url.href; //Why is this here + const ext = path.extname(url.href); - this._curSuffix = this.props.renderDepth < 1 ? "_o" : this.layoutDoc[WidthSym]() < 100 ? "_s" : "_m"; + this._curSuffix = this.props.renderDepth < 1 ? "_o" : this.props.PanelWidth() < 100 ? "_s" : "_m"; return url.href.replace(ext, this._curSuffix + ext); } @@ -253,45 +241,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD } _curSuffix = "_m"; - resize = (imgPath: string) => { - const basePath = imgPath.replace(/_[oms]./, ""); - const cachedNativeSize = { - width: basePath === this.dataDoc[this.fieldKey + "-path"] ? NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"]) : 0, - height: basePath === this.dataDoc[this.fieldKey + "-path"] ? NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"]) : 0, - }; - const docAspect = this.layoutDoc[HeightSym]() / this.layoutDoc[WidthSym](); - const cachedAspect = cachedNativeSize.height / cachedNativeSize.width; - if (!cachedNativeSize.width || !cachedNativeSize.height || Math.abs(NumCast(this.layoutDoc._width) / NumCast(this.layoutDoc._height) - cachedNativeSize.width / cachedNativeSize.height) > 0.05) { - if (!this.layoutDoc.isTemplateDoc || this.dataDoc !== this.layoutDoc) { - requestImageSize(imgPath).then(action((inquiredSize: any) => { - const rotation = NumCast(this.dataDoc[this.fieldKey + "-rotation"]) % 180; - const rotatedNativeSize = { width: inquiredSize.width, height: inquiredSize.height }; - if (inquiredSize.orientation === 6 || rotation === 90 || rotation === 270) { - rotatedNativeSize.width = inquiredSize.height; - rotatedNativeSize.height = inquiredSize.width; - } - const rotatedAspect = rotatedNativeSize.height / rotatedNativeSize.width; - if (this.layoutDoc[WidthSym]() && (!cachedNativeSize.width || !cachedNativeSize.height || Math.abs(1 - docAspect / rotatedAspect) > 0.1)) { - this.layoutDoc._height = this.layoutDoc[WidthSym]() * rotatedAspect; - this.dataDoc[this.fieldKey + "-nativeWidth"] = this.layoutDoc._nativeWidth = this.layoutDoc._width; - this.dataDoc[this.fieldKey + "-nativeHeight"] = this.layoutDoc._nativeHeight = this.layoutDoc._height; - this.dataDoc[this.fieldKey + "-path"] = basePath; - } - })).catch(console.log); - } else if (Math.abs(1 - docAspect / cachedAspect) > 0.1) { - this.layoutDoc._width = this.layoutDoc[WidthSym]() || cachedNativeSize.width; - this.layoutDoc._height = this.layoutDoc[WidthSym]() * cachedAspect; - } - } else if (this.layoutDoc._nativeWidth !== cachedNativeSize.width || this.layoutDoc._nativeHeight !== cachedNativeSize.height) { - !(this.layoutDoc[StrCast(this.layoutDoc.layoutKey)] instanceof Doc) && setTimeout(() => { - if (!(this.layoutDoc[StrCast(this.layoutDoc.layoutKey)] instanceof Doc)) { - this.layoutDoc._nativeWidth = cachedNativeSize.width; - this.layoutDoc._nativeHeight = cachedNativeSize.height; - } - }, 0); - } - } - @action onPointerEnter = () => { const self = this; @@ -371,10 +320,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD @computed get nativeSize() { TraceMobx(); - const pw = this.props.PanelWidth?.() || 50; - const nativeWidth = NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"], pw); + const nativeWidth = NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"], 500); const nativeHeight = NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"], 1); - return { nativeWidth, nativeHeight }; + const nativeOrientation = NumCast(this.dataDoc[this.fieldKey + "-nativeOrientation"], 1); + return { nativeWidth, nativeHeight, nativeOrientation }; } // this._curSuffix = ""; @@ -395,11 +344,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD const srcpath = this.paths[0]; const fadepath = this.paths[Math.min(1, this.paths.length - 1)]; - const { nativeWidth, nativeHeight } = this.nativeSize; + const { nativeWidth, nativeHeight, nativeOrientation } = this.nativeSize; const rotation = NumCast(this.dataDoc[this.fieldKey + "-rotation"]); - const aspect = (rotation % 180) ? nativeHeight / nativeWidth : 1; - const shift = (rotation % 180) ? (nativeHeight - nativeWidth) * (1 - 1 / aspect) : 0; - this.resize(srcpath); + const aspect = rotation % 180 ? nativeHeight / nativeWidth : 1; let transformOrigin = "center center"; let transform = `translate(0%, 0%) rotate(${rotation}deg) scale(${aspect})`; if (rotation === 90 || rotation === -270) { @@ -437,7 +384,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD > <FontAwesomeIcon className="imageBox-audioFont" style={{ color: [DocListCast(this.dataDoc[this.fieldKey + "-audioAnnotations"]).length ? "blue" : "gray", "green", "red"][this._audioState] }} - icon={!DocListCast(this.dataDoc[this.fieldKey + "-audioAnnotations"]).length ? "microphone" : faFileAudio} size="sm" /> + icon={!DocListCast(this.dataDoc[this.fieldKey + "-audioAnnotations"]).length ? "microphone" : "file-audio"} size="sm" /> </div>} {this.considerDownloadIcon} {this.considerGooglePhotosLink()} @@ -465,15 +412,13 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD transform: this.props.PanelWidth() ? undefined : `scale(${this.props.ContentScaling()})`, width: this.props.PanelWidth() ? undefined : `${100 / this.props.ContentScaling()}%`, height: this.props.PanelWidth() ? undefined : `${100 / this.props.ContentScaling()}%`, - pointerEvents: this.layoutDoc.isBackground ? "none" : undefined, + pointerEvents: this.layoutDoc._isBackground ? "none" : undefined, borderRadius: `${Number(StrCast(this.layoutDoc.borderRounding).replace("px", "")) / this.props.ContentScaling()}px` }} > - <CollectionFreeFormView {...this.props} + <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} forceScaling={true} PanelHeight={this.props.PanelHeight} PanelWidth={this.props.PanelWidth} - NativeHeight={returnZero} - NativeWidth={returnZero} annotationsKey={this.annotationKey} isAnnotationOverlay={true} focus={this.props.focus} @@ -489,6 +434,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD ScreenToLocalTransform={this.screenToLocalTransform} renderDepth={this.props.renderDepth + 1} docFilters={this.props.docFilters} + docRangeFilters={this.props.docRangeFilters} + searchFilterDocs={this.props.searchFilterDocs} ContainingCollectionDoc={this.props.ContainingCollectionDoc}> {this.contentFunc} </CollectionFreeFormView> diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index b732f5f83..c5ff42a1a 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -230,7 +230,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { openItems.push({ description: "Default Perspective", event: () => { this.props.addDocTab(this.props.Document, "close"); - this.props.addDocTab(this.fieldDocToLayout, "onRight"); + this.props.addDocTab(this.fieldDocToLayout, "add:right"); }, icon: "image" }); !open && cm.addItem({ description: "Change Perspective...", subitems: openItems, icon: "external-link-alt" }); diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index 4568a6b16..2e2319447 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -1,7 +1,7 @@ import { action, observable } from 'mobx'; import { observer } from "mobx-react"; import { Doc, Field, Opt } from '../../../fields/Doc'; -import { emptyFunction, returnFalse, returnOne, returnZero, returnEmptyFilter } from '../../../Utils'; +import { emptyFunction, returnFalse, returnOne, returnZero, returnEmptyFilter, returnEmptyDoclist } from '../../../Utils'; import { Docs } from '../../documents/Documents'; import { Transform } from '../../util/Transform'; import { undoBatch } from '../../util/UndoManager'; @@ -46,7 +46,7 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { if (value instanceof Doc) { e.stopPropagation(); e.preventDefault(); - ContextMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(value, { _width: 300, _height: 300 }), "onRight"), icon: "layer-group" }); + ContextMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(value, { _width: 300, _height: 300 }), "add:right"), icon: "layer-group" }); ContextMenu.Instance.displayMenu(e.clientX, e.clientY); } } @@ -56,7 +56,9 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { Document: this.props.doc, DataDoc: this.props.doc, LibraryPath: [], - docFilters:returnEmptyFilter, + docFilters: returnEmptyFilter, + docRangeFilters: returnEmptyFilter, + searchFilterDocs: returnEmptyDoclist, ContainingCollectionView: undefined, ContainingCollectionDoc: undefined, fieldKey: this.props.keyName, @@ -70,8 +72,6 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { whenActiveChanged: emptyFunction, ScreenToLocalTransform: Transform.Identity, focus: emptyFunction, - NativeHeight: returnZero, - NativeWidth: returnZero, PanelWidth: this.props.PanelWidth, PanelHeight: this.props.PanelHeight, addDocTab: returnFalse, diff --git a/src/client/views/nodes/LabelBox.scss b/src/client/views/nodes/LabelBox.scss index b605df262..109a02df4 100644 --- a/src/client/views/nodes/LabelBox.scss +++ b/src/client/views/nodes/LabelBox.scss @@ -8,6 +8,7 @@ } .labelBox-mainButton { + max-width: 100%; width: fit-content; height: max-content; border-radius: inherit; diff --git a/src/client/views/nodes/LinkAnchorBox.scss b/src/client/views/nodes/LinkAnchorBox.scss index 42ef2958e..62ee9513c 100644 --- a/src/client/views/nodes/LinkAnchorBox.scss +++ b/src/client/views/nodes/LinkAnchorBox.scss @@ -22,6 +22,11 @@ padding-left: 2px; padding-top: 1px; } + .linkAnchorBox-button { + pointer-events: all; + position: relative; + display: inline-block; + } } .linkAnchorBox-cont-small { diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx index 50b2af0d7..ec8c43ab4 100644 --- a/src/client/views/nodes/LinkAnchorBox.tsx +++ b/src/client/views/nodes/LinkAnchorBox.tsx @@ -73,7 +73,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch anchorContainerDoc && this.props.bringToFront(anchorContainerDoc, false); if (anchorContainerDoc && !this.layoutDoc.onClick && !this._isOpen) { this._timeout = setTimeout(action(() => { - DocumentManager.Instance.FollowLink(this.rootDoc, anchorContainerDoc, document => this.props.addDocTab(document, StrCast(this.layoutDoc.linkOpenLocation, e.altKey ? "inTab" : "onRight")), false); + DocumentManager.Instance.FollowLink(this.rootDoc, anchorContainerDoc, (doc, where) => this.props.addDocTab(doc, where), false); this._editing = false; }), 300 - (Date.now() - this._lastTap)); } @@ -87,14 +87,14 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch } openLinkDocOnRight = (e: React.MouseEvent) => { - this.props.addDocTab(this.rootDoc, "onRight"); + this.props.addDocTab(this.rootDoc, "add:right"); } openLinkTargetOnRight = (e: React.MouseEvent) => { const alias = Doc.MakeAlias(Cast(this.layoutDoc[this.fieldKey], Doc, null)); alias.isLinkButton = undefined; - alias.isBackground = undefined; + alias._isBackground = undefined; alias.layoutKey = "layout"; - this.props.addDocTab(alias, "onRight"); + this.props.addDocTab(alias, "add:right"); } @action openLinkEditor = action((e: React.MouseEvent) => { @@ -148,7 +148,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch }} > {!this._editing && !this._forceOpen ? (null) : <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout} open={this._forceOpen ? true : undefined} onOpen={() => this._isOpen = true} onClose={action(() => this._isOpen = this._forceOpen = this._editing = false)}> - <span className="parentDocumentSelector-button" > + <span className="linkAnchorBox-button" > <FontAwesomeIcon icon={"eye"} size={"lg"} /> </span> </Flyout>} diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx index 532e7dc15..64ae1051b 100644 --- a/src/client/views/nodes/LinkBox.tsx +++ b/src/client/views/nodes/LinkBox.tsx @@ -22,10 +22,10 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps, LinkDocument>( <CollectionTreeView {...this.props} ChromeHeight={returnZero} overrideDocuments={[this.dataDoc]} - NativeHeight={returnZero} - NativeWidth={returnZero} ignoreFields={Cast(this.props.Document.linkBoxExcludedKeys, listSpec("string"), null)} annotationsKey={""} + dontRegisterView={true} + renderDepth={this.props.renderDepth + 1} CollectionView={undefined} addDocument={returnFalse} removeDocument={returnFalse} diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx index c4481b213..d14157c09 100644 --- a/src/client/views/nodes/LinkDocPreview.tsx +++ b/src/client/views/nodes/LinkDocPreview.tsx @@ -3,7 +3,7 @@ import { observer } from "mobx-react"; import wiki from "wikijs"; import { Doc, DocCastAsync, HeightSym, Opt, WidthSym } from "../../../fields/Doc"; import { Cast, FieldValue, NumCast } from "../../../fields/Types"; -import { emptyFunction, emptyPath, returnEmptyFilter, returnFalse, returnOne, returnZero } from "../../../Utils"; +import { emptyFunction, emptyPath, returnEmptyFilter, returnFalse, returnOne, returnZero, returnEmptyDoclist } from "../../../Utils"; import { Docs } from "../../documents/Documents"; import { DocumentManager } from "../../util/DocumentManager"; import { Transform } from "../../util/Transform"; @@ -16,12 +16,13 @@ interface Props { linkDoc?: Doc; linkSrc?: Doc; href?: string; - backgroundColor: (doc: Doc, renderDepth: number) => string; + backgroundColor: (doc: Opt<Doc>, renderDepth: number) => string; addDocTab: (document: Doc, where: string) => boolean; location: number[]; } @observer export class LinkDocPreview extends React.Component<Props> { + static TargetDoc: Doc | undefined; @observable public static LinkInfo: Opt<{ linkDoc?: Doc; addDocTab: (document: Doc, where: string) => boolean, linkSrc: Doc; href?: string; Location: number[] }>; @observable _targetDoc: Opt<Doc>; @observable _toolTipText = ""; @@ -40,14 +41,16 @@ export class LinkDocPreview extends React.Component<Props> { async followDefault() { DocumentLinksButton.EditLink = undefined; LinkDocPreview.LinkInfo = undefined; - this._targetDoc ? DocumentManager.Instance.FollowLink(this.props.linkDoc, this._targetDoc, doc => this.props.addDocTab(doc, "onRight"), false) : null; + this._targetDoc ? DocumentManager.Instance.FollowLink(this.props.linkDoc, this._targetDoc, (doc, where) => this.props.addDocTab(doc, where), false) : null; } + componentWillUnmount() { LinkDocPreview.TargetDoc = undefined; } componentDidUpdate() { this.updatePreview(); } componentDidMount() { this.updatePreview(); } async updatePreview() { const linkDoc = this.props.linkDoc; const linkSrc = this.props.linkSrc; + LinkDocPreview.TargetDoc = undefined; if (this.props.href) { if (this.props.href.startsWith("https://en.wikipedia.org/wiki/")) { wiki().page(this.props.href.replace("https://en.wikipedia.org/wiki/", "")).then(page => page.summary().then(action(summary => this._toolTipText = summary.substring(0, 500)))); @@ -59,7 +62,7 @@ export class LinkDocPreview extends React.Component<Props> { const target = anchor?.annotationOn ? await DocCastAsync(anchor.annotationOn) : anchor; runInAction(() => { this._toolTipText = ""; - this._targetDoc = target; + LinkDocPreview.TargetDoc = this._targetDoc = target; if (anchor !== this._targetDoc && anchor && this._targetDoc) { this._targetDoc._scrollY = NumCast(anchor?.y); } @@ -69,13 +72,13 @@ export class LinkDocPreview extends React.Component<Props> { pointerDown = (e: React.PointerEvent) => { if (this.props.linkDoc && this.props.linkSrc) { DocumentManager.Instance.FollowLink(this.props.linkDoc, this.props.linkSrc, - (doc: Doc, followLinkLocation: string) => this.props.addDocTab(doc, e.ctrlKey ? "inTab" : followLinkLocation)); + (doc: Doc, followLinkLocation: string) => this.props.addDocTab(doc, e.ctrlKey ? "add" : followLinkLocation)); } else if (this.props.href) { - this.props.addDocTab(Docs.Create.WebDocument(this.props.href, { title: this.props.href, _width: 200, _height: 400, UseCors: true }), "onRight"); + this.props.addDocTab(Docs.Create.WebDocument(this.props.href, { _fitWidth: true, title: this.props.href, _width: 200, _height: 400, useCors: true }), "add:right"); } } - width = () => Math.min(225, NumCast(this._targetDoc?.[WidthSym](), 225)); - height = () => Math.min(225, NumCast(this._targetDoc?.[HeightSym](), 225)); + width = () => Math.min(225, NumCast(this._targetDoc?.[WidthSym](), 225)) - 16; + height = () => Math.min(225, NumCast(this._targetDoc?.[HeightSym](), 225)) - 16; @computed get targetDocView() { return !this._targetDoc ? <div style={{ @@ -101,17 +104,17 @@ export class LinkDocPreview extends React.Component<Props> { pinToPres={returnFalse} dontRegisterView={true} docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} ContainingCollectionDoc={undefined} ContainingCollectionView={undefined} - renderDepth={0} - PanelWidth={() => this.width() - 16} //Math.min(350, NumCast(target._width, 350))} - PanelHeight={() => this.height() - 16} //Math.min(250, NumCast(target._height, 250))} + renderDepth={-1} + PanelWidth={this.width} //Math.min(350, NumCast(target._width, 350))} + PanelHeight={this.height} //Math.min(250, NumCast(target._height, 250))} focus={emptyFunction} whenActiveChanged={returnFalse} bringToFront={returnFalse} ContentScaling={returnOne} - NativeWidth={returnZero} - NativeHeight={returnZero} backgroundColor={this.props.backgroundColor} />; } @@ -119,7 +122,7 @@ export class LinkDocPreview extends React.Component<Props> { return <div className="linkDocPreview" style={{ position: "absolute", left: this.props.location[0], - top: this.props.location[1], width: this.width(), height: this.height(), + top: this.props.location[1], width: this.width() + 16, height: this.height() + 16, zIndex: 1000, border: "8px solid white", borderRadius: "7px", boxShadow: "3px 3px 1.5px grey", diff --git a/src/client/views/nodes/MenuIconBox.scss b/src/client/views/nodes/MenuIconBox.scss deleted file mode 100644 index 1b72f5a8f..000000000 --- a/src/client/views/nodes/MenuIconBox.scss +++ /dev/null @@ -1,49 +0,0 @@ -.menuButton { - //padding: 7px; - padding-left: 7px; - width: 100%; - width: 60px; - height: 70px; - - .menuButton-wrap { - width: 45px; - /* padding: 5px; */ - touch-action: none; - background: black; - transform-origin: top left; - /* margin-bottom: 5px; */ - margin-top: 5px; - margin-right: 25px; - border-radius: 8px; - - &:hover { - background: rgb(61, 61, 61); - cursor: pointer; - } - } - - .menuButton-label { - color: white; - margin-right: 4px; - border-radius: 8px; - width: 42px; - position: relative; - text-align: center; - font-size: 8px; - margin-top: 1px; - letter-spacing: normal; - padding: 3px; - background-color: inherit; - } - - .menuButton-icon { - width: auto; - height: 35px; - padding: 5px; - } - - svg { - width: 95% !important; - height: 95%; - } -}
\ No newline at end of file diff --git a/src/client/views/nodes/MenuIconBox.tsx b/src/client/views/nodes/MenuIconBox.tsx deleted file mode 100644 index 5ed8a9b78..000000000 --- a/src/client/views/nodes/MenuIconBox.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { observer } from 'mobx-react'; -import * as React from 'react'; -import { createSchema, makeInterface } from '../../../fields/Schema'; -import { StrCast } from '../../../fields/Types'; -import { DocComponent } from '../DocComponent'; -import { FieldView, FieldViewProps } from './FieldView'; -import './MenuIconBox.scss'; -const MenuIconSchema = createSchema({ - icon: "string" -}); - -type MenuIconDocument = makeInterface<[typeof MenuIconSchema]>; -const MenuIconDocument = makeInterface(MenuIconSchema); -@observer -export class MenuIconBox extends DocComponent<FieldViewProps, MenuIconDocument>(MenuIconDocument) { - public static LayoutString(fieldKey: string) { return FieldView.LayoutString(MenuIconBox, fieldKey); } - _ref: React.RefObject<HTMLButtonElement> = React.createRef(); - - render() { - - const color = this.props.backgroundColor?.(this.props.Document, this.props.renderDepth) === "lightgrey" ? "black" : "white"; - const menuBTN = <div className="menuButton" style={{ backgroundColor: this.props.backgroundColor?.(this.props.Document, this.props.renderDepth) }}> - <div className="menuButton-wrap" - style={{ backgroundColor: this.props.backgroundColor?.(this.props.Document, this.props.renderDepth) }} > - <FontAwesomeIcon className="menuButton-icon" icon={StrCast(this.dataDoc.icon, "user") as any} color={color} size="lg" /> - <div className="menuButton-label" style={{ color: color }}> {this.dataDoc.title} </div> - </div> - </div>; - - return menuBTN; - } -}
\ No newline at end of file diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss index f2ab37984..e2bdd98cc 100644 --- a/src/client/views/nodes/PDFBox.scss +++ b/src/client/views/nodes/PDFBox.scss @@ -15,6 +15,8 @@ height: 100%; z-index: 1; pointer-events: none; + top: 0; + left: 0; .pdfBox-pageNums { display: flex; @@ -175,7 +177,7 @@ } .pdfBox-title-outer { - width: 150%; + width: 100%; height: 100%; position: relative; display: grid; @@ -187,14 +189,13 @@ color: lightgray; margin-top: auto; margin-bottom: auto; - transform-origin: 42% 15%; - width: 100%; - transform: rotate(55deg); - font-size: 200; + transform-origin: 40% 40%; + width: 125%; + transform: rotate(40deg) scale(0.8); + font-size: 80; padding: 5%; overflow: hidden; display: inline-block; - white-space: pre; text-overflow: ellipsis; text-align: center; } @@ -202,6 +203,9 @@ } .pdfBox { + width: 100%; + height: 100%; + pointer-events: none; .pdfViewerDash-text { .textLayer { span { @@ -211,8 +215,17 @@ } } +.pdfBox-background { + width: 100%; + height: 100%; + background: lightGray; +} + .pdfBox-interactive { + width: 100%; + height: 100%; pointer-events: all; + z-index: -1; .pdfViewerDash-text { .textLayer { span { diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 255a1b2d0..74b431bea 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -21,6 +21,8 @@ import { KeyCodes } from '../../util/KeyCodes'; import "./PDFBox.scss"; import React = require("react"); import { documentSchema } from '../../../fields/documentSchemas'; +import { CollectionViewType } from '../collections/CollectionView'; +import { TraceMobx } from '../../../fields/util'; type PdfDocument = makeInterface<[typeof documentSchema, typeof panZoomSchema, typeof pageSchema]>; const PdfDocument = makeInterface(documentSchema, panZoomSchema, pageSchema); @@ -99,20 +101,28 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum public search = (string: string, fwd: boolean) => { this._pdfViewer?.search(string, fwd); }; public prevAnnotation = () => { this._pdfViewer?.prevAnnotation(); }; public nextAnnotation = () => { this._pdfViewer?.nextAnnotation(); }; - public backPage = () => { this._pdfViewer!.gotoPage((this.Document.curPage || 1) - 1); }; - public forwardPage = () => { this._pdfViewer!.gotoPage((this.Document.curPage || 1) + 1); }; - public gotoPage = (p: number) => { this._pdfViewer!.gotoPage(p); }; + public backPage = () => { this.Document._curPage = (this.Document._curPage || 1) - 1; return true; }; + public forwardPage = () => { this.Document._curPage = (this.Document._curPage || 1) + 1; return true; }; + public gotoPage = (p: number) => { this.Document._curPage = p; }; @undoBatch onKeyDown = action((e: KeyboardEvent) => { + let processed = false; if (e.key === "f" && e.ctrlKey) { this._searching = true; setTimeout(() => this._searchRef.current && this._searchRef.current.focus(), 100); + processed = true; + } + if (e.key === "PageDown") processed = this.forwardPage(); + if (e.key === "PageUp") processed = this.backPage(); + if (e.target instanceof HTMLInputElement || this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Freeform) { + if (e.key === "ArrowDown" || e.key === "ArrowRight") processed = this.forwardPage(); + if (e.key === "ArrowUp" || e.key === "ArrowLeft") processed = this.backPage(); + } + if (processed) { e.stopImmediatePropagation(); e.preventDefault(); } - if (e.key === "PageDown" || e.key === "ArrowDown" || e.key === "ArrowRight") this.forwardPage(); - if (e.key === "PageUp" || e.key === "ArrowUp" || e.key === "ArrowLeft") this.backPage(); }); @undoBatch @@ -150,7 +160,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum </button> </>; const searchTitle = `${!this._searching ? "Open" : "Close"} Search Bar`; - const curPage = this.Document.curPage || 1; + const curPage = this.Document._curPage || 1; return !this.active() ? (null) : (<div className="pdfBox-ui" onKeyDown={e => e.keyCode === KeyCodes.BACKSPACE || e.keyCode === KeyCodes.DELETE ? e.stopPropagation() : true} onPointerDown={e => e.stopPropagation()} style={{ display: this.active() ? "flex" : "none" }}> @@ -177,12 +187,12 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum <div className="pdfBox-pageNums"> <input value={curPage} - onChange={e => this.gotoPage(Number(e.currentTarget.value))} + onChange={e => this.Document._curPage = Number(e.currentTarget.value)} style={{ width: `${curPage > 99 ? 4 : 3}ch`, pointerEvents: "all" }} onClick={action(() => this._pageControls = !this._pageControls)} /> {this._pageControls ? pageBtns : (null)} </div> - <div className="pdfBox-settingsCont" key="settings" onPointerDown={(e) => e.stopPropagation()}> + {/* <div className="pdfBox-settingsCont" key="settings" onPointerDown={(e) => e.stopPropagation()}> <button className="pdfBox-settingsButton" onClick={action(() => this._flyout = !this._flyout)} title="Open Annotation Settings" > <div className="pdfBox-settingsButton-arrow" style={{ transform: `scaleX(${this._flyout ? -1 : 1})` }} /> <div className="pdfBox-settingsButton-iconCont"> @@ -211,7 +221,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum </button> </div> </div> - </div> + </div> */} </div>); } @@ -227,12 +237,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum @computed get contentScaling() { return this.props.ContentScaling(); } @computed get renderTitleBox() { const classname = "pdfBox" + (this.active() ? "-interactive" : ""); - return <div className={classname} style={{ - width: !this.props.Document._fitWidth ? this.Document._nativeWidth || 0 : `${100 / this.contentScaling}%`, - //height adjusted for mobile (window.screen.width > 600) - height: !this.props.Document._fitWidth && (window.screen.width > 600) ? this.Document._nativeHeight || 0 : `${100 / this.contentScaling}%`, - transform: `scale(${this.contentScaling})` - }} > + return <div className={classname} > <div className="pdfBox-title-outer"> <strong className="pdfBox-title" >{this.props.Document.title}</strong> </div> @@ -241,27 +246,33 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum isChildActive = (outsideReaction?: boolean) => this._isChildActive; @computed get renderPdfView() { + TraceMobx(); const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField); return <div className={"pdfBox"} onContextMenu={this.specificContextMenu} style={{ height: this.props.Document._scrollTop && !this.Document._fitWidth && (window.screen.width > 600) ? NumCast(this.Document._height) * this.props.PanelWidth() / NumCast(this.Document._width) : undefined }}> + <div className="pdfBox-background"></div> <PDFViewer {...this.props} pdf={this._pdf!} url={pdfUrl!.url.pathname} active={this.props.active} loaded={this.loaded} setPdfViewer={this.setPdfViewer} ContainingCollectionView={this.props.ContainingCollectionView} renderDepth={this.props.renderDepth} PanelHeight={this.props.PanelHeight} PanelWidth={this.props.PanelWidth} - addDocTab={this.props.addDocTab} focus={this.props.focus} docFilters={this.props.docFilters} + addDocTab={this.props.addDocTab} focus={this.props.focus} searchFilterDocs={this.props.searchFilterDocs} + docFilters={this.props.docFilters} docRangeFilters={this.props.docRangeFilters} pinToPres={this.props.pinToPres} addDocument={this.addDocument} Document={this.props.Document} DataDoc={this.dataDoc} ContentScaling={this.props.ContentScaling} ScreenToLocalTransform={this.props.ScreenToLocalTransform} select={this.props.select} isSelected={this.props.isSelected} whenActiveChanged={this.whenActiveChanged} isChildActive={this.isChildActive} - fieldKey={this.props.fieldKey} startupLive={this._initialScale < 2.5 || this.props.Document._scrollTop ? true : false} /> + fieldKey={this.props.fieldKey} startupLive={true} /> {this.settingsPanel()} </div>; } _pdfjsRequested = false; render() { + TraceMobx(); const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField, null); - if (this.props.isSelected() || this.props.renderDepth === 0 || this.props.Document._scrollY !== undefined) this._everActive = true; - if (pdfUrl && (this._everActive || this.props.Document._scrollTop || (this.dataDoc[this.props.fieldKey + "-nativeWidth"] && this.props.ScreenToLocalTransform().Scale < 2.5))) { + if (true) {//this.props.isSelected() || (this.props.active() && this.props.renderDepth === 0) || this.props.Document._scrollY !== undefined) { + this._everActive = true; + } + if (pdfUrl && this._everActive) { if (pdfUrl instanceof PdfField && this._pdf) { return this.renderPdfView; } diff --git a/src/client/views/nodes/PresBox.scss b/src/client/views/nodes/PresBox.scss index c4d8f1a4f..7bc6c1dfd 100644 --- a/src/client/views/nodes/PresBox.scss +++ b/src/client/views/nodes/PresBox.scss @@ -144,6 +144,21 @@ $light-background: #ececec; grid-template-columns: repeat(auto-fit, 70px); } + .ribbon-colorBox { + cursor: pointer; + border: solid 1px black; + display: flex; + margin-left: 5px; + margin-top: 5px; + margin-bottom: 5px; + justify-content: center; + align-items: center; + height: 15px; + width: 15px; + padding: 0px; + transform: translate(2px, 3px); + } + .ribbon-property { font-size: 11; font-weight: 200; @@ -415,10 +430,38 @@ $light-background: #ececec; .ribbon-button { cursor: pointer; font-size: 10.5; + font-weight: 300; + height: 20; + background-color: #979797; + color: white; + display: flex; + margin-top: 5px; + margin-bottom: 5px; + border-radius: 5px; + margin-right: 5px; + width: max-content; + justify-content: center; + align-items: center; + padding-right: 10px; + padding-left: 10px; + } + + .ribbon-button:hover { + transform: scale(1.03); + transition: all 0.4s; + font-weight: 400; + opacity: 1; + color: white; + background-color: black; + } + + .ribbon-toggle { + cursor: pointer; + font-size: 10.5; font-weight: 200; height: 20; background-color: $light-background; - border: solid 1px black; + border: solid 1px rgba(0, 0, 0, 0.5); display: flex; margin-top: 5px; margin-bottom: 5px; @@ -431,12 +474,14 @@ $light-background: #ececec; padding-left: 10px; } - .ribbon-button.active { + .ribbon-toggle.active { background-color: #aedef8; } - .ribbon-button:hover { - background-color: lightgrey; + .ribbon-toggle:hover { + transform: scale(1.03); + transition: all 0.4s; + border: solid 1px rgba(0, 0, 0, 0.75); } svg.svg-inline--fa.fa-thumbtack.fa-w-12.toolbar-thumbtack { @@ -488,7 +533,7 @@ $light-background: #ececec; max-width: 200px; overflow: visible; - + .presBox-dropdownOption { cursor: pointer; font-size: 11; @@ -801,82 +846,6 @@ $light-background: #ececec; } - .presPanelOverlay { - background-color: #323232; - color: white; - border-radius: 5px; - grid-template-rows: 100%; - height: 25; - width: max-content; - min-width: max-content; - justify-content: space-evenly; - align-items: center; - display: flex; - position: absolute; - right: 10px; - transition: all 0.2s; - - .presPanel-button-text { - display: flex; - height: 20; - width: max-content; - font-family: Roboto; - font-weight: 400; - margin-left: 3px; - margin-right: 3px; - padding-right: 3px; - padding-left: 3px; - letter-spacing: normal; - border-radius: 5px; - align-items: center; - justify-content: center; - transition: all 0.3s; - } - - .presPanel-divider { - width: 0.5px; - height: 80%; - border-right: solid 1px #5a5a5a; - } - - .presPanel-button-frame { - justify-self: center; - align-self: center; - align-items: center; - display: grid; - grid-template-columns: auto auto auto; - justify-content: space-around; - font-size: 11; - margin-left: 7; - width: 30; - height: 85%; - background-color: rgba(91, 157, 221, 0.4); - border-radius: 5px; - } - - .presPanel-button { - cursor: pointer; - display: flex; - height: 20; - min-width: 20; - margin-left: 3px; - margin-right: 3px; - border-radius: 100%; - align-items: center; - justify-content: center; - transition: all 0.3s; - } - - .presPanel-button:hover { - background-color: #5a5a5a; - } - - .presPanel-button-text:hover { - background-color: #5a5a5a; - } - } - - .collectionViewBaseChrome-viewPicker { min-width: 50; @@ -952,80 +921,165 @@ $light-background: #ececec; } } -.miniPres:hover { - opacity: 1; -} - .miniPres { cursor: grab; position: absolute; - overflow: hidden; right: 10; top: 10; opacity: 0.1; transition: all 0.4s; - /* border: solid 1px; */ color: white; - /* box-shadow: black 0.4vw 0.4vw 0.8vw; */ - - .miniPresOverlay { - display: grid; - grid-template-columns: auto auto auto auto auto auto auto auto; - grid-template-rows: 100%; - height: 100%; - justify-items: center; - align-items: center; +} - .miniPres-button-text { - cursor: pointer; - display: flex; - height: 20; - font-weight: 400; - min-width: 100%; - border-radius: 5px; - align-items: center; - justify-content: center; - transition: all 0.3s; - } +.miniPres:hover { + opacity: 1; +} - .miniPres-button-frame { - justify-self: center; - align-self: center; - align-items: center; - display: grid; - grid-template-columns: auto auto auto; - justify-content: space-around; - font-size: 11; - margin-left: 7; - width: 30; - height: 85%; - background-color: rgba(91, 157, 221, 0.4); - border-radius: 5px; - } +.presPanelOverlay { + background-color: #323232; + color: white; + border-radius: 5px; + grid-template-rows: 100%; + height: 25; + width: max-content; + min-width: max-content; + justify-content: space-evenly; + align-items: center; + display: flex; + position: absolute; + right: 10px; + transition: all 0.2s; - .miniPres-divider { - width: 0.5px; - height: 80%; - border-right: solid 2px #5a5a5a; - } + .presPanel-button-text { + display: flex; + height: 20; + width: max-content; + font-family: Roboto; + font-weight: 400; + margin-left: 3px; + margin-right: 3px; + padding-right: 3px; + padding-left: 3px; + letter-spacing: normal; + border-radius: 5px; + align-items: center; + justify-content: center; + transition: all 0.3s; + } - .miniPres-button { - cursor: pointer; - display: flex; - height: 20; - min-width: 20; - border-radius: 100%; - align-items: center; - justify-content: center; - transition: all 0.3s; - } + .presPanel-divider { + width: 0.5px; + height: 80%; + border-right: solid 1px #5a5a5a; + } - .miniPres-button:hover { - background-color: #5a5a5a; - } + .presPanel-button-frame { + justify-self: center; + align-self: center; + align-items: center; + display: grid; + grid-template-columns: auto auto auto; + justify-content: space-around; + font-size: 11; + margin-left: 7; + width: 30; + height: 85%; + background-color: rgba(91, 157, 221, 0.4); + border-radius: 5px; + } - .miniPres-button-text:hover { - background-color: #5a5a5a; - } + .presPanel-button { + cursor: pointer; + display: flex; + height: 20; + min-width: 20; + margin-left: 3px; + margin-right: 3px; + border-radius: 100%; + align-items: center; + justify-content: center; + transition: all 0.3s; + } + + .presPanel-button:hover { + background-color: #5a5a5a; + } + + .presPanel-button-text:hover { + background-color: #5a5a5a; } } + +// .miniPres { +// cursor: grab; +// position: absolute; +// overflow: hidden; +// right: 10; +// top: 10; +// opacity: 0.1; +// transition: all 0.4s; +// /* border: solid 1px; */ +// color: white; +// /* box-shadow: black 0.4vw 0.4vw 0.8vw; */ + +// .miniPresOverlay { +// display: grid; +// grid-template-columns: auto auto auto auto auto auto auto auto auto auto; +// grid-template-rows: 100%; +// height: 100%; +// justify-items: center; +// align-items: center; + +// .miniPres-button-text { +// cursor: pointer; +// display: flex; +// height: 20; +// font-weight: 400; +// min-width: 100%; +// border-radius: 5px; +// align-items: center; +// justify-content: center; +// transition: all 0.3s; +// } + +// .miniPres-button-frame { +// justify-self: center; +// align-self: center; +// align-items: center; +// display: grid; +// grid-template-columns: auto auto auto; +// justify-content: space-around; +// font-size: 11; +// margin-left: 7; +// width: 30; +// height: 85%; +// background-color: rgba(91, 157, 221, 0.4); +// border-radius: 5px; +// } + +// .miniPres-divider { +// width: 0.5px; +// height: 80%; +// border-right: solid 2px #5a5a5a; +// } + +// .miniPres-button { +// cursor: pointer; +// display: flex; +// height: 20; +// min-width: 20; +// border-radius: 100%; +// align-items: center; +// justify-content: center; +// transition: all 0.3s; +// } + +// .miniPres-button:hover { +// background-color: #5a5a5a; +// } + +// .miniPres-button-text:hover { +// background-color: #5a5a5a; +// } +// } +// }
\ No newline at end of file diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx index 5fc76223d..7eca09f8c 100644 --- a/src/client/views/nodes/PresBox.tsx +++ b/src/client/views/nodes/PresBox.tsx @@ -1,34 +1,50 @@ import React = require("react"); import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Tooltip } from "@material-ui/core"; import { action, computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCast, DocCastAsync, WidthSym } from "../../../fields/Doc"; -import { InkTool } from "../../../fields/InkField"; -import { BoolCast, Cast, NumCast, StrCast, ScriptCast } from "../../../fields/Types"; -import { returnFalse, returnOne, numberRange, returnTrue } from "../../../Utils"; +import { ColorState, SketchPicker } from "react-color"; +import { Doc, DocCastAsync, DocListCast, DocListCastAsync } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; -import { DocumentManager } from "../../util/DocumentManager"; -import { undoBatch } from "../../util/UndoManager"; -import { CollectionDockingView, DockedFrameRenderer } from "../collections/CollectionDockingView"; -import { CollectionView, CollectionViewType } from "../collections/CollectionView"; -import { FieldView, FieldViewProps } from './FieldView'; -import { DocumentType } from "../../documents/DocumentTypes"; -import "./PresBox.scss"; -import { ViewBoxBaseComponent } from "../DocComponent"; -import { makeInterface, listSpec } from "../../../fields/Schema"; -import { Docs, DocUtils } from "../../documents/Documents"; +import { InkTool } from "../../../fields/InkField"; +import { List } from "../../../fields/List"; import { PrefetchProxy } from "../../../fields/Proxy"; +import { listSpec, makeInterface } from "../../../fields/Schema"; import { ScriptField } from "../../../fields/ScriptField"; +import { Cast, NumCast, StrCast } from "../../../fields/Types"; +import { returnFalse, returnOne, returnZero } from "../../../Utils"; +import { Docs } from "../../documents/Documents"; +import { DocumentType } from "../../documents/DocumentTypes"; +import { CurrentUserUtils } from "../../util/CurrentUserUtils"; +import { DocumentManager } from "../../util/DocumentManager"; import { Scripting } from "../../util/Scripting"; -import { CollectionFreeFormDocumentView } from "./CollectionFreeFormDocumentView"; -import { List } from "../../../fields/List"; -import { Tooltip } from "@material-ui/core"; -import { actionAsync } from "mobx-utils"; import { SelectionManager } from "../../util/SelectionManager"; +import { undoBatch } from "../../util/UndoManager"; +import { CollectionDockingView } from "../collections/CollectionDockingView"; +import { CollectionView, CollectionViewType } from "../collections/CollectionView"; +import { TabDocView } from "../collections/TabDocView"; +import { ViewBoxBaseComponent } from "../DocComponent"; import { AudioBox } from "./AudioBox"; -import { DocumentView } from "./DocumentView"; -import { SketchPicker, ColorState } from "react-color"; -import { CurrentUserUtils } from "../../util/CurrentUserUtils"; +import { CollectionFreeFormDocumentView } from "./CollectionFreeFormDocumentView"; +import { FieldView, FieldViewProps } from './FieldView'; +import "./PresBox.scss"; +import { VideoBox } from "./VideoBox"; + +export enum PresMovement { + Zoom = "zoom", + Pan = "pan", + Jump = "jump", + None = "none", +} + +export enum PresEffect { + Fade = "fade", + Flip = "flip", + Rotate = "rotate", + Bounce = "bounce", + Roll = "roll", + None = "none", +} type PresBoxSchema = makeInterface<[typeof documentSchema]>; const PresBoxDocument = makeInterface(documentSchema); @@ -51,16 +67,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @observable private transitionTools: boolean = false; @observable private newDocumentTools: boolean = false; @observable private progressivizeTools: boolean = false; - @observable private moreInfoTools: boolean = false; - @observable private playTools: boolean = false; @observable private presentTools: boolean = false; @observable private pathBoolean: boolean = false; - @observable private expandBoolean: boolean = false; @observable private openMovementDropdown: boolean = false; @observable private openEffectDropdown: boolean = false; - @computed get childDocs() { return DocListCast(this.dataDoc[this.fieldKey]); } @computed get itemIndex() { return NumCast(this.rootDoc._itemIndex); } + @computed get activeItem() { return Cast(this.childDocs[this.itemIndex], Doc, null); } + @computed get targetDoc() { return Cast(this.activeItem?.presentationTargetDoc, Doc, null); } @computed get presElement() { return Cast(Doc.UserDoc().presElement, Doc, null); } constructor(props: any) { super(props); @@ -79,35 +93,37 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> this.props.Document.presentationFieldKey = this.fieldKey; // provide info to the presElement script so that it can look up rendering information about the presBox } @computed get selectedDocumentView() { - if (SelectionManager.SelectedDocuments().length) { - return SelectionManager.SelectedDocuments()[0]; - } else if (PresBox.Instance._selectedArray.length) { - return DocumentManager.Instance.getDocumentView(PresBox.Instance.rootDoc); - } else { return undefined; } + if (SelectionManager.SelectedDocuments().length) return SelectionManager.SelectedDocuments()[0]; + if (PresBox.Instance && PresBox.Instance._selectedArray) return DocumentManager.Instance.getDocumentView(PresBox.Instance.rootDoc); + return undefined; } @computed get isPres(): boolean { + document.removeEventListener("keydown", this.keyEvents, true); if (this.selectedDoc?.type === DocumentType.PRES) { - document.removeEventListener("keydown", this.keyEvents, true); document.addEventListener("keydown", this.keyEvents, true); return true; - } else { - document.removeEventListener("keydown", this.keyEvents, true); - return false; } + return false; } @computed get selectedDoc() { return this.selectedDocumentView?.rootDoc; } componentWillUnmount() { document.removeEventListener("keydown", this.keyEvents, true); - this.turnOffEdit(); + // Turn of progressivize editors + this.turnOffEdit(true); } - componentDidMount() { + componentDidMount = async () => { this.rootDoc.presBox = this.rootDoc; this.rootDoc._forceRenderEngine = "timeline"; this.rootDoc._replacedChrome = "replaced"; this.layoutDoc.presStatus = "edit"; this.layoutDoc._gridGap = 5; + this.turnOffEdit(true); + DocListCastAsync((Doc.UserDoc().myPresentations as Doc).data).then(async pres => { + await Promise.all(pres!); + if (!DocListCast((Doc.UserDoc().myPresentations as Doc).data).includes(this.rootDoc)) Doc.AddDocToList(Doc.UserDoc().myPresentations as Doc, "data", this.rootDoc); + }); } updateCurrentPresentation = () => { @@ -123,37 +139,46 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> next = () => { this.updateCurrentPresentation(); const activeNext = Cast(this.childDocs[this.itemIndex + 1], Doc, null); - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const presTargetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); - const childDocs = DocListCast(presTargetDoc[Doc.LayoutFieldKey(presTargetDoc)]); - const currentFrame = Cast(presTargetDoc.currentFrame, "number", null); - const lastFrame = Cast(presTargetDoc.lastFrame, "number", null); - const curFrame = NumCast(presTargetDoc.currentFrame); + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; + const childDocs = DocListCast(targetDoc[Doc.LayoutFieldKey(targetDoc)]); + const currentFrame = Cast(targetDoc._currentFrame, "number", null); + const lastFrame = Cast(targetDoc.lastFrame, "number", null); + const curFrame = NumCast(targetDoc._currentFrame); let internalFrames: boolean = false; - if (presTargetDoc.presProgressivize || activeItem.zoomProgressivize || presTargetDoc.scrollProgressivize) internalFrames = true; + if (activeItem.presProgressivize || activeItem.zoomProgressivize || targetDoc.scrollProgressivize) internalFrames = true; // Case 1: There are still other frames and should go through all frames before going to next slide if (internalFrames && lastFrame !== undefined && curFrame < lastFrame) { - presTargetDoc._viewTransition = "all 1s"; - setTimeout(() => presTargetDoc._viewTransition = undefined, 1010); - presTargetDoc.currentFrame = curFrame + 1; - if (presTargetDoc.scrollProgressivize) CollectionFreeFormDocumentView.updateScrollframe(presTargetDoc, currentFrame); - if (presTargetDoc.presProgressivize) CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0, presTargetDoc); - else presTargetDoc.editing = true; - if (activeItem.zoomProgressivize) this.zoomProgressivizeNext(presTargetDoc); - // Case 2: Audio or video therefore wait to play the audio or video before moving on - } else if ((presTargetDoc.type === DocumentType.AUDIO) && !this._moveOnFromAudio && this.layoutDoc.presStatus !== 'auto') { - AudioBox.Instance.playFrom(0); - this._moveOnFromAudio = true; - // Case 3: No more frames in current doc and next slide is defined, therefore move to next slide + targetDoc._viewTransition = "all 1s"; + setTimeout(() => targetDoc._viewTransition = undefined, 1010); + // targetDoc._currentFrame = curFrame + 1; + this.nextKeyframe(targetDoc, activeItem); + // if (targetDoc.scrollProgressivize) CollectionFreeFormDocumentView.updateScrollframe(targetDoc, currentFrame); + if (activeItem.presProgressivize) CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0, targetDoc); + else targetDoc.editing = true; + // if (activeItem.zoomProgressivize) this.zoomProgressivizeNext(targetDoc); + // Case 2: 'Play on next' for audio or video therefore first navigate to the audio/video before it should be played + } else if ((targetDoc.type === DocumentType.AUDIO || targetDoc.type === DocumentType.VID) && !activeItem.playAuto && activeItem.playNow && this.layoutDoc.presStatus !== 'auto') { + if (targetDoc.type === DocumentType.AUDIO) AudioBox.Instance.playFrom(NumCast(activeItem.presStartTime)); + // if (targetDoc.type === DocumentType.VID) { VideoBox.Instance.Play() }; + activeItem.playNow = false; + // Case 3: No more frames in current doc and next slide is defined, therefore move to next slide } else if (this.childDocs[this.itemIndex + 1] !== undefined) { + if (activeNext.presPinView) setTimeout(() => this.selectPres(), 0); + else this.selectPres(); const nextSelected = this.itemIndex + 1; - if (presTargetDoc.type === DocumentType.AUDIO) AudioBox.Instance.pause(); - this.gotoDocument(nextSelected, this.itemIndex); + if (targetDoc.type === DocumentType.AUDIO) { if (AudioBox.Instance._ele) AudioBox.Instance.pause(); } + // if (targetDoc.type === DocumentType.VID) { if (AudioBox.Instance._ele) VideoBox.Instance.Pause(); } const targetNext = Cast(activeNext.presentationTargetDoc, Doc, null); - if (activeNext && targetNext.type === DocumentType.AUDIO && activeNext.playAuto) { - AudioBox.Instance.playFrom(0); - this._moveOnFromAudio = true; - } else this._moveOnFromAudio = false; + // If next slide is audio / video 'Play automatically' then the next slide should be played + if (activeNext && (targetNext.type === DocumentType.AUDIO || targetNext.type === DocumentType.VID) && activeNext.playAuto) { + console.log('play next automatically'); + if (targetNext.type === DocumentType.AUDIO) AudioBox.Instance.playFrom(NumCast(activeNext.presStartTime)); + // if (targetNext.type === DocumentType.VID) { VideoBox.Instance.Play() }; + } else if (targetNext.type === DocumentType.AUDIO || targetNext.type === DocumentType.VID) { activeNext.playNow = true; console.log('play next after it is navigated to'); } + this.gotoDocument(nextSelected, this.itemIndex); + } else if (this.childDocs[this.itemIndex + 1] === undefined && this.layoutDoc.presLoop) { + this.gotoDocument(0, this.itemIndex); } } @@ -166,19 +191,21 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @action back = () => { this.updateCurrentPresentation(); - const docAtCurrent = this.childDocs[this.itemIndex]; - const targetDoc = Cast(docAtCurrent.presentationTargetDoc, Doc, null); + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; const prevItem = Cast(this.childDocs[Math.max(0, this.itemIndex - 1)], Doc, null); const prevTargetDoc = Cast(prevItem.presentationTargetDoc, Doc, null); const lastFrame = Cast(targetDoc.lastFrame, "number", null); - const curFrame = NumCast(targetDoc.currentFrame); + const curFrame = NumCast(targetDoc._currentFrame); + if (prevItem.presPinView) setTimeout(() => this.selectPres(), 0); + else this.selectPres(); if (lastFrame !== undefined && curFrame >= 1) { - this.prevKeyframe(targetDoc, docAtCurrent); - } else if (docAtCurrent) { + this.prevKeyframe(targetDoc, activeItem); + } else if (activeItem) { let prevSelected = this.itemIndex; prevSelected = Math.max(0, prevSelected - 1); this.gotoDocument(prevSelected, this.itemIndex); - if (NumCast(prevTargetDoc.lastFrame) > 0) prevTargetDoc.currentFrame = NumCast(prevTargetDoc.lastFrame); + if (NumCast(prevTargetDoc.lastFrame) > 0) prevTargetDoc._currentFrame = NumCast(prevTargetDoc.lastFrame); } } @@ -189,9 +216,27 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> Doc.UnBrushAllDocs(); if (index >= 0 && index < this.childDocs.length) { this.rootDoc._itemIndex = index; + const activeItem: Doc = this.activeItem; const presTargetDoc = Cast(this.childDocs[this.itemIndex].presentationTargetDoc, Doc, null); + if (activeItem.presPinView) { + const bestTarget = DocumentManager.Instance.getFirstDocumentView(presTargetDoc)?.props.Document; + bestTarget && runInAction(() => { + if (activeItem.presMovement === PresMovement.Jump) { + bestTarget._viewTransition = '0s'; + } else { + bestTarget._viewTransition = activeItem.presTransition ? `transform ${activeItem.presTransition}ms` : 'all 1s'; + setTimeout(() => bestTarget._viewTransition = undefined, activeItem.presTransition ? NumCast(activeItem.presTransition) + 10 : 1010); + } + }); + } else { + presTargetDoc && runInAction(() => { + if (activeItem.presMovement === PresMovement.Jump) presTargetDoc.focusSpeed = 0; + else presTargetDoc.focusSpeed = activeItem.presTransition ? activeItem.presTransition : 500; + }); + setTimeout(() => presTargetDoc.focusSpeed = 500, this.activeItem.presTransition ? NumCast(this.activeItem.presTransition) + 10 : 510); + } if (presTargetDoc?.lastFrame !== undefined) { - presTargetDoc.currentFrame = 0; + presTargetDoc._currentFrame = 0; } this._selectedArray = [this.childDocs[index]]; //Update selected array //Handles movement to element @@ -209,40 +254,39 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> * on the right. */ navigateToElement = async (curDoc: Doc) => { - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; const srcContext = await DocCastAsync(targetDoc.context); const presCollection = Cast(this.layoutDoc.presCollection, Doc, null); const collectionDocView = presCollection ? await DocumentManager.Instance.getDocumentView(presCollection) : undefined; this.turnOffEdit(); - if (this.itemIndex >= 0) { if (srcContext && targetDoc) { this.layoutDoc.presCollection = srcContext; } else if (targetDoc) this.layoutDoc.presCollection = targetDoc; } - if (collectionDocView) { - if (srcContext && srcContext !== presCollection) { - // Case 1: new srcContext inside of current collection so add a new tab to the current pres collection - collectionDocView.props.addDocTab(srcContext, "inPlace"); - } - } + // if (collectionDocView) { + // if (srcContext && srcContext !== presCollection) { + // // Case 1: new srcContext inside of current collection so add a new tab to the current pres collection + // collectionDocView.props.addDocTab(srcContext, "inPlace"); + // } + // } this.updateCurrentPresentation(); const docToJump = curDoc; const willZoom = false; // If openDocument is selected then it should open the document for the user if (activeItem.openDocument) { - this.props.addDocTab(activeItem, "replace"); + collectionDocView ? collectionDocView.props.addDocTab(activeItem, "inPlace") : this.props.addDocTab(activeItem, "replace:right"); } else //docToJump stayed same meaning, it was not in the group or was the last element in the group if (activeItem.zoomProgressivize && this.rootDoc.presStatus !== 'edit') { this.zoomProgressivizeNext(targetDoc); } else if (docToJump === curDoc) { //checking if curDoc has navigation open - if (curDoc.presNavButton && targetDoc) { + if (curDoc.presMovement === PresMovement.Pan && targetDoc) { await DocumentManager.Instance.jumpToDocument(targetDoc, false, undefined, srcContext); - } else if (curDoc.presZoomButton && targetDoc) { + } else if ((curDoc.presMovement === PresMovement.Zoom || curDoc.presMovement === PresMovement.Jump) && targetDoc) { //awaiting jump so that new scale can be found, since jumping is async await DocumentManager.Instance.jumpToDocument(targetDoc, true, undefined, srcContext); } @@ -254,9 +298,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> // adjust the pan and scale to that of the pinView when it was added. // TODO: Add option to remove presPinView if (activeItem.presPinView) { - targetDoc._panX = activeItem.presPinViewX; - targetDoc._panY = activeItem.presPinViewY; - targetDoc._viewScale = activeItem.presPinViewScale; + // if targetDoc is not displayed but one of its aliases is, then we need to modify that alias, not the original target + const bestTarget = DocumentManager.Instance.getFirstDocumentView(targetDoc)?.props.Document; + bestTarget && runInAction(() => { + bestTarget._viewTransition = activeItem.presTransition ? `transform ${activeItem.presTransition}ms` : 'all 0.5s'; + bestTarget._panX = activeItem.presPinViewX; + bestTarget._panY = activeItem.presPinViewY; + bestTarget._viewScale = activeItem.presPinViewScale; + }); + //setTimeout(() => targetDoc._viewTransition = undefined, 1010); } // If website and has presWebsite data associated then on click it should // go back to that specific website @@ -271,7 +321,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> * @param presTargetDoc: document for which internal zoom is used */ zoomProgressivizeNext = (activeItem: Doc) => { - const targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); + const targetDoc: Doc = this.targetDoc; const srcContext = Cast(targetDoc?.context, Doc, null); const docView = DocumentManager.Instance.getDocumentView(targetDoc); const vfLeft = this.checkList(targetDoc, activeItem["viewfinder-left-indexed"]); @@ -346,15 +396,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @action startAutoPres = (startSlide: number) => { this.updateCurrentPresentation(); - let activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - let targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); - let duration = NumCast(targetDoc.presDuration) + NumCast(targetDoc.presTransition); + let activeItem: Doc = this.activeItem; + let targetDoc: Doc = this.targetDoc; + let duration = NumCast(activeItem.presDuration) + NumCast(activeItem.presTransition); const timer = (ms: number) => new Promise(res => this._presTimer = setTimeout(res, ms)); const load = async () => { // Wrap the loop into an async function for this to work for (var i = startSlide; i < this.childDocs.length; i++) { activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); - duration = NumCast(targetDoc.presDuration) + NumCast(targetDoc.presTransition); + duration = NumCast(activeItem.presDuration) + NumCast(activeItem.presTransition); if (duration <= 100) { duration = 2500; } if (NumCast(targetDoc.lastFrame) > 0) { for (var f = 0; f < NumCast(targetDoc.lastFrame); f++) { @@ -363,17 +413,27 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> } } await timer(duration); this.next(); // then the created Promise can be awaited - if (i === this.childDocs.length - 1) setTimeout(() => { clearTimeout(this._presTimer); if (this.layoutDoc.presStatus === 'auto') this.layoutDoc.presStatus = "manual"; }, duration); + if (i === this.childDocs.length - 1) { + setTimeout(() => { + clearTimeout(this._presTimer); + if (this.layoutDoc.presStatus === 'auto' && !this.layoutDoc.presLoop) this.layoutDoc.presStatus = "manual"; + else if (this.layoutDoc.presLoop) this.startAutoPres(0); + }, duration); + } } }; + this.layoutDoc.presStatus = "auto"; + this.startPresentation(startSlide); + this.gotoDocument(startSlide, this.itemIndex); + load(); + } + + @action + pauseAutoPres = () => { if (this.layoutDoc.presStatus === "auto") { if (this._presTimer) clearTimeout(this._presTimer); this.layoutDoc.presStatus = "manual"; - } else { - this.layoutDoc.presStatus = "auto"; - this.startPresentation(startSlide); - this.gotoDocument(startSlide, this.itemIndex); - load(); + this.layoutDoc.presLoop = false; } } @@ -384,7 +444,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> this.rootDoc._itemIndex = 0; } - @action togglePath = () => this.pathBoolean = !this.pathBoolean; + @action togglePath = (srcContext: Doc, off?: boolean) => { + if (off) { + this.pathBoolean = false; + srcContext.presPathView = false; + } else { + this.pathBoolean = !this.pathBoolean; + srcContext.presPathView = this.pathBoolean; + } + } @action toggleExpandMode = () => { this.rootDoc.expandBoolean = !this.rootDoc.expandBoolean; this.childDocs.forEach((doc) => { @@ -401,12 +469,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> startPresentation = (startIndex: number) => { this.updateCurrentPresentation(); this.childDocs.map(doc => { - const presTargetDoc = doc.presentationTargetDoc as Doc; + const tagDoc = doc.presentationTargetDoc as Doc; if (doc.presHideTillShownButton && this.childDocs.indexOf(doc) > startIndex) { - presTargetDoc.opacity = 0; + tagDoc.opacity = 0; } if (doc.presHideAfterButton && this.childDocs.indexOf(doc) < startIndex) { - presTargetDoc.opacity = 0; + tagDoc.opacity = 0; } }); } @@ -418,20 +486,32 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @undoBatch @action updateMinimize = () => { + const docView = DocumentManager.Instance.getDocumentView(this.layoutDoc); if (this.layoutDoc.inOverlay) { this.layoutDoc.presStatus = 'edit'; - Doc.RemoveDocFromList((Doc.UserDoc().myOverlayDocuments as Doc), undefined, this.rootDoc); - CollectionDockingView.AddRightSplit(this.rootDoc); + Doc.RemoveDocFromList((Doc.UserDoc().myOverlayDocs as Doc), undefined, this.rootDoc); + CollectionDockingView.AddSplit(this.rootDoc, "right"); this.layoutDoc.inOverlay = false; + } else if (this.layoutDoc.context && docView) { + this.layoutDoc.presStatus = 'edit'; + clearTimeout(this._presTimer); + const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); + this.rootDoc.x = pt[0] + (this.props.PanelWidth() - 250); + this.rootDoc.y = pt[1] + 10; + this.rootDoc._height = 35; + this.rootDoc._width = 250; + docView.props.removeDocument?.(this.layoutDoc); + Doc.AddDocToList((Doc.UserDoc().myOverlayDocs as Doc), undefined, this.rootDoc); } else { - this.layoutDoc.presStatus = 'manual'; + this.layoutDoc.presStatus = 'edit'; + clearTimeout(this._presTimer); const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); this.rootDoc.x = pt[0] + (this.props.PanelWidth() - 250); this.rootDoc.y = pt[1] + 10; this.rootDoc._height = 35; this.rootDoc._width = 250; this.props.addDocTab?.(this.rootDoc, "close"); - Doc.AddDocToList((Doc.UserDoc().myOverlayDocuments as Doc), undefined, this.rootDoc); + Doc.AddDocToList((Doc.UserDoc().myOverlayDocs as Doc), undefined, this.rootDoc); } } @@ -453,38 +533,36 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> * When the movement dropdown is changes */ @undoBatch - movementChanged = action((movement: string) => { - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); + updateMovement = action((movement: any, activeItem: Doc, targetDoc: Doc) => { switch (movement) { - case 'zoom': //Pan and zoom - activeItem.presZoomButton = !activeItem.presZoomButton; - if (activeItem.presZoomButton) activeItem.presMovement = 'Zoom'; - else activeItem.presMovement = 'None'; - activeItem.presNavButton = false; + case PresMovement.Zoom: //Pan and zoom + activeItem.presMovement = PresMovement.Zoom; break; - case 'pan': //Pan - activeItem.presZoomButton = false; - activeItem.presNavButton = !activeItem.presNavButton; - if (activeItem.presNavButton) activeItem.presMovement = 'Pan'; - else activeItem.presMovement = 'None'; + case PresMovement.Pan: //Pan + activeItem.presMovement = PresMovement.Pan; break; - case 'jump': //Jump Cut - targetDoc.presTransition = 0; - activeItem.presZoomButton = true; - activeItem.presSwitchButton = !activeItem.presSwitchButton; - if (activeItem.presSwitchButton) activeItem.presMovement = 'Jump cut'; - else activeItem.presMovement = 'None'; + case PresMovement.Jump: //Jump Cut + activeItem.presJump = true; + activeItem.presMovement = PresMovement.Jump; break; - case 'none': default: - activeItem.presMovement = 'None'; - activeItem.presZoomButton = false; - activeItem.presNavButton = false; - activeItem.presSwitchButton = false; + case PresMovement.None: default: + activeItem.presMovement = PresMovement.None; break; } }); + setMovementName = action((movement: any, activeItem: Doc): string => { + let output: string = 'none'; + switch (movement) { + case PresMovement.Zoom: output = 'Zoom'; break; //Pan and zoom + case PresMovement.Pan: output = 'Pan'; break; //Pan + case PresMovement.Jump: output = 'Jump cut'; break; //Jump Cut + case PresMovement.None: output = 'None'; break; //None + default: output = 'Zoom'; activeItem.presMovement = 'zoom'; break; //default set as zoom + } + return output; + }); + whenActiveChanged = action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive)); // For dragging documents into the presentation trail addDocumentFilter = (doc: Doc | Doc[]) => { @@ -492,19 +570,30 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> docs.forEach((doc, i) => { if (this.childDocs.includes(doc)) { if (docs.length === i + 1) return false; + } else if (doc.type === DocumentType.LABEL) { + const audio = Cast(doc.annotationOn, Doc, null); + if (audio) { + audio.aliasOf instanceof Doc; + audio.presStartTime = NumCast(doc.audioStart); + audio.presEndTime = NumCast(doc.audioEnd); + audio.presDuration = NumCast(doc.audioEnd) - NumCast(doc.audioStart); + TabDocView.PinDoc(audio, false, true); + setTimeout(() => this.removeDocument(doc), 1); + return false; + } } else { doc.aliasOf instanceof Doc && (doc.presentationTargetDoc = doc.aliasOf); - !this.childDocs.includes(doc) && (doc.presZoomButton = true); + !this.childDocs.includes(doc) && (doc.presMovement = PresMovement.Zoom); if (this.rootDoc.expandBoolean) doc.presExpandInlineButton = true; } }); return true; } childLayoutTemplate = () => this.rootDoc._viewType !== CollectionViewType.Stacking ? undefined : this.presElement; - removeDocument = (doc: Doc) => Doc.RemoveDocFromList(this.dataDoc, this.fieldKey, doc); + removeDocument = (doc: Doc) => { Doc.RemoveDocFromList(this.dataDoc, this.fieldKey, doc); }; getTransform = () => this.props.ScreenToLocalTransform().translate(-5, -65);// listBox padding-left and pres-box-cont minHeight panelHeight = () => this.props.PanelHeight() - 40; - active = (outsideReaction?: boolean) => ((Doc.GetSelectedTool() === InkTool.None && !this.layoutDoc.isBackground) && + active = (outsideReaction?: boolean) => ((Doc.GetSelectedTool() === InkTool.None && !this.layoutDoc._isBackground) && (this.layoutDoc.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false) /** @@ -526,18 +615,17 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> */ @computed get listOfSelected() { const list = this._selectedArray.map((doc: Doc, index: any) => { - const activeItem = Cast(doc, Doc, null); - const targetDoc = Cast(activeItem.presentationTargetDoc!, Doc, null); - return ( - <div className="selectedList-items">{index + 1}. {targetDoc.title}</div> - ); + const curDoc = Cast(doc, Doc, null); + const tagDoc = Cast(curDoc.presentationTargetDoc!, Doc, null); + if (tagDoc) return <div className="selectedList-items">{index + 1}. {curDoc.title}</div>; + else if (curDoc) return <div className="selectedList-items">{index + 1}. {curDoc.title}</div>; }); return list; } @action selectPres = () => { - const presDocView = DocumentManager.Instance.getDocumentView(PresBox.Instance.rootDoc)!; + const presDocView = DocumentManager.Instance.getDocumentView(this.rootDoc)!; SelectionManager.SelectDoc(presDocView, false); } @@ -545,7 +633,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @action selectElement = (doc: Doc) => { this.gotoDocument(this.childDocs.indexOf(doc), NumCast(this.itemIndex)); - this.selectPres(); + if (doc.presPinView) setTimeout(() => this.selectPres(), 0); + else this.selectPres(); } //Command click @@ -563,8 +652,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @action shiftSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement) => { this._selectedArray = []; - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - if (activeItem) { + // const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + if (this.activeItem) { for (let i = Math.min(this.itemIndex, this.childDocs.indexOf(doc)); i <= Math.max(this.itemIndex, this.childDocs.indexOf(doc)); i++) { this._selectedArray.push(this.childDocs[i]); this._eleArray.push(ref); @@ -575,8 +664,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> } // Key for when the presentaiton is active - @action - keyEvents = (e: KeyboardEvent) => { + @undoBatch + keyEvents = action((e: KeyboardEvent) => { + if (e.target instanceof HTMLInputElement) return; let handled = false; const anchorNode = document.activeElement as HTMLDivElement; if (anchorNode && anchorNode.className?.includes("lm_title")) return; @@ -592,43 +682,54 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> handled = true; } } if (e.keyCode === 37 || e.keyCode === 38) { // left(37) / a(65) / up(38) to go back - this.back(); if (this._presTimer) clearTimeout(this._presTimer); + this.back(); + if (this._presTimer) clearTimeout(this._presTimer); handled = true; } if (e.keyCode === 39 || e.keyCode === 40) { // right (39) / d(68) / down(40) to go to next - this.next(); if (this._presTimer) clearTimeout(this._presTimer); + this.next(); + if (this._presTimer) clearTimeout(this._presTimer); handled = true; } if (e.keyCode === 32) { // spacebar to 'present' or autoplay if (this.layoutDoc.presStatus !== "edit") this.startAutoPres(0); - else this.layoutDoc.presStatus = "manual"; if (this._presTimer) clearTimeout(this._presTimer); + else this.next(); handled = true; - } - if (e.keyCode === 8) { // delete selected items + } if (e.keyCode === 8) { // delete selected items if (this.layoutDoc.presStatus === "edit") { - this._selectedArray.forEach((doc, i) => { - this.removeDocument(doc); - }); + this._selectedArray.forEach((doc, i) => this.removeDocument(doc)); + this._selectedArray = []; + this._eleArray = []; + this._dragArray = []; handled = true; } - } - if (handled) { + } if (handled) { e.stopPropagation(); e.preventDefault(); + } if ((e.keyCode === 37 || e.keyCode === 38) && e.shiftKey) { // left(37) / a(65) / up(38) to go back + if (this.layoutDoc.presStatus === "edit" && this._selectedArray.length > 0) { + const index = this.childDocs.indexOf(this._selectedArray[this._selectedArray.length]); + if ((index - 1) > 0) this._selectedArray.push(this.childDocs[index - 1]); + } + handled = true; + } if ((e.keyCode === 39 || e.keyCode === 40) && e.shiftKey) { // left(37) / a(65) / up(38) to go back + if (this.layoutDoc.presStatus === "edit" && this._selectedArray.length > 0) { + const index = this.childDocs.indexOf(this._selectedArray[this._selectedArray.length]); + if ((index - 1) > 0) { + this._selectedArray.push(this.childDocs[index - 1]); + } + } + handled = true; } - } + }); /** * */ @undoBatch @action - viewPaths = async () => { + viewPaths = () => { const srcContext = Cast(this.rootDoc.presCollection, Doc, null); - if (this.pathBoolean && srcContext) { - this.togglePath(); - srcContext.presPathView = false; - } else if (srcContext) { - this.togglePath(); - srcContext.presPathView = true; + if (srcContext) { + this.togglePath(srcContext); } } @@ -636,12 +737,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @computed get order() { const order: JSX.Element[] = []; this.childDocs.forEach((doc, index) => { - const targetDoc = Cast(doc.presentationTargetDoc, Doc, null); - const srcContext = Cast(targetDoc?.context, Doc, null); + const tagDoc = Cast(doc.presentationTargetDoc, Doc, null); + const srcContext = Cast(tagDoc?.context, Doc, null); + const width = NumCast(tagDoc._width) / 10; + const height = Math.max(NumCast(tagDoc._height) / 10, 15); + const edge = Math.max(width, height); + const fontSize = edge * 0.8; // Case A: Document is contained within the colleciton if (this.rootDoc.presCollection === srcContext) { order.push( - <div className="pathOrder" style={{ top: NumCast(targetDoc.y), left: NumCast(targetDoc.x) }}> + <div className="pathOrder" style={{ top: NumCast(tagDoc.y) - (edge / 2), left: NumCast(tagDoc.x) - (edge / 2), width: edge, height: edge, fontSize: fontSize }}> <div className="pathOrder-frame">{index + 1}</div> </div>); // Case B: Document is not inside of the collection @@ -666,11 +771,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @computed get paths() { let pathPoints = ""; this.childDocs.forEach((doc, index) => { - const targetDoc = Cast(doc.presentationTargetDoc, Doc, null); - const srcContext = Cast(targetDoc?.context, Doc, null); - if (targetDoc && this.rootDoc.presCollection === srcContext) { - const n1x = NumCast(targetDoc.x) + (NumCast(targetDoc._width) / 2); - const n1y = NumCast(targetDoc.y) + (NumCast(targetDoc._height) / 2); + const tagDoc = Cast(doc.presentationTargetDoc, Doc, null); + const srcContext = Cast(tagDoc?.context, Doc, null); + if (tagDoc && this.rootDoc.presCollection === srcContext) { + const n1x = NumCast(tagDoc.x) + (NumCast(tagDoc._width) / 2); + const n1y = NumCast(tagDoc.y) + (NumCast(tagDoc._height) / 2); if (index = 0) pathPoints = n1x + "," + n1y; else pathPoints = pathPoints + " " + n1x + "," + n1y; } else { @@ -685,11 +790,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> stroke: "#69a6db", strokeWidth: 5, strokeDasharray: '10 5', + boxShadow: '0px 4px 4px rgba(0, 0, 0, 0.25)', }} fill="none" - markerStart="url(#markerSquare)" + markerStart="url(#markerArrow)" markerMid="url(#markerSquare)" - markerEnd="url(#markerArrow)" + markerEnd="url(#markerSquareFilled)" />); } @@ -701,8 +807,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @action onFadeDocumentAfterPresentedClick = (e: React.MouseEvent) => { e.stopPropagation(); - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; activeItem.presFadeButton = !activeItem.presFadeButton; if (!activeItem.presFadeButton) { if (targetDoc) { @@ -722,9 +828,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> if (change) timeInMS += change; if (timeInMS < 100) timeInMS = 100; if (timeInMS > 10000) timeInMS = 10000; - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); - if (targetDoc) targetDoc.presTransition = timeInMS; + if (this.activeItem) this.activeItem.presTransition = timeInMS; } // Converts seconds to ms and updates presDuration @@ -733,19 +837,17 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> if (change) timeInMS += change; if (timeInMS < 100) timeInMS = 100; if (timeInMS > 20000) timeInMS = 20000; - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); - if (targetDoc) targetDoc.presDuration = timeInMS; + if (this.activeItem) this.activeItem.presDuration = timeInMS; } @computed get transitionDropdown() { - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; if (activeItem && targetDoc) { - const transitionSpeed = targetDoc.presTransition ? NumCast(targetDoc.presTransition) / 1000 : 0.5; - let duration = targetDoc.presDuration ? NumCast(targetDoc.presDuration) / 1000 : 2; - if (targetDoc.type === DocumentType.AUDIO) duration = NumCast(targetDoc.duration); + const transitionSpeed = activeItem.presTransition ? NumCast(activeItem.presTransition) / 1000 : 0.5; + let duration = activeItem.presDuration ? NumCast(activeItem.presDuration) / 1000 : 2; + if (activeItem.type === DocumentType.AUDIO) duration = NumCast(activeItem.duration); const effect = targetDoc.presEffect ? targetDoc.presEffect : 'None'; activeItem.presMovement = activeItem.presMovement ? activeItem.presMovement : 'Zoom'; return ( @@ -753,21 +855,20 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> <div className="ribbon-box"> Movement <div className="presBox-dropdown" onClick={action(e => { e.stopPropagation(); this.openMovementDropdown = !this.openMovementDropdown; })} style={{ borderBottomLeftRadius: this.openMovementDropdown ? 0 : 5, border: this.openMovementDropdown ? 'solid 2px #5B9FDD' : 'solid 1px black' }}> - {activeItem.presMovement} + {this.setMovementName(activeItem.presMovement, activeItem)} <FontAwesomeIcon className='presBox-dropdownIcon' style={{ gridColumn: 2, color: this.openMovementDropdown ? '#5B9FDD' : 'black' }} icon={"angle-down"} /> <div className={'presBox-dropdownOptions'} id={'presBoxMovementDropdown'} onPointerDown={e => e.stopPropagation()} style={{ display: this.openMovementDropdown ? "grid" : "none" }}> - <div className={`presBox-dropdownOption ${activeItem.presMovement === 'None' ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.movementChanged('none')}>None</div> - <div className={`presBox-dropdownOption ${activeItem.presMovement === 'Zoom' ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.movementChanged('zoom')}>Pan and Zoom</div> - <div className={`presBox-dropdownOption ${activeItem.presMovement === 'Pan' ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.movementChanged('pan')}>Pan</div> - <div className={`presBox-dropdownOption ${activeItem.presMovement === 'Jump cut' ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.movementChanged('jump')}>Jump cut</div> + <div className={`presBox-dropdownOption ${activeItem.presMovement === PresMovement.None ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateMovement(PresMovement.None, activeItem, targetDoc)}>None</div> + <div className={`presBox-dropdownOption ${activeItem.presMovement === PresMovement.Zoom ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateMovement(PresMovement.Zoom, activeItem, targetDoc)}>Pan and Zoom</div> + <div className={`presBox-dropdownOption ${activeItem.presMovement === PresMovement.Pan ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateMovement(PresMovement.Pan, activeItem, targetDoc)}>Pan</div> + <div className={`presBox-dropdownOption ${activeItem.presMovement === PresMovement.Jump ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateMovement(PresMovement.Jump, activeItem, targetDoc)}>Jump cut</div> </div> </div> - <div className="ribbon-doubleButton" style={{ display: activeItem.presMovement === 'Pan' || activeItem.presMovement === 'Zoom' ? "inline-flex" : "none" }}> + <div className="ribbon-doubleButton" style={{ display: activeItem.presMovement === PresMovement.Pan || activeItem.presMovement === PresMovement.Zoom ? "inline-flex" : "none" }}> <div className="presBox-subheading">Transition Speed</div> <div className="ribbon-property"> <input className="presBox-input" type="number" value={transitionSpeed} - onFocus={() => { document.removeEventListener("keydown", this.keyEvents, true); }} onChange={action((e) => this.setTransitionTime(e.target.value))} /> s </div> <div className="ribbon-propertyUpDown"> @@ -779,8 +880,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> </div> </div> </div> - <input type="range" step="0.1" min="0.1" max="10" value={transitionSpeed} className={`toolbar-slider ${activeItem.presMovement === 'Pan' || activeItem.presMovement === 'Zoom' ? "" : "none"}`} id="toolbar-slider" onChange={(e: React.ChangeEvent<HTMLInputElement>) => { e.stopPropagation(); this.setTransitionTime(e.target.value); }} /> - <div className={`slider-headers ${activeItem.presMovement === 'Pan' || activeItem.presMovement === 'Zoom' ? "" : "none"}`}> + <input type="range" step="0.1" min="0.1" max="10" value={transitionSpeed} className={`toolbar-slider ${activeItem.presMovement === PresMovement.Pan || activeItem.presMovement === PresMovement.Zoom ? "" : "none"}`} id="toolbar-slider" onChange={(e: React.ChangeEvent<HTMLInputElement>) => { e.stopPropagation(); this.setTransitionTime(e.target.value); }} /> + <div className={`slider-headers ${activeItem.presMovement === PresMovement.Pan || activeItem.presMovement === PresMovement.Zoom ? "" : "none"}`}> <div className="slider-text">Fast</div> <div className="slider-text">Medium</div> <div className="slider-text">Slow</div> @@ -789,15 +890,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> <div className="ribbon-box"> Visibility {"&"} Duration <div className="ribbon-doubleButton"> - <Tooltip title={<><div className="dash-tooltip">{"Hide before presented"}</div></>}><div className={`ribbon-button ${activeItem.presHideTillShownButton ? "active" : ""}`} onClick={() => activeItem.presHideTillShownButton = !activeItem.presHideTillShownButton}>Hide before</div></Tooltip> - <Tooltip title={<><div className="dash-tooltip">{"Hide after presented"}</div></>}><div className={`ribbon-button ${activeItem.presHideAfterButton ? "active" : ""}`} onClick={() => activeItem.presHideAfterButton = !activeItem.presHideAfterButton}>Hide after</div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Hide before presented"}</div></>}><div className={`ribbon-toggle ${activeItem.presHideTillShownButton ? "active" : ""}`} onClick={() => activeItem.presHideTillShownButton = !activeItem.presHideTillShownButton}>Hide before</div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Hide after presented"}</div></>}><div className={`ribbon-toggle ${activeItem.presHideAfterButton ? "active" : ""}`} onClick={() => activeItem.presHideAfterButton = !activeItem.presHideAfterButton}>Hide after</div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Open document in a new tab"}</div></>}><div className="ribbon-toggle" style={{ backgroundColor: activeItem.openDocument ? "#aedef8" : "" }} onClick={() => activeItem.openDocument = !activeItem.openDocument}>Open</div></Tooltip> </div> <div className="ribbon-doubleButton" > <div className="presBox-subheading">Slide Duration</div> <div className="ribbon-property"> <input className="presBox-input" type="number" value={duration} - onFocus={() => { document.removeEventListener("keydown", this.keyEvents, true); }} + // onFocus={() => document.removeEventListener("keydown", this.keyEvents, true)} onChange={action((e) => this.setDurationTime(e.target.value))} /> s </div> <div className="ribbon-propertyUpDown"> @@ -837,11 +939,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> </div> </div> <div className="effectDirection" style={{ display: effect === 'None' ? "none" : "grid", width: 40 }}> - <Tooltip title={<><div className="dash-tooltip">{"Enter from left"}</div></>}><div style={{ gridColumn: 1, gridRow: 2, justifySelf: 'center', color: targetDoc.presEffectDirection === "left" ? "#5a9edd" : "black" }} onClick={() => targetDoc.presEffectDirection = 'left'}><FontAwesomeIcon icon={"angle-right"} /></div></Tooltip> - <Tooltip title={<><div className="dash-tooltip">{"Enter from right"}</div></>}><div style={{ gridColumn: 3, gridRow: 2, justifySelf: 'center', color: targetDoc.presEffectDirection === "right" ? "#5a9edd" : "black" }} onClick={() => targetDoc.presEffectDirection = 'right'}><FontAwesomeIcon icon={"angle-left"} /></div></Tooltip> - <Tooltip title={<><div className="dash-tooltip">{"Enter from top"}</div></>}><div style={{ gridColumn: 2, gridRow: 1, justifySelf: 'center', color: targetDoc.presEffectDirection === "top" ? "#5a9edd" : "black" }} onClick={() => targetDoc.presEffectDirection = 'top'}><FontAwesomeIcon icon={"angle-down"} /></div></Tooltip> - <Tooltip title={<><div className="dash-tooltip">{"Enter from bottom"}</div></>}><div style={{ gridColumn: 2, gridRow: 3, justifySelf: 'center', color: targetDoc.presEffectDirection === "bottom" ? "#5a9edd" : "black" }} onClick={() => targetDoc.presEffectDirection = 'bottom'}><FontAwesomeIcon icon={"angle-up"} /></div></Tooltip> - <Tooltip title={<><div className="dash-tooltip">{"Enter from center"}</div></>}><div style={{ gridColumn: 2, gridRow: 2, width: 10, height: 10, alignSelf: 'center', justifySelf: 'center', border: targetDoc.presEffectDirection ? "solid 2px black" : "solid 2px #5a9edd", borderRadius: "100%" }} onClick={() => targetDoc.presEffectDirection = false}></div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Enter from left"}</div></>}><div style={{ gridColumn: 1, gridRow: 2, justifySelf: 'center', color: targetDoc.presEffectDirection === "left" ? "#5a9edd" : "black", cursor: "pointer" }} onClick={() => targetDoc.presEffectDirection = 'left'}><FontAwesomeIcon icon={"angle-right"} /></div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Enter from right"}</div></>}><div style={{ gridColumn: 3, gridRow: 2, justifySelf: 'center', color: targetDoc.presEffectDirection === "right" ? "#5a9edd" : "black", cursor: "pointer" }} onClick={() => targetDoc.presEffectDirection = 'right'}><FontAwesomeIcon icon={"angle-left"} /></div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Enter from top"}</div></>}><div style={{ gridColumn: 2, gridRow: 1, justifySelf: 'center', color: targetDoc.presEffectDirection === "top" ? "#5a9edd" : "black", cursor: "pointer" }} onClick={() => targetDoc.presEffectDirection = 'top'}><FontAwesomeIcon icon={"angle-down"} /></div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Enter from bottom"}</div></>}><div style={{ gridColumn: 2, gridRow: 3, justifySelf: 'center', color: targetDoc.presEffectDirection === "bottom" ? "#5a9edd" : "black", cursor: "pointer" }} onClick={() => targetDoc.presEffectDirection = 'bottom'}><FontAwesomeIcon icon={"angle-up"} /></div></Tooltip> + <Tooltip title={<><div className="dash-tooltip">{"Enter from center"}</div></>}><div style={{ gridColumn: 2, gridRow: 2, width: 10, height: 10, alignSelf: 'center', justifySelf: 'center', border: targetDoc.presEffectDirection ? "solid 2px black" : "solid 2px #5a9edd", borderRadius: "100%", cursor: "pointer" }} onClick={() => targetDoc.presEffectDirection = false}></div></Tooltip> </div> </div> <div className="ribbon-final-box"> @@ -858,10 +960,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> } @computed get effectDirection(): string { - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); let effect = ''; - switch (targetDoc.presEffectDirection) { + switch (this.targetDoc.presEffectDirection) { case 'left': effect = "Enter from left"; break; case 'right': effect = "Enter from right"; break; case 'top': effect = "Enter from top"; break; @@ -874,41 +974,62 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @undoBatch @action applyTo = (array: Doc[]) => { - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); - array.forEach((doc, index) => { + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; + array.forEach((doc) => { const curDoc = Cast(doc, Doc, null); const tagDoc = Cast(curDoc.presentationTargetDoc, Doc, null); if (tagDoc && targetDoc) { - tagDoc.presTransition = targetDoc.presTransition; - tagDoc.presDuration = targetDoc.presDuration; + curDoc.presTransition = activeItem.presTransition; + curDoc.presDuration = activeItem.presDuration; tagDoc.presEffect = targetDoc.presEffect; tagDoc.presEffectDirection = targetDoc.presEffectDirection; + tagDoc.presMovement = targetDoc.presMovement; curDoc.presMovement = activeItem.presMovement; + this.updateMovement(activeItem.presMovement, curDoc, tagDoc); curDoc.presHideTillShownButton = activeItem.presHideTillShownButton; curDoc.presHideAfterButton = activeItem.presHideAfterButton; } }); } @computed get optionsDropdown() { - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; + const presPinWithViewIcon = <img src="/assets/pinWithView.png" style={{ margin: "auto", width: 16, filter: 'invert(1)' }} />; if (activeItem && targetDoc) { return ( <div> <div className={'presBox-ribbon'} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> <div className="ribbon-box"> <div className="ribbon-doubleButton" style={{ display: targetDoc.type === DocumentType.VID || targetDoc.type === DocumentType.AUDIO ? "inline-flex" : "none" }}> - <div className="ribbon-button" style={{ backgroundColor: activeItem.playAuto ? "#aedef8" : "" }} onClick={() => activeItem.playAuto = !activeItem.playAuto}>Play automatically</div> - <div className="ribbon-button" style={{ display: "flex", backgroundColor: activeItem.playAuto ? "" : "#aedef8" }} onClick={() => activeItem.playAuto = !activeItem.playAuto}>Play on next</div> - </div> - <div className="ribbon-doubleButton" style={{ display: "flex" }}> - <div className="ribbon-button" style={{ backgroundColor: activeItem.openDocument ? "#aedef8" : "" }} onClick={() => activeItem.openDocument = !activeItem.openDocument}>Open document</div> + <div className="ribbon-toggle" style={{ backgroundColor: activeItem.playAuto ? "#aedef8" : "" }} onClick={() => activeItem.playAuto = !activeItem.playAuto}>Play automatically</div> + <div className="ribbon-toggle" style={{ display: "flex", backgroundColor: activeItem.playAuto ? "" : "#aedef8" }} onClick={() => activeItem.playAuto = !activeItem.playAuto}>Play on next</div> </div> + {targetDoc.type === DocumentType.VID ? <div className="ribbon-toggle" style={{ backgroundColor: activeItem.presVidFullScreen ? "#aedef8" : "" }} onClick={() => activeItem.presVidFullScreen = !activeItem.presVidFullScreen}>Full screen</div> : (null)} + {targetDoc.type === DocumentType.VID || targetDoc.type === DocumentType.AUDIO ? <div className="ribbon-doubleButton" style={{ marginRight: 10 }}> + <div className="presBox-subheading">Start time</div> + <div className="ribbon-property" style={{ paddingRight: 0, paddingLeft: 0 }}> + <input className="presBox-input" + style={{ textAlign: 'left', width: 50 }} + type="number" value={NumCast(activeItem.presStartTime)} + onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { activeItem.presStartTime = Number(e.target.value); })} /> + </div> + </div> : (null)} + {targetDoc.type === DocumentType.VID || targetDoc.type === DocumentType.AUDIO ? <div className="ribbon-doubleButton" style={{ marginRight: 10 }}> + <div className="presBox-subheading">End time</div> + <div className="ribbon-property" style={{ paddingRight: 0, paddingLeft: 0 }}> + <input className="presBox-input" + style={{ textAlign: 'left', width: 50 }} + type="number" value={NumCast(activeItem.presEndTime)} + onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { const val = e.target.value; activeItem.presEndTime = Number(val); })} /> + </div> + </div> : (null)} + {targetDoc.type === DocumentType.COL ? 'Presentation Pin View' : (null)} <div className="ribbon-doubleButton" style={{ display: targetDoc.type === DocumentType.COL ? "inline-flex" : "none" }}> - <div className="ribbon-button" style={{ backgroundColor: activeItem.presPinView ? "#aedef8" : "" }} + <div className="ribbon-toggle" style={{ width: 20, padding: 0, backgroundColor: activeItem.presPinView ? "#aedef8" : "" }} onClick={() => { activeItem.presPinView = !activeItem.presPinView; + targetDoc.presPinView = activeItem.presPinView; if (activeItem.presPinView) { const x = targetDoc._panX; const y = targetDoc._panY; @@ -917,7 +1038,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> activeItem.presPinViewY = y; activeItem.presPinViewScale = scale; } - }}>Presentation pin view</div> + }}>{presPinWithViewIcon}</div> + {activeItem.presPinView ? <div className="ribbon-button" + onClick={() => { + const x = targetDoc._panX; + const y = targetDoc._panY; + const scale = targetDoc._viewScale; + activeItem.presPinViewX = x; + activeItem.presPinViewY = y; + activeItem.presPinViewScale = scale; + }}>Update</div> : (null)} </div> <div style={{ display: activeItem.presPinView ? "block" : "none" }}> <div className="ribbon-doubleButton" style={{ marginRight: 10 }}> @@ -926,7 +1056,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> <input className="presBox-input" style={{ textAlign: 'left', width: 50 }} type="number" value={NumCast(activeItem.presPinViewX)} - onFocus={() => { document.removeEventListener("keydown", this.keyEvents, true); }} onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { const val = e.target.value; activeItem.presPinViewX = Number(val); })} /> </div> </div> @@ -936,7 +1065,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> <input className="presBox-input" style={{ textAlign: 'left', width: 50 }} type="number" value={NumCast(activeItem.presPinViewY)} - onFocus={() => { document.removeEventListener("keydown", this.keyEvents, true); }} onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { const val = e.target.value; activeItem.presPinViewY = Number(val); })} /> </div> </div> @@ -946,13 +1074,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> <input className="presBox-input" style={{ textAlign: 'left', width: 50 }} type="number" value={NumCast(activeItem.presPinViewScale)} - onFocus={() => { document.removeEventListener("keydown", this.keyEvents, true); }} onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { const val = e.target.value; activeItem.presPinViewScale = Number(val); })} /> </div> </div> </div> {/* <div className="ribbon-doubleButton" style={{ display: targetDoc.type === DocumentType.WEB ? "inline-flex" : "none" }}> - <div className="ribbon-button" onClick={this.progressivizeText}>Store original website</div> + <div className="ribbon-toggle" onClick={this.progressivizeText}>Store original website</div> </div> */} </div> </div> @@ -996,20 +1123,18 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> <div className="ribbon-box"> Slide Title: <br></br> <input className="ribbon-textInput" placeholder="..." type="text" name="fname" - onFocus={() => { - document.removeEventListener("keydown", this.keyEvents, true); - }} onChange={(e) => { e.stopPropagation(); e.preventDefault(); runInAction(() => this.title = e.target.value); - }}></input> + }}> + </input> </div> <div className="ribbon-box"> Choose type: <div className="ribbon-doubleButton"> - <div title="Text" className={'ribbon-button'} style={{ background: this.addFreeform ? "" : "#aedef8" }} onClick={action(() => this.addFreeform = !this.addFreeform)}>Text</div> - <div title="Freeform" className={'ribbon-button'} style={{ background: this.addFreeform ? "#aedef8" : "" }} onClick={action(() => this.addFreeform = !this.addFreeform)}>Freeform</div> + <div title="Text" className={'ribbon-toggle'} style={{ background: this.addFreeform ? "" : "#aedef8" }} onClick={action(() => this.addFreeform = !this.addFreeform)}>Text</div> + <div title="Freeform" className={'ribbon-toggle'} style={{ background: this.addFreeform ? "#aedef8" : "" }} onClick={action(() => this.addFreeform = !this.addFreeform)}>Freeform</div> </div> </div> <div className="ribbon-box" style={{ display: this.addFreeform ? "grid" : "none" }}> @@ -1052,21 +1177,23 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> if (layout) doc = this.createTemplate(layout); if (freeform && layout) doc = this.createTemplate(layout, title); if (!freeform && !layout) doc = Docs.Create.TextDocument("", { _nativeWidth: 400, _width: 225, title: title }); - const presCollection = Cast(this.layoutDoc.presCollection, Doc, null); - const data = Cast(presCollection?.data, listSpec(Doc)); - const presData = Cast(this.rootDoc.data, listSpec(Doc)); - if (data && doc && presData) { - data.push(doc); - DockedFrameRenderer.PinDoc(doc, false); - this.gotoDocument(this.childDocs.length, this.itemIndex); - } else { - this.props.addDocTab(doc as Doc, "onRight"); + if (doc) { + const presCollection = Cast(this.layoutDoc.presCollection, Doc, null); + const data = Cast(presCollection?.data, listSpec(Doc)); + const presData = Cast(this.rootDoc.data, listSpec(Doc)); + if (data && presData) { + data.push(doc); + TabDocView.PinDoc(doc, false); + this.gotoDocument(this.childDocs.length, this.itemIndex); + } else { + this.props.addDocTab(doc, "add:right"); + } } } createTemplate = (layout: string, input?: string) => { - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; let x = 0; let y = 0; if (activeItem && targetDoc) { @@ -1107,10 +1234,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @computed get presentDropdown() { return ( <div className={`dropdown-play ${this.presentTools ? "active" : ""}`} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> - <div className="dropdown-play-button" onClick={this.updateMinimize}> + <div className="dropdown-play-button" onClick={(action(() => { this.updateMinimize(); this.turnOffEdit(true); }))}> Minimize </div> - <div className="dropdown-play-button" onClick={(action(() => { this.layoutDoc.presStatus = "manual"; this.turnOffEdit(); }))}> + <div className="dropdown-play-button" onClick={(action(() => { this.layoutDoc.presStatus = "manual"; this.turnOffEdit(true); }))}> Sidebar view </div> </div> @@ -1120,57 +1247,57 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> // Case in which the document has keyframes to navigate to next key frame @undoBatch @action - nextKeyframe = (tagDoc: Doc, activeItem: Doc): void => { + nextKeyframe = (tagDoc: Doc, curDoc: Doc): void => { const childDocs = DocListCast(tagDoc[Doc.LayoutFieldKey(tagDoc)]); - const currentFrame = Cast(tagDoc.currentFrame, "number", null); + const currentFrame = Cast(tagDoc._currentFrame, "number", null); if (currentFrame === undefined) { - tagDoc.currentFrame = 0; + tagDoc._currentFrame = 0; CollectionFreeFormDocumentView.setupScroll(tagDoc, 0); CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0); } CollectionFreeFormDocumentView.updateScrollframe(tagDoc, currentFrame); CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0, tagDoc); - tagDoc.currentFrame = Math.max(0, (currentFrame || 0) + 1); - tagDoc.lastFrame = Math.max(NumCast(tagDoc.currentFrame), NumCast(tagDoc.lastFrame)); - if (activeItem.zoomProgressivize) { - const resize = document.getElementById('resizable'); - if (resize) { - resize.style.width = this.checkList(tagDoc, activeItem["viewfinder-width-indexed"]) + 'px'; - resize.style.height = this.checkList(tagDoc, activeItem["viewfinder-height-indexed"]) + 'px'; - resize.style.top = this.checkList(tagDoc, activeItem["viewfinder-top-indexed"]) + 'px'; - resize.style.left = this.checkList(tagDoc, activeItem["viewfinder-left-indexed"]) + 'px'; - } - } + tagDoc._currentFrame = Math.max(0, (currentFrame || 0) + 1); + tagDoc.lastFrame = Math.max(NumCast(tagDoc._currentFrame), NumCast(tagDoc.lastFrame)); + // if (curDoc.zoomProgressivize) { + // const resize = document.getElementById('resizable'); + // if (resize) { + // resize.style.width = this.checkList(tagDoc, curDoc["viewfinder-width-indexed"]) + 'px'; + // resize.style.height = this.checkList(tagDoc, curDoc["viewfinder-height-indexed"]) + 'px'; + // resize.style.top = this.checkList(tagDoc, curDoc["viewfinder-top-indexed"]) + 'px'; + // resize.style.left = this.checkList(tagDoc, curDoc["viewfinder-left-indexed"]) + 'px'; + // } + // } } @undoBatch @action - prevKeyframe = (tagDoc: Doc, activeItem: Doc): void => { + prevKeyframe = (tagDoc: Doc, actItem: Doc): void => { const childDocs = DocListCast(tagDoc[Doc.LayoutFieldKey(tagDoc)]); - const currentFrame = Cast(tagDoc.currentFrame, "number", null); + const currentFrame = Cast(tagDoc._currentFrame, "number", null); if (currentFrame === undefined) { - tagDoc.currentFrame = 0; + tagDoc._currentFrame = 0; CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0); } CollectionFreeFormDocumentView.gotoKeyframe(childDocs.slice()); - tagDoc.currentFrame = Math.max(0, (currentFrame || 0) - 1); - if (activeItem.zoomProgressivize) { - const resize = document.getElementById('resizable'); - if (resize) { - resize.style.width = this.checkList(tagDoc, activeItem["viewfinder-width-indexed"]) + 'px'; - resize.style.height = this.checkList(tagDoc, activeItem["viewfinder-height-indexed"]) + 'px'; - resize.style.top = this.checkList(tagDoc, activeItem["viewfinder-top-indexed"]) + 'px'; - resize.style.left = this.checkList(tagDoc, activeItem["viewfinder-left-indexed"]) + 'px'; - } - } + tagDoc._currentFrame = Math.max(0, (currentFrame || 0) - 1); + // if (actItem.zoomProgressivize) { + // const resize = document.getElementById('resizable'); + // if (resize) { + // resize.style.width = this.checkList(tagDoc, actItem["viewfinder-width-indexed"]) + 'px'; + // resize.style.height = this.checkList(tagDoc, actItem["viewfinder-height-indexed"]) + 'px'; + // resize.style.top = this.checkList(tagDoc, actItem["viewfinder-top-indexed"]) + 'px'; + // resize.style.left = this.checkList(tagDoc, actItem["viewfinder-left-indexed"]) + 'px'; + // } + // } } /** * Returns the collection type as a string for headers */ @computed get stringType(): string { - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; let type: string = ''; if (activeItem) { switch (targetDoc.type) { @@ -1193,39 +1320,39 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @computed get progressivizeDropdown() { - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); - const activeFontColor = targetDoc["pres-text-color"] ? StrCast(targetDoc["pres-text-color"]) : "Black"; - const viewedFontColor = targetDoc["pres-text-viewed-color"] ? StrCast(targetDoc["pres-text-viewed-color"]) : "Black"; + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; if (activeItem && targetDoc) { + const activeFontColor = targetDoc["pres-text-color"] ? StrCast(targetDoc["pres-text-color"]) : "Black"; + const viewedFontColor = targetDoc["pres-text-viewed-color"] ? StrCast(targetDoc["pres-text-viewed-color"]) : "Black"; return ( <div> <div className={`presBox-ribbon ${this.progressivizeTools && this.layoutDoc.presStatus === "edit" ? "active" : ""}`} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> <div className="ribbon-box"> {this.stringType} selected - <div className="ribbon-doubleButton" style={{ borderTop: 'solid 1px darkgrey', display: targetDoc.type === DocumentType.COL && targetDoc._viewType === 'freeform' ? "inline-flex" : "none" }}> - <div className="ribbon-button" style={{ backgroundColor: activeItem.presProgressivize ? "#aedef8" : "" }} onClick={this.progressivizeChild}>Contents</div> - <div className="ribbon-button" style={{ opacity: activeItem.presProgressivize ? 1 : 0.4, backgroundColor: targetDoc.editProgressivize ? "#aedef8" : "" }} onClick={this.editProgressivize}>Edit</div> + <div className="ribbon-doubleButton" style={{ borderTop: 'solid 1px darkgrey', display: (targetDoc.type === DocumentType.COL && targetDoc._viewType === 'freeform') || targetDoc.type === DocumentType.IMG ? "inline-flex" : "none" }}> + <div className="ribbon-toggle" style={{ backgroundColor: activeItem.presProgressivize ? "#aedef8" : "" }} onClick={this.progressivizeChild}>Contents</div> + <div className="ribbon-toggle" style={{ opacity: activeItem.presProgressivize ? 1 : 0.4, backgroundColor: targetDoc.editProgressivize ? "#aedef8" : "" }} onClick={this.editProgressivize}>Edit</div> </div> <div className="ribbon-doubleButton" style={{ display: activeItem.presProgressivize ? "inline-flex" : "none" }}> <div className="presBox-subheading">Active text color</div> - <div className="ribbon-property" style={{ backgroundColor: activeFontColor }} onClick={action(() => { console.log("hi"); this.openActiveColorPicker = !this.openActiveColorPicker; })}> + <div className="ribbon-colorBox" style={{ backgroundColor: activeFontColor, height: 15, width: 15 }} onClick={action(() => { this.openActiveColorPicker = !this.openActiveColorPicker; })}> </div> </div> {this.activeColorPicker} <div className="ribbon-doubleButton" style={{ display: activeItem.presProgressivize ? "inline-flex" : "none" }}> <div className="presBox-subheading">Viewed font color</div> - <div className="ribbon-property" style={{ backgroundColor: viewedFontColor }} onClick={action(() => this.openViewedColorPicker = !this.openViewedColorPicker)}> + <div className="ribbon-colorBox" style={{ backgroundColor: viewedFontColor, height: 15, width: 15 }} onClick={action(() => this.openViewedColorPicker = !this.openViewedColorPicker)}> </div> </div> {this.viewedColorPicker} {/* <div className="ribbon-doubleButton" style={{ borderTop: 'solid 1px darkgrey', display: (targetDoc.type === DocumentType.COL && targetDoc._viewType === 'freeform') || targetDoc.type === DocumentType.IMG ? "inline-flex" : "none" }}> - <div className="ribbon-button" style={{ backgroundColor: activeItem.zoomProgressivize ? "#aedef8" : "" }} onClick={this.progressivizeZoom}>Zoom</div> - <div className="ribbon-button" style={{ opacity: activeItem.zoomProgressivize ? 1 : 0.4, backgroundColor: activeItem.editZoomProgressivize ? "#aedef8" : "" }} onClick={this.editZoomProgressivize}>Edit</div> + <div className="ribbon-toggle" style={{ backgroundColor: activeItem.zoomProgressivize ? "#aedef8" : "" }} onClick={this.progressivizeZoom}>Zoom</div> + <div className="ribbon-toggle" style={{ opacity: activeItem.zoomProgressivize ? 1 : 0.4, backgroundColor: activeItem.editZoomProgressivize ? "#aedef8" : "" }} onClick={this.editZoomProgressivize}>Edit</div> </div> */} <div className="ribbon-doubleButton" style={{ borderTop: 'solid 1px darkgrey', display: targetDoc._viewType === "stacking" || targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.WEB || targetDoc.type === DocumentType.RTF ? "inline-flex" : "none" }}> - <div className="ribbon-button" style={{ backgroundColor: activeItem.scrollProgressivize ? "#aedef8" : "" }} onClick={this.progressivizeScroll}>Scroll</div> - <div className="ribbon-button" style={{ opacity: activeItem.scrollProgressivize ? 1 : 0.4, backgroundColor: targetDoc.editScrollProgressivize ? "#aedef8" : "" }} onClick={this.editScrollProgressivize}>Edit</div> + <div className="ribbon-toggle" style={{ backgroundColor: activeItem.scrollProgressivize ? "#aedef8" : "" }} onClick={this.progressivizeScroll}>Scroll</div> + <div className="ribbon-toggle" style={{ opacity: activeItem.scrollProgressivize ? 1 : 0.4, backgroundColor: targetDoc.editScrollProgressivize ? "#aedef8" : "" }} onClick={this.editScrollProgressivize}>Edit</div> </div> </div> <div className="ribbon-final-box" style={{ display: activeItem.zoomProgressivize || activeItem.scrollProgressivize || activeItem.presProgressivize || activeItem.textProgressivize ? "grid" : "none" }}> @@ -1237,7 +1364,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> </div> <div key="num" title="toggle view all" className="numKeyframe" style={{ color: targetDoc.editing ? "white" : "black", backgroundColor: targetDoc.editing ? "#5B9FDD" : "#AEDDF8" }} onClick={action(() => targetDoc.editing = !targetDoc.editing)} > - {NumCast(targetDoc.currentFrame)} + {NumCast(targetDoc._currentFrame)} </div> <div key="fwd" title="forward frame" className="fwdKeyframe" onClick={e => { e.stopPropagation(); this.nextKeyframe(targetDoc, activeItem); }}> <FontAwesomeIcon icon={"caret-right"} size={"lg"} /> @@ -1245,7 +1372,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> </div> <Tooltip title={<><div className="dash-tooltip">{"Last frame"}</div></>}><div className="ribbon-property">{NumCast(targetDoc.lastFrame)}</div></Tooltip> </div> - <div className="ribbon-button" style={{ height: 20, backgroundColor: "#AEDDF8" }} onClick={() => console.log(" TODO: play frames")}>Play</div> + <div className="ribbon-toggle" style={{ height: 20, backgroundColor: "#AEDDF8" }} onClick={() => console.log(" TODO: play frames")}>Play</div> </div> </div> </div> @@ -1256,8 +1383,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @undoBatch @action switchActive = (color: ColorState) => { - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; const val = String(color.hex); targetDoc["pres-text-color"] = val; return true; @@ -1265,16 +1392,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @undoBatch @action switchPresented = (color: ColorState) => { - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; const val = String(color.hex); targetDoc["pres-text-viewed-color"] = val; return true; } @computed get activeColorPicker() { - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; return !this.openActiveColorPicker ? (null) : <SketchPicker onChange={this.switchActive} presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', @@ -1283,8 +1410,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> } @computed get viewedColorPicker() { - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; return !this.openViewedColorPicker ? (null) : <SketchPicker onChange={this.switchPresented} presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', @@ -1292,15 +1419,21 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> color={StrCast(targetDoc["pres-text-viewed-color"])} />; } - turnOffEdit = () => { + @action + turnOffEdit = (paths?: boolean) => { + if (paths) { + // Turn off paths + const srcContext = Cast(this.rootDoc.presCollection, Doc, null); + if (srcContext) this.togglePath(srcContext, true); + } + // Turn off the progressivize editors for each this.childDocs.forEach((doc) => { doc.editSnapZoomProgressivize = false; doc.editZoomProgressivize = false; - doc.editScrollProgressivize = false; const targetDoc = Cast(doc.presentationTargetDoc, Doc, null); if (targetDoc) { targetDoc.editZoomProgressivize = false; - targetDoc.editScrollProgressivize = false; + // targetDoc.editScrollProgressivize = false; } }); } @@ -1308,8 +1441,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> //Toggle whether the user edits or not @action editZoomProgressivize = (e: React.MouseEvent) => { - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; if (!targetDoc.editZoomProgressivize) { if (!activeItem.zoomProgressivize) activeItem.zoomProgressivize = true; targetDoc.zoomProgressivize = true; targetDoc.editZoomProgressivize = true; @@ -1323,8 +1456,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> //Toggle whether the user edits or not @action editScrollProgressivize = (e: React.MouseEvent) => { - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; if (!targetDoc.editScrollProgressivize) { if (!targetDoc.scrollProgressivize) { targetDoc.scrollProgressivize = true; activeItem.scrollProgressivize = true; } targetDoc.editScrollProgressivize = true; @@ -1337,14 +1470,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @action progressivizeScroll = (e: React.MouseEvent) => { e.stopPropagation(); - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + const activeItem: Doc = this.activeItem; activeItem.scrollProgressivize = !activeItem.scrollProgressivize; - const targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); + const targetDoc: Doc = this.targetDoc; targetDoc.scrollProgressivize = !targetDoc.scrollProgressivize; - CollectionFreeFormDocumentView.setupScroll(targetDoc, NumCast(targetDoc.currentFrame), true); + CollectionFreeFormDocumentView.setupScroll(targetDoc, NumCast(targetDoc._currentFrame)); if (targetDoc.editScrollProgressivize) { targetDoc.editScrollProgressivize = false; - targetDoc.currentFrame = 0; + targetDoc._currentFrame = 0; targetDoc.lastFrame = 0; } } @@ -1353,14 +1486,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @action progressivizeZoom = (e: React.MouseEvent) => { e.stopPropagation(); - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + const activeItem: Doc = this.activeItem; activeItem.zoomProgressivize = !activeItem.zoomProgressivize; - const targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); + const targetDoc: Doc = this.targetDoc; targetDoc.zoomProgressivize = !targetDoc.zoomProgressivize; - CollectionFreeFormDocumentView.setupZoom(activeItem, targetDoc, true); + CollectionFreeFormDocumentView.setupZoom(activeItem, targetDoc); if (activeItem.editZoomProgressivize) { activeItem.editZoomProgressivize = false; - targetDoc.currentFrame = 0; + targetDoc._currentFrame = 0; targetDoc.lastFrame = 0; } } @@ -1368,9 +1501,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> //Progressivize Child Docs @action editProgressivize = (e: React.MouseEvent) => { - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); - targetDoc.currentFrame = targetDoc.lastFrame; + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; + targetDoc._currentFrame = targetDoc.lastFrame; if (!targetDoc.editProgressivize) { if (!activeItem.presProgressivize) { activeItem.presProgressivize = true; targetDoc.presProgressivize = true; } targetDoc.editProgressivize = true; @@ -1382,22 +1515,21 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @action progressivizeChild = (e: React.MouseEvent) => { e.stopPropagation(); - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; const docs = DocListCast(targetDoc[Doc.LayoutFieldKey(targetDoc)]); if (!activeItem.presProgressivize) { targetDoc.editing = false; activeItem.presProgressivize = true; targetDoc.presProgressivize = true; - targetDoc.currentFrame = 0; - CollectionFreeFormDocumentView.setupKeyframes(docs, docs.length, true); - targetDoc.lastFrame = docs.length - 1; + targetDoc._currentFrame = 0; + docs.forEach((doc, i) => CollectionFreeFormDocumentView.setupKeyframes([doc], i, true)); + targetDoc.lastFrame = targetDoc.lastFrame ? NumCast(targetDoc.lastFrame) : docs.length - 1; } else { targetDoc.editProgressivize = false; activeItem.presProgressivize = false; targetDoc.presProgressivize = false; - targetDoc.currentFrame = 0; - targetDoc.lastFrame = 0; + targetDoc._currentFrame = 0; targetDoc.editing = true; } } @@ -1438,18 +1570,17 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @action checkList = (doc: Doc, list: any): number => { const x: List<number> = list; - if (x && x.length >= NumCast(doc.currentFrame) + 1) { - return x[NumCast(doc.currentFrame)]; + if (x && x.length >= NumCast(doc._currentFrame) + 1) { + return x[NumCast(doc._currentFrame)]; } else if (x) { - x.length = NumCast(doc.currentFrame) + 1; - x[NumCast(doc.currentFrame)] = x[NumCast(doc.currentFrame) - 1]; - return x[NumCast(doc.currentFrame)]; + x.length = NumCast(doc._currentFrame) + 1; + x[NumCast(doc._currentFrame)] = x[NumCast(doc._currentFrame) - 1]; + return x[NumCast(doc._currentFrame)]; } else return 100; } @computed get progressivizeChildDocs() { - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); + const targetDoc: Doc = this.targetDoc; const docs = DocListCast(targetDoc[Doc.LayoutFieldKey(targetDoc)]); const tags: JSX.Element[] = []; docs.forEach((doc, index) => { @@ -1457,7 +1588,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> tags.push(<div style={{ position: 'absolute', display: doc.displayMovement ? "block" : "none" }}>{this.checkMovementLists(doc, doc["x-indexed"], doc["y-indexed"])}</div>); } tags.push( - <div className="progressivizeButton" onPointerLeave={() => { if (NumCast(targetDoc.currentFrame) < NumCast(doc.appearFrame)) doc.opacity = 0; }} onPointerOver={() => { if (NumCast(targetDoc.currentFrame) < NumCast(doc.appearFrame)) doc.opacity = 0.5; }} onClick={e => { this.toggleDisplayMovement(doc); e.stopPropagation(); }} style={{ backgroundColor: doc.displayMovement ? "#aedff8" : "#c8c8c8", top: NumCast(doc.y), left: NumCast(doc.x) }}> + <div className="progressivizeButton" key={index} onPointerLeave={() => { if (NumCast(targetDoc._currentFrame) < NumCast(doc.appearFrame)) doc.opacity = 0; }} onPointerOver={() => { if (NumCast(targetDoc._currentFrame) < NumCast(doc.appearFrame)) doc.opacity = 0.5; }} onClick={e => { this.toggleDisplayMovement(doc); e.stopPropagation(); }} style={{ backgroundColor: doc.displayMovement ? "#aedff8" : "#c8c8c8", top: NumCast(doc.y), left: NumCast(doc.x) }}> <div className="progressivizeButton-prev"><FontAwesomeIcon icon={"caret-left"} size={"lg"} onClick={e => { e.stopPropagation(); this.prevAppearFrame(doc, index); }} /></div> <div className="progressivizeButton-frame">{doc.appearFrame}</div> <div className="progressivizeButton-next"><FontAwesomeIcon icon={"caret-right"} size={"lg"} onClick={e => { e.stopPropagation(); this.nextAppearFrame(doc, index); }} /></div> @@ -1469,8 +1600,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @undoBatch @action nextAppearFrame = (doc: Doc, i: number): void => { - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); + // const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + // const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); const appearFrame = Cast(doc.appearFrame, "number", null); if (appearFrame === undefined) { doc.appearFrame = 0; @@ -1482,8 +1613,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @undoBatch @action prevAppearFrame = (doc: Doc, i: number): void => { - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); + // const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + // const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); const appearFrame = Cast(doc.appearFrame, "number", null); if (appearFrame === undefined) { doc.appearFrame = 0; @@ -1604,13 +1735,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> } @computed get playButtonFrames() { - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); + const targetDoc: Doc = this.targetDoc; return ( <> - {targetDoc ? <div className="miniPres-button-frame" style={{ display: targetDoc.lastFrame !== undefined && targetDoc.lastFrame >= 0 ? "inline-flex" : "none" }}> - <div>{targetDoc.currentFrame}</div> - <div className="miniPres-divider" style={{ border: 'solid 0.5px white', height: '60%' }}></div> + {this.targetDoc ? <div className="presPanel-button-frame" style={{ display: targetDoc.lastFrame !== undefined && targetDoc.lastFrame >= 0 ? "inline-flex" : "none" }}> + <div>{targetDoc._currentFrame}</div> + <div className="presPanel-divider" style={{ border: 'solid 0.5px white', height: '60%' }}></div> <div>{targetDoc.lastFrame}</div> </div> : null} </> @@ -1620,8 +1750,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @computed get playButtons() { // Case 1: There are still other frames and should go through all frames before going to next slide return (<div className="presPanelOverlay" style={{ display: this.layoutDoc.presStatus !== "edit" ? "inline-flex" : "none" }}> + <Tooltip title={<><div className="dash-tooltip">{"Loop"}</div></>}><div className="presPanel-button" style={{ color: this.layoutDoc.presLoop ? '#AEDDF8' : 'white' }} onClick={() => this.layoutDoc.presLoop = !this.layoutDoc.presLoop}><FontAwesomeIcon icon={"redo-alt"} /></div></Tooltip> + <div className="presPanel-divider"></div> <div className="presPanel-button" onClick={this.back}><FontAwesomeIcon icon={"arrow-left"} /></div> - <div className="presPanel-button" onClick={() => this.startAutoPres(this.itemIndex)}><FontAwesomeIcon icon={this.layoutDoc.presStatus === "auto" ? "pause" : "play"} /></div> + <Tooltip title={<><div className="dash-tooltip">{this.layoutDoc.presStatus === "auto" ? "Pause" : "Autoplay"}</div></>}><div className="presPanel-button" onClick={this.startOrPause}><FontAwesomeIcon icon={this.layoutDoc.presStatus === "auto" ? "pause" : "play"} /></div></Tooltip> <div className="presPanel-button" onClick={this.next}><FontAwesomeIcon icon={"arrow-right"} /></div> <div className="presPanel-divider"></div> <div className="presPanel-button-text" style={{ display: this.props.PanelWidth() > 250 ? "inline-flex" : "none" }}> @@ -1629,13 +1761,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> {this.playButtonFrames} </div> <div className="presPanel-divider"></div> - {this.props.PanelWidth() > 250 ? <div className="presPanel-button-text" onClick={() => this.layoutDoc.presStatus = "edit"}>EXIT</div> + {this.props.PanelWidth() > 250 ? <div className="presPanel-button-text" onClick={() => { this.layoutDoc.presStatus = "edit"; clearTimeout(this._presTimer); }}>EXIT</div> : <div className="presPanel-button" onClick={() => this.layoutDoc.presStatus = "edit"}> <FontAwesomeIcon icon={"times"} /> </div>} </div>); } + @action + startOrPause = () => { + if (this.layoutDoc.presStatus === "manual" || this.layoutDoc.presStatus === "edit") this.startAutoPres(this.itemIndex); + else this.pauseAutoPres(); + } + render() { // calling this method for keyEvents this.isPres; @@ -1643,19 +1781,21 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> this.childDocs.slice(); const mode = StrCast(this.rootDoc._viewType) as CollectionViewType; return this.layoutDoc.inOverlay ? - <div className="miniPres" style={{ width: 250, height: 35, background: '#323232', top: 0, zIndex: 3000000 }}> - {<div className="miniPresOverlay"> - <div className="miniPres-button" onClick={this.back}><FontAwesomeIcon icon={"arrow-left"} /></div> - <div className="miniPres-button" onClick={() => this.startAutoPres(this.itemIndex)}><FontAwesomeIcon icon={this.layoutDoc.presStatus === "auto" ? "pause" : "play"} /></div> - <div className="miniPres-button" onClick={this.next}><FontAwesomeIcon icon={"arrow-right"} /></div> - <div className="miniPres-divider"></div> - <div className="miniPres-button-text"> + <div className="miniPres"> + <div className="presPanelOverlay" style={{ display: "inline-flex", height: 35, background: '#323232', top: 0, zIndex: 3000000 }}> + <Tooltip title={<><div className="dash-tooltip">{"Loop"}</div></>}><div className="presPanel-button" style={{ color: this.layoutDoc.presLoop ? '#AEDDF8' : undefined }} onClick={() => this.layoutDoc.presLoop = !this.layoutDoc.presLoop}><FontAwesomeIcon icon={"redo-alt"} /></div></Tooltip> + <div className="presPanel-divider"></div> + <div className="presPanel-button" onClick={this.back}><FontAwesomeIcon icon={"arrow-left"} /></div> + <Tooltip title={<><div className="dash-tooltip">{this.layoutDoc.presStatus === "auto" ? "Pause" : "Autoplay"}</div></>}><div className="presPanel-button" onClick={this.startOrPause}><FontAwesomeIcon icon={this.layoutDoc.presStatus === "auto" ? "pause" : "play"} /></div></Tooltip> + <div className="presPanel-button" onClick={this.next}><FontAwesomeIcon icon={"arrow-right"} /></div> + <div className="presPanel-divider"></div> + <div className="presPanel-button-text"> Slide {this.itemIndex + 1} / {this.childDocs.length} {this.playButtonFrames} </div> - <div className="miniPres-divider"></div> - <div className="miniPres-button-text" onClick={this.updateMinimize}>EXIT</div> - </div>} + <div className="presPanel-divider"></div> + <div className="presPanel-button-text" onClick={this.updateMinimize}>EXIT</div> + </div> </div> : <div className="presBox-cont" style={{ minWidth: this.layoutDoc.inOverlay ? 240 : undefined }} > @@ -1668,6 +1808,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> ContainingCollectionDoc={this.props.Document} PanelWidth={this.props.PanelWidth} PanelHeight={this.panelHeight} + childIgnoreNativeSize={true} moveDocument={returnFalse} childOpacity={returnOne} childLayoutTemplate={this.childLayoutTemplate} diff --git a/src/client/views/nodes/RadialMenuItem.tsx b/src/client/views/nodes/RadialMenuItem.tsx index bd5b3bff4..8876b4879 100644 --- a/src/client/views/nodes/RadialMenuItem.tsx +++ b/src/client/views/nodes/RadialMenuItem.tsx @@ -1,13 +1,9 @@ import React = require("react"); -import { observable, action } from "mobx"; -import { observer } from "mobx-react"; -import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; -import { faAngleRight } from '@fortawesome/free-solid-svg-icons'; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { observer } from "mobx-react"; import { UndoManager } from "../../util/UndoManager"; -library.add(faAngleRight); - export interface RadialMenuProps { description: string; event: (stuff?: any) => void; diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx index 1cd29d795..16ce749bc 100644 --- a/src/client/views/nodes/ScreenshotBox.tsx +++ b/src/client/views/nodes/ScreenshotBox.tsx @@ -1,16 +1,15 @@ import React = require("react"); -import { library } from "@fortawesome/fontawesome-svg-core"; -import { faVideo } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, computed, IReactionDisposer, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; import * as rp from 'request-promise'; import { Doc } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; +import { InkTool } from "../../../fields/InkField"; import { listSpec, makeInterface } from "../../../fields/Schema"; import { Cast, NumCast } from "../../../fields/Types"; import { VideoField } from "../../../fields/URLField"; -import { emptyFunction, returnFalse, returnOne, returnZero, Utils } from "../../../Utils"; +import { emptyFunction, returnFalse, returnOne, returnZero, Utils, OmitKeys } from "../../../Utils"; import { Docs } from "../../documents/Documents"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; import { ContextMenu } from "../ContextMenu"; @@ -18,14 +17,11 @@ import { ContextMenuProps } from "../ContextMenuItem"; import { ViewBoxBaseComponent } from "../DocComponent"; import { FieldView, FieldViewProps } from './FieldView'; import "./ScreenshotBox.scss"; -import { InkTool } from "../../../fields/InkField"; const path = require('path'); type ScreenshotDocument = makeInterface<[typeof documentSchema]>; const ScreenshotDocument = makeInterface(documentSchema); -library.add(faVideo); - @observer export class ScreenshotBox extends ViewBoxBaseComponent<FieldViewProps, ScreenshotDocument>(ScreenshotDocument) { private _reactionDisposer?: IReactionDisposer; @@ -77,7 +73,7 @@ export class ScreenshotBox extends ViewBoxBaseComponent<FieldViewProps, Screensh const spt = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); imageSummary.x = spt[0]; imageSummary.y = spt[1]; - Cast(Cast(Doc.UserDoc().myOverlayDocuments, Doc, null)?.data, listSpec(Doc), []).push(imageSummary); + Cast(Cast(Doc.UserDoc().myOverlayDocs, Doc, null)?.data, listSpec(Doc), []).push(imageSummary); } else { this.props.addDocument?.(imageSummary); } @@ -173,11 +169,9 @@ export class ScreenshotBox extends ViewBoxBaseComponent<FieldViewProps, Screensh return (<div className="videoBox" onContextMenu={this.specificContextMenu} style={{ transform: `scale(${this.props.ContentScaling()})`, width: `${100 / this.props.ContentScaling()}%`, height: `${100 / this.props.ContentScaling()}%` }} > <div className="videoBox-viewer" > - <CollectionFreeFormView {...this.props} + <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} PanelHeight={this.props.PanelHeight} PanelWidth={this.props.PanelWidth} - NativeHeight={returnZero} - NativeWidth={returnZero} annotationsKey={""} focus={this.props.focus} isSelected={this.props.isSelected} diff --git a/src/client/views/nodes/SliderBox.tsx b/src/client/views/nodes/SliderBox.tsx index 45cdfc5ad..d13680046 100644 --- a/src/client/views/nodes/SliderBox.tsx +++ b/src/client/views/nodes/SliderBox.tsx @@ -1,5 +1,3 @@ -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faEdit } from '@fortawesome/free-regular-svg-icons'; import { runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -16,9 +14,6 @@ import { FieldView, FieldViewProps } from './FieldView'; import { Handle, Tick, TooltipRail, Track } from './SliderBox-components'; import './SliderBox.scss'; - -library.add(faEdit as any); - const SliderSchema = createSchema({ _sliderMin: "number", _sliderMax: "number", diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index ee92e517c..998ddde9a 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -8,7 +8,7 @@ import { InkTool } from "../../../fields/InkField"; import { createSchema, makeInterface } from "../../../fields/Schema"; import { Cast, StrCast } from "../../../fields/Types"; import { VideoField } from "../../../fields/URLField"; -import { Utils, emptyFunction, returnOne, returnZero } from "../../../Utils"; +import { Utils, emptyFunction, returnOne, returnZero, OmitKeys } from "../../../Utils"; import { Docs, DocUtils } from "../../documents/Documents"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; import { ContextMenu } from "../ContextMenu"; @@ -23,7 +23,7 @@ import { SnappingManager } from "../../util/SnappingManager"; const path = require('path'); export const timeSchema = createSchema({ - currentTimecode: "number", // the current time of a video or other linear, time-based document. Note, should really get set on an extension field, but that's more complicated when it needs to be set since the extension doc needs to be found first + _currentTimecode: "number", // the current time of a video or other linear, time-based document. Note, should really get set on an extension field, but that's more complicated when it needs to be set since the extension doc needs to be found first }); type VideoDocument = makeInterface<[typeof documentSchema, typeof timeSchema]>; const VideoDocument = makeInterface(documentSchema, timeSchema); @@ -82,7 +82,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD @action public FullScreen() { this._fullScreen = true; this.player && this.player.requestFullscreen(); - this._youtubePlayer && this.props.addDocTab(this.rootDoc, "inTab"); + this._youtubePlayer && this.props.addDocTab(this.rootDoc, "add"); } choosePath(url: string) { @@ -109,14 +109,14 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD if (!this._videoRef) { const b = Docs.Create.LabelDocument({ x: (this.layoutDoc.x || 0) + width, y: (this.layoutDoc.y || 1), - _width: 150, _height: 50, title: (this.layoutDoc.currentTimecode || 0).toString(), + _width: 150, _height: 50, title: (this.layoutDoc._currentTimecode || 0).toString(), }); b.isLinkButton = true; this.props.addDocument?.(b); DocUtils.MakeLink({ doc: b }, { doc: this.rootDoc }, "video snapshot"); Networking.PostToServer("/youtubeScreenshot", { id: this.youtubeVideoId, - timecode: this.layoutDoc.currentTimecode + timecode: this.layoutDoc._currentTimecode }).then(response => { const resolved = response?.accessPaths?.agnostic?.client; if (resolved) { @@ -128,7 +128,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD //convert to desired file format const dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png' // if you want to preview the captured image, - const filename = path.basename(encodeURIComponent("snapshot" + StrCast(this.rootDoc.title).replace(/\..*$/, "") + "_" + (this.layoutDoc.currentTimecode || 0).toString().replace(/\./, "_"))); + const filename = path.basename(encodeURIComponent("snapshot" + StrCast(this.rootDoc.title).replace(/\..*$/, "") + "_" + (this.layoutDoc._currentTimecode || 0).toString().replace(/\./, "_"))); VideoBox.convertDataUri(dataUrl, filename).then((returnedFilename: string) => { if (returnedFilename) { this.createRealSummaryLink(returnedFilename); @@ -144,7 +144,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD const imageSummary = Docs.Create.ImageDocument(url, { _nativeWidth: this.layoutDoc._nativeWidth, _nativeHeight: this.layoutDoc._nativeHeight, x: (this.layoutDoc.x || 0) + width, y: (this.layoutDoc.y || 0), - _width: 150, _height: height / width * 150, title: "--snapshot" + (this.layoutDoc.currentTimecode || 0) + " image-" + _width: 150, _height: height / width * 150, title: "--snapshot" + (this.layoutDoc._currentTimecode || 0) + " image-" }); Doc.GetProto(imageSummary)["data-nativeWidth"] = this.layoutDoc._nativeWidth; Doc.GetProto(imageSummary)["data-nativeHeight"] = this.layoutDoc._nativeHeight; @@ -155,8 +155,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD @action updateTimecode = () => { - this.player && (this.layoutDoc.currentTimecode = this.player.currentTime); - this._youtubePlayer && (this.layoutDoc.currentTimecode = this._youtubePlayer.getCurrentTime()); + this.player && (this.layoutDoc._currentTimecode = this.player.currentTime); + this._youtubePlayer && (this.layoutDoc._currentTimecode = this._youtubePlayer.getCurrentTime()); } componentDidMount() { @@ -185,9 +185,10 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD this._videoRef = vref; if (vref) { this._videoRef!.ontimeupdate = this.updateTimecode; + // @ts-ignore vref.onfullscreenchange = action((e) => this._fullScreen = vref.webkitDisplayingFullscreen); - this._reactionDisposer && this._reactionDisposer(); - this._reactionDisposer = reaction(() => (this.layoutDoc.currentTimecode || 0), + this._reactionDisposer?.(); + this._reactionDisposer = reaction(() => (this.layoutDoc._currentTimecode || 0), time => !this._playing && (vref.currentTime = time), { fireImmediately: true }); } } @@ -272,21 +273,21 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD const onYoutubePlayerReady = (event: any) => { this._reactionDisposer?.(); this._youtubeReactionDisposer?.(); - this._reactionDisposer = reaction(() => this.layoutDoc.currentTimecode, () => !this._playing && this.Seek((this.layoutDoc.currentTimecode || 0))); + this._reactionDisposer = reaction(() => this.layoutDoc._currentTimecode, () => !this._playing && this.Seek((this.layoutDoc._currentTimecode || 0))); this._youtubeReactionDisposer = reaction( - () => Doc.GetSelectedTool() === InkTool.None && this.props.isSelected(true) && !SnappingManager.GetIsDragging() && !DocumentDecorations.Instance.Interacting, + () => !this.props.Document.isAnnotating && Doc.GetSelectedTool() === InkTool.None && this.props.isSelected(true) && !SnappingManager.GetIsDragging() && !DocumentDecorations.Instance.Interacting, (interactive) => iframe.style.pointerEvents = interactive ? "all" : "none", { fireImmediately: true }); }; this._youtubePlayer = new YT.Player(`${this.youtubeVideoId + this._youtubeIframeId}-player`, { events: { - 'onReady': onYoutubePlayerReady, - 'onStateChange': onYoutubePlayerStateChange, + 'onReady': this.props.dontRegisterView ? undefined : onYoutubePlayerReady, + 'onStateChange': this.props.dontRegisterView ? undefined : onYoutubePlayerStateChange, } }); } private get uIButtons() { - const curTime = (this.layoutDoc.currentTimecode || 0); + const curTime = (this.layoutDoc._currentTimecode || 0); return ([<div className="videoBox-time" key="time" onPointerDown={this.onResetDown} > <span>{"" + Math.round(curTime)}</span> <span style={{ fontSize: 8 }}>{" " + Math.round((curTime - Math.trunc(curTime)) * 100)}</span> @@ -328,7 +329,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD onResetMove = (e: PointerEvent) => { this._isResetClick += Math.abs(e.movementX) + Math.abs(e.movementY); - this.Seek(Math.max(0, (this.layoutDoc.currentTimecode || 0) + Math.sign(e.movementX) * 0.0333)); + this.Seek(Math.max(0, (this.layoutDoc._currentTimecode || 0) + Math.sign(e.movementX) * 0.0333)); e.stopImmediatePropagation(); } @@ -336,14 +337,14 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD onResetUp = (e: PointerEvent) => { document.removeEventListener("pointermove", this.onResetMove, true); document.removeEventListener("pointerup", this.onResetUp, true); - this._isResetClick < 10 && (this.layoutDoc.currentTimecode = 0); + this._isResetClick < 10 && (this.layoutDoc._currentTimecode = 0); } @computed get youtubeContent() { this._youtubeIframeId = VideoBox._youtubeIframeCounter++; this._youtubeContentCreated = this._forceCreateYouTubeIFrame ? true : true; const style = "videoBox-content-YouTube" + (this._fullScreen ? "-fullScreen" : ""); - const start = untracked(() => Math.round((this.layoutDoc.currentTimecode || 0))); + const start = untracked(() => Math.round((this.layoutDoc._currentTimecode || 0))); return <iframe key={this._youtubeIframeId} id={`${this.youtubeVideoId + this._youtubeIframeId}-player`} onLoad={this.youtubeIframeLoaded} className={`${style}`} width={(this.layoutDoc._nativeWidth || 640)} height={(this.layoutDoc._nativeHeight || 390)} src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=0&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._showControls ? 1 : 0}`} />; @@ -353,7 +354,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD addDocumentWithTimestamp(doc: Doc | Doc[]): boolean { const docs = doc instanceof Doc ? [doc] : doc; docs.forEach(doc => { - const curTime = (this.layoutDoc.currentTimecode || -1); + const curTime = (this.layoutDoc._currentTimecode || -1); curTime !== -1 && (doc.displayTimecode = curTime); }); return this.addDocument(doc); @@ -364,11 +365,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD return (<div className="videoBox" onContextMenu={this.specificContextMenu} style={{ transform: `scale(${this.props.ContentScaling()})`, width: `${100 / this.props.ContentScaling()}%`, height: `${100 / this.props.ContentScaling()}%` }} > <div className="videoBox-viewer" > - <CollectionFreeFormView {...this.props} + <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} PanelHeight={this.props.PanelHeight} PanelWidth={this.props.PanelWidth} - NativeHeight={returnZero} - NativeWidth={returnZero} annotationsKey={this.annotationKey} focus={this.props.focus} isSelected={this.props.isSelected} @@ -384,6 +383,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD ScreenToLocalTransform={this.props.ScreenToLocalTransform} renderDepth={this.props.renderDepth + 1} docFilters={this.props.docFilters} + docRangeFilters={this.props.docRangeFilters} + searchFilterDocs={this.props.searchFilterDocs} ContainingCollectionDoc={this.props.ContainingCollectionDoc}> {this.contentFunc} </CollectionFreeFormView> diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss index 875142169..ea822f553 100644 --- a/src/client/views/nodes/WebBox.scss +++ b/src/client/views/nodes/WebBox.scss @@ -2,15 +2,16 @@ .webBox { - height:100%; + height: 100%; position: relative; display: flex; .pdfViewerDash-dragAnnotationBox { - position:absolute; + position: absolute; background-color: transparent; opacity: 0.1; } + .webBox-annotationLayer { position: absolute; transform-origin: left top; @@ -19,10 +20,12 @@ pointer-events: none; mix-blend-mode: multiply; // bcz: makes text fuzzy! } + .webBox-annotationBox { position: absolute; background-color: rgba(245, 230, 95, 0.616); } + .webBox-container { transform-origin: top left; width: 100%; @@ -33,10 +36,13 @@ top: 0; left: 0; } + .webBox-cont { pointer-events: none; } - .webBox-cont, .webBox-cont-interactive { + + .webBox-cont, + .webBox-cont-interactive { padding: 0vw; position: absolute; top: 0; @@ -45,18 +51,21 @@ height: 100%; transform-origin: top left; overflow: auto; + .webBox-iframe { width: 100%; height: 100%; position: absolute; - top:0; + top: 0; } } + .webBox-cont-interactive { span { user-select: text !important; } } + .webBox-outerContent { width: 100%; height: 100%; @@ -65,12 +74,12 @@ left: 0; overflow: auto; } + div.webBox-outerContent::-webkit-scrollbar-thumb { - display:none; + display: none; } } - .webBox-overlay { width: 100%; height: 100%; @@ -79,64 +88,31 @@ .webBox-buttons { margin-left: 44; - background:lightGray; + background: lightGray; width: 100%; } - .webBox-freeze { - display: flex; - align-items: center; - justify-content: center; - margin-right: 5px; - width: 30px; - } - .webBox-urlEditor { - position: relative; - opacity: 0.9; + .webBox-annotationToggle { z-index: 9001; - transition: top .5s; - - .urlEditor { - display: grid; - grid-template-columns: 1fr auto; - padding-bottom: 10px; - overflow: hidden; - - .editorBase { - display: flex; - - .editor-collapse { - transition: all .5s, opacity 0.3s; - position: absolute; - width: 40px; - transform-origin: top left; - } - - .switchToText { - color: $main-accent; - } - - .switchToText:hover { - color: $dark-color; - } - } + position: absolute; + top: 2; + left: 2; + cursor: pointer; + box-shadow: black 0.3em 0.3em 1em; + border-radius: 5px; + display: flex; + opacity: 0.3; + width: 25px; + height: 25px; + align-items: center; - button:hover { - transform: scale(1); - } + >svg { + margin: auto; } } - .webpage-urlInput { - padding: 12px 10px 11px 10px; - border: 0px; - color: grey; - letter-spacing: 2px; - outline-color: black; - background: rgb(238, 238, 238); - width: 100%; - margin-right: 10px; - height: 100%; + .webBox-annotationToggle:hover { + opacity: 1; } .touch-iframe-overlay { diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 1393e7868..421aac69f 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -4,7 +4,7 @@ import { action, computed, IReactionDisposer, observable, reaction, runInAction import { observer } from "mobx-react"; import { Dictionary } from "typescript-collections"; import * as WebRequest from 'web-request'; -import { Doc, DocListCast, Opt, AclAddonly, AclEdit, AclAdmin, DataSym } from "../../../fields/Doc"; +import { Doc, DocListCast, Opt, AclAddonly, AclEdit, AclAdmin, DataSym, HeightSym, WidthSym } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; import { Id } from "../../../fields/FieldSymbols"; import { HtmlField } from "../../../fields/HtmlField"; @@ -14,7 +14,7 @@ import { listSpec, makeInterface } from "../../../fields/Schema"; import { Cast, NumCast, StrCast } from "../../../fields/Types"; import { WebField } from "../../../fields/URLField"; import { TraceMobx, GetEffectiveAcl } from "../../../fields/util"; -import { addStyleSheet, clearStyleSheetRules, emptyFunction, returnOne, returnZero, Utils, returnTrue } from "../../../Utils"; +import { addStyleSheet, clearStyleSheetRules, emptyFunction, returnOne, returnZero, Utils, returnTrue, OmitKeys } from "../../../Utils"; import { Docs, DocUtils } from "../../documents/Documents"; import { DragManager } from "../../util/DragManager"; import { ImageUtils } from "../../util/Import & Export/ImageUtils"; @@ -24,13 +24,16 @@ import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; import { ViewBoxAnnotatableComponent } from "../DocComponent"; import { DocumentDecorations } from "../DocumentDecorations"; -import Annotation from "../pdf/Annotation"; -import PDFMenu from "../pdf/PDFMenu"; +import { Annotation } from "../pdf/Annotation"; +import { PDFMenu } from "../pdf/PDFMenu"; import { PdfViewerMarquee } from "../pdf/PDFViewer"; import { FieldView, FieldViewProps } from './FieldView'; import "./WebBox.scss"; import "../pdf/PDFViewer.scss"; import React = require("react"); +import { Tooltip } from '@material-ui/core'; +import { CurrentUserUtils } from '../../util/CurrentUserUtils'; +import { FormattedTextBox } from './formattedText/FormattedTextBox'; const htmlToText = require("html-to-text"); type WebDocument = makeInterface<[typeof documentSchema]>; @@ -67,6 +70,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum private _iframeDragRef = React.createRef<HTMLDivElement>(); private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean) => void); + constructor(props: any) { + super(props); + this.dataDoc[this.fieldKey + "-nativeWidth"] = this.Document._nativeWidth = NumCast(this.dataDoc[this.props.fieldKey + "-nativeWidth"], NumCast(this.Document._nativeWidth, 850)); + this.dataDoc[this.fieldKey + "-nativeHeight"] = this.Document._nativeHeight = NumCast(this.dataDoc[this.props.fieldKey + "-nativeHeight"], NumCast(this.Document._nativeHeight, this.Document[HeightSym]() / this.Document[WidthSym]() * 850)); + } + iframeLoaded = action((e: any) => { const iframe = this._iframeRef.current; if (iframe && iframe.contentDocument) { @@ -100,10 +109,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum this._setPreviewCursor?.(e.screenX, e.screenY, false); } iframeScrolled = (e: any) => { - const scrollTop = e.target?.children?.[0].scrollTop; - const scrollLeft = e.target?.children?.[0].scrollLeft; - this.layoutDoc._scrollTop = this._outerRef.current!.scrollTop = scrollTop; - this.layoutDoc._scrollLeft = this._outerRef.current!.scrollLeft = scrollLeft; + if (e.target?.children) { + e.target.children[0].scrollLeft = 0; + const scrollTop = e.target.children[0].scrollTop; + const scrollLeft = e.target.children[0].scrollLeft; + this.layoutDoc._scrollTop = this._outerRef.current!.scrollTop = scrollTop; + this.layoutDoc._scrollLeft = this._outerRef.current!.scrollLeft = scrollLeft; + } } async componentDidMount() { const urlField = Cast(this.dataDoc[this.props.fieldKey], WebField); @@ -235,81 +247,16 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum e.stopPropagation(); } - toggleAnnotationMode = () => { - this.layoutDoc.isAnnotating = !this.layoutDoc.isAnnotating; - } - - urlEditor() { - return ( - <div className="webBox-urlEditor" - onDrop={this.onUrlDrop} - onDragOver={this.onUrlDragover} style={{ top: this._collapsed ? -70 : 0 }}> - <div className="urlEditor"> - <div className="editorBase"> - <button className="editor-collapse" - style={{ - top: this._collapsed ? 70 : 0, - transform: `rotate(${this._collapsed ? 180 : 0}deg) scale(${this._collapsed ? 0.5 : 1}) translate(${this._collapsed ? "-100%, -100%" : "0, 0"})`, - opacity: (this._collapsed && !this.props.isSelected()) ? 0 : 0.9, - left: (this._collapsed ? 0 : "unset"), - }} - title="Collapse Url Editor" onClick={this.toggleCollapse}> - <FontAwesomeIcon icon="caret-up" size="2x" /> - </button> - <div className="webBox-buttons" - onDrop={this.onUrlDrop} - onDragOver={this.onUrlDragover} style={{ display: this._collapsed ? "none" : "flex" }}> - <div className="webBox-freeze" title={"Annotate"} style={{ background: this.layoutDoc.isAnnotating ? "lightBlue" : "gray" }} onClick={this.toggleAnnotationMode} > - <FontAwesomeIcon icon={faPen} size={"2x"} /> - </div> - <div className="webBox-freeze" title={"Select"} style={{ background: !this.layoutDoc.isAnnotating ? "lightBlue" : "gray" }} onClick={this.toggleAnnotationMode} > - <FontAwesomeIcon icon={faMousePointer} size={"2x"} /> - </div> - <input className="webpage-urlInput" - placeholder="ENTER URL" - value={this._url} - onDrop={this.onUrlDrop} - onDragOver={this.onUrlDragover} - onChange={this.onURLChange} - onKeyDown={this.onValueKeyDown} - onClick={(e) => { - this._keyInput.current!.select(); - e.stopPropagation(); - }} - ref={this._keyInput} - /> - <div style={{ - display: "flex", - flexDirection: "row", - justifyContent: "space-between", - maxWidth: "120px", - }}> - <button className="submitUrl" onClick={this.submitURL} - onDragOver={this.onUrlDragover} onDrop={this.onUrlDrop}> - GO - </button> - <button className="submitUrl" onClick={this.back}> - <FontAwesomeIcon icon="caret-left" size="lg"></FontAwesomeIcon> - </button> - <button className="submitUrl" onClick={this.forward}> - <FontAwesomeIcon icon="caret-right" size="lg"></FontAwesomeIcon> - </button> - </div> - </div> - </div> - </div> + editToggleBtn() { + return <Tooltip title={<div className="dash-tooltip" >{`${this.props.Document.isAnnotating ? "Exit" : "Enter"} annotation mode`}</div>}> + <div className="webBox-annotationToggle" + style={{ color: this.props.Document.isAnnotating ? "black" : "white", backgroundColor: this.props.Document.isAnnotating ? "white" : "black" }} + onClick={action(() => this.layoutDoc.isAnnotating = !this.layoutDoc.isAnnotating)}> + <FontAwesomeIcon icon="edit" size="sm" /> </div> - ); + </Tooltip>; } - - @action - toggleCollapse = () => { - this._collapsed = !this._collapsed; - } - - - _ignore = 0; onPreWheel = (e: React.WheelEvent) => { this._ignore = e.timeStamp; @@ -433,12 +380,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum @undoBatch @action toggleNativeDimensions = () => { - Doc.toggleNativeDimensions(this.layoutDoc, this.props.ContentScaling(), this.props.NativeWidth(), this.props.NativeHeight()); + Doc.toggleNativeDimensions(this.layoutDoc, this.props.ContentScaling(), this.props.NativeWidth?.() || 0, this.props.NativeHeight?.() || 0); } specificContextMenu = (e: React.MouseEvent): void => { const cm = ContextMenu.Instance; const funcs: ContextMenuProps[] = []; - funcs.push({ description: (this.layoutDoc.UseCors ? "Don't Use" : "Use") + " Cors", event: () => this.layoutDoc.UseCors = !this.layoutDoc.UseCors, icon: "snowflake" }); + funcs.push({ description: (this.layoutDoc.useCors ? "Don't Use" : "Use") + " Cors", event: () => this.layoutDoc.useCors = !this.layoutDoc.useCors, icon: "snowflake" }); + funcs.push({ description: (this.layoutDoc[this.fieldKey + "-contentWidth"] ? "Unfreeze" : "Freeze") + " Content Width", event: () => this.layoutDoc[this.fieldKey + "-contentWidth"] = this.layoutDoc[this.fieldKey + "-contentWidth"] ? undefined : NumCast(this.layoutDoc._nativeWidth), icon: "snowflake" }); cm.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); } @@ -453,7 +401,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum if (field instanceof HtmlField) { view = <span className="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: field.html }} />; } else if (field instanceof WebField) { - const url = this.layoutDoc.UseCors ? Utils.CorsProxy(field.url.href) : field.url.href; + const url = this.layoutDoc.useCors ? Utils.CorsProxy(field.url.href) : field.url.href; + // view = <iframe className="webBox-iframe" src={url} onLoad={e => { e.currentTarget.before((e.currentTarget.contentDocument?.body || e.currentTarget.contentDocument)?.children[0]!); e.currentTarget.remove(); }} + view = <iframe className="webBox-iframe" enable-annotation={"true"} ref={this._iframeRef} src={url} onLoad={this.iframeLoaded} // the 'allow-top-navigation' and 'allow-top-navigation-by-user-activation' attributes are left out to prevent iframes from redirecting the top-level Dash page sandbox={"allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts"} />; @@ -465,25 +415,23 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum @computed get content() { const view = this.urlContent; - const decInteracting = DocumentDecorations.Instance?.Interacting; - const frozen = !this.props.isSelected() || decInteracting; + const frozen = !this.props.isSelected() || DocumentDecorations.Instance?.Interacting; return (<> - <div className={"webBox-cont" + (this.props.isSelected() && Doc.GetSelectedTool() === InkTool.None && !decInteracting ? "-interactive" : "")} - style={{ width: Number.isFinite(this.props.ContentScaling()) ? `${Math.max(100, 100 / this.props.ContentScaling())}% ` : "100%" }} + <div className={"webBox-cont" + (this.props.isSelected() && Doc.GetSelectedTool() === InkTool.None && !DocumentDecorations.Instance?.Interacting ? "-interactive" : "")} + style={{ width: NumCast(this.layoutDoc[this.fieldKey + "-contentWidth"]) || (Number.isFinite(this.props.ContentScaling()) ? `${Math.max(100, 100 / this.props.ContentScaling())}% ` : "100%") }} onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}> {view} - </div>; + </div> {!frozen ? (null) : - <div className="webBox-overlay" style={{ pointerEvents: this.layoutDoc.isBackground ? undefined : "all" }} + <div className="webBox-overlay" style={{ pointerEvents: this.layoutDoc._isBackground ? undefined : "all" }} onWheel={this.onPreWheel} onPointerDown={this.onPrePointer} onPointerMove={this.onPrePointer} onPointerUp={this.onPrePointer}> <div className="touch-iframe-overlay" onPointerDown={this.onLongPressDown} > <div className="indicator" ref={this._iframeIndicatorRef}></div> <div className="dragger" ref={this._iframeDragRef}></div> </div> </div>} - {this.urlEditor()} </>); } @@ -510,7 +458,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum TraceMobx(); return <div className="webBox-annotationLayer" style={{ height: NumCast(this.Document._nativeHeight) }} ref={this._annotationLayer}> {this.nonDocAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map(anno => - <Annotation {...this.props} focus={this.props.focus} dataDoc={this.dataDoc} fieldKey={this.props.fieldKey} anno={anno} key={`${anno[Id]}-annotation`} />) + <Annotation {...this.props} showInfo={emptyFunction} focus={this.props.focus} dataDoc={this.dataDoc} fieldKey={this.props.fieldKey} anno={anno} key={`${anno[Id]}-annotation`} />) } </div>; } @@ -538,7 +486,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum highlight = (color: string) => { // creates annotation documents for current highlights const effectiveAcl = GetEffectiveAcl(this.props.Document[DataSym]); - const annotationDoc = [AclAddonly, AclEdit, AclAdmin].includes(effectiveAcl) ? this.makeAnnotationDocument(color) : undefined; + const annotationDoc = [AclAddonly, AclEdit, AclAdmin].includes(effectiveAcl) ? this.makeAnnotationDocument(color.replace(/[0-9.]*\)/, "0.3)")) : undefined; annotationDoc && this.addDocument?.(annotationDoc); return annotationDoc ?? undefined; } @@ -551,22 +499,16 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum e.preventDefault(); e.stopPropagation(); - const clipDoc = Doc.MakeAlias(this.dataDoc); - clipDoc._fitWidth = true; - clipDoc._width = this.marqueeWidth(); - clipDoc._height = this.marqueeHeight(); - clipDoc._scrollTop = this.marqueeY(); - const targetDoc = Docs.Create.TextDocument("", { _width: 125, _height: 125, title: "Note linked to " + this.props.Document.title }); - Doc.GetProto(targetDoc).data = new List<Doc>([clipDoc]); - clipDoc.rootDocument = targetDoc; - targetDoc.layoutKey = "layout"; - const annotationDoc = this.highlight("rgba(146, 245, 95, 0.467)"); // yellowish highlight color when dragging out a text selection + const targetDoc = CurrentUserUtils.GetNewTextDoc("Note linked to " + this.props.Document.title, 0, 0, 125, 125); + FormattedTextBox.SelectOnLoad = targetDoc[Id]; + const annotationDoc = this.highlight("rgba(173, 216, 230, 0.35)"); // hyperlink color if (annotationDoc) { DragManager.StartPdfAnnoDrag([ele], new DragManager.PdfAnnoDragData(this.props.Document, annotationDoc, targetDoc), e.pageX, e.pageY, { dragComplete: e => { - if (!e.aborted && e.annoDragData && !e.annoDragData.linkedToDoc) { - DocUtils.MakeLink({ doc: annotationDoc }, { doc: e.annoDragData.dropDocument }, "Annotation"); + if (!e.aborted && e.annoDragData && !e.annoDragData.linkDocument) { + e.annoDragData.linkDocument = DocUtils.MakeLink({ doc: annotationDoc }, { doc: e.annoDragData.dropDocument }, "Annotation"); annotationDoc.isLinkButton = true; + annotationDoc.isPushpin = e.annoDragData?.dropDocument.annotationOn === this.props.Document; } } }); @@ -682,7 +624,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum transform: `scale(${this.props.ContentScaling()})`, width: Number.isFinite(this.props.ContentScaling()) ? `${100 / this.props.ContentScaling()}% ` : "100%", height: Number.isFinite(this.props.ContentScaling()) ? `${100 / this.props.ContentScaling()}% ` : "100%", - pointerEvents: this.layoutDoc.isBackground ? "none" : undefined + pointerEvents: this.layoutDoc._isBackground ? "none" : undefined }} onContextMenu={this.specificContextMenu}> <base target="_blank" /> @@ -690,7 +632,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum <div className={"webBox-outerContent"} ref={this._outerRef} style={{ width: Number.isFinite(this.props.ContentScaling()) ? `${Math.max(100, 100 / this.props.ContentScaling())}% ` : "100%", - pointerEvents: this.layoutDoc.isAnnotating && !this.layoutDoc.isBackground ? "all" : "none" + pointerEvents: this.layoutDoc.isAnnotating && !this.layoutDoc._isBackground ? "all" : "none" }} onWheel={e => e.stopPropagation()} onPointerDown={this.onMarqueeDown} @@ -709,14 +651,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum }}> <div className={"webBox-innerContent"} style={{ height: NumCast(this.layoutDoc.scrollHeight), - pointerEvents: this.layoutDoc.isBackground ? "none" : undefined + pointerEvents: this.layoutDoc._isBackground ? "none" : undefined }}> - <CollectionFreeFormView {...this.props} + <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} PanelHeight={this.props.PanelHeight} PanelWidth={this.props.PanelWidth} annotationsKey={this.annotationKey} - NativeHeight={returnZero} - NativeWidth={returnZero} VisibleHeight={this.visibleHeiht} focus={this.props.focus} setPreviewCursor={this.setPreviewCursor} @@ -733,6 +673,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum ScreenToLocalTransform={this.scrollXf} renderDepth={this.props.renderDepth + 1} docFilters={this.props.docFilters} + docRangeFilters={this.props.docRangeFilters} + searchFilterDocs={this.props.searchFilterDocs} ContainingCollectionDoc={this.props.ContainingCollectionDoc}> </CollectionFreeFormView> </div> @@ -740,6 +682,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum {this.annotationLayer} <PdfViewerMarquee isMarqueeing={this.marqueeing} width={this.marqueeWidth} height={this.marqueeHeight} x={this.marqueeX} y={this.marqueeY} /> </div > + + {this.props.isSelected() ? this.editToggleBtn() : null} </div>); } }
\ No newline at end of file diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx index 145ee8c2e..123946dea 100644 --- a/src/client/views/nodes/formattedText/DashDocView.tsx +++ b/src/client/views/nodes/formattedText/DashDocView.tsx @@ -5,13 +5,14 @@ import { Id } from "../../../../fields/FieldSymbols"; import { ObjectField } from "../../../../fields/ObjectField"; import { ComputedField } from "../../../../fields/ScriptField"; import { BoolCast, Cast, NumCast, StrCast } from "../../../../fields/Types"; -import { emptyFunction, returnEmptyString, returnFalse, Utils, returnZero, returnEmptyFilter } from "../../../../Utils"; +import { emptyFunction, returnEmptyString, returnFalse, Utils, returnZero, returnEmptyFilter, returnEmptyDoclist } from "../../../../Utils"; import { DocServer } from "../../../DocServer"; import { Docs, DocUtils } from "../../../documents/Documents"; import { DocumentView } from "../DocumentView"; import { FormattedTextBox } from "./FormattedTextBox"; import { Transform } from "../../../util/Transform"; import React = require("react"); +import { CurrentUserUtils } from "../../../util/CurrentUserUtils"; interface IDashDocView { node: any; @@ -131,14 +132,6 @@ export class DashDocView extends React.Component<IDashDocView> { if (!Doc.AreProtosEqual(finalLayout, dashDoc)) { finalLayout.rootDocument = dashDoc.aliasOf; } - const layoutKey = StrCast(finalLayout.layoutKey); - const finalKey = layoutKey && StrCast(finalLayout[layoutKey]).split("'")?.[1]; - if (finalLayout !== dashDoc && finalKey) { - const finalLayoutField = finalLayout[finalKey]; - if (finalLayoutField instanceof ObjectField) { - finalLayout[finalKey + "-textTemplate"] = ComputedField.MakeFunction(`copyField(this.${finalKey})`, { this: Doc.name }); - } - } this._finalLayout = finalLayout; this._resolvedDataDoc = Cast(finalLayout.resolvedDataDoc, Doc, null); return { finalLayout, resolvedDataDoc: Cast(finalLayout.resolvedDataDoc, Doc, null) }; @@ -175,7 +168,7 @@ export class DashDocView extends React.Component<IDashDocView> { const outerStyle = { position: "relative" as "relative", textIndent: "0", - border: "1px solid " + StrCast(this._textBox.Document.color, (Cast(Doc.UserDoc().activeWorkspace, Doc, null).darkScheme ? "dimGray" : "lightGray")), + border: "1px solid " + StrCast(this._textBox.Document.color, (CurrentUserUtils.ActiveDashboard.darkScheme ? "dimGray" : "lightGray")), width: this.props.node.props.width, height: this.props.node.props.height, display: this.props.node.props.hidden ? "none" : "inline-block", @@ -202,7 +195,7 @@ export class DashDocView extends React.Component<IDashDocView> { ({ dim, color }) => { spanStyle.width = outerStyle.width = Math.max(20, dim[0]) + "px"; spanStyle.height = outerStyle.height = Math.max(20, dim[1]) + "px"; - outerStyle.border = "1px solid " + StrCast(finalLayout.color, (Cast(Doc.UserDoc().activeWorkspace, Doc, null).darkScheme ? "dimGray" : "lightGray")); + outerStyle.border = "1px solid " + StrCast(finalLayout.color, (CurrentUserUtils.ActiveDashboard.darkScheme ? "dimGray" : "lightGray")); }, { fireImmediately: true }); if (node.attrs.width !== dashDoc._width + "px" || node.attrs.height !== dashDoc._height + "px") { @@ -244,8 +237,6 @@ export class DashDocView extends React.Component<IDashDocView> { addDocTab={this._textBox.props.addDocTab} pinToPres={returnFalse} renderDepth={self._textBox.props.renderDepth + 1} - NativeHeight={returnZero} - NativeWidth={returnZero} PanelWidth={finalLayout[WidthSym]} PanelHeight={finalLayout[HeightSym]} focus={this.outerFocus} @@ -255,6 +246,8 @@ export class DashDocView extends React.Component<IDashDocView> { bringToFront={emptyFunction} dontRegisterView={false} docFilters={this.props.tbox?.props.docFilters || returnEmptyFilter} + docRangeFilters={this.props.tbox?.props.docRangeFilters || returnEmptyFilter} + searchFilterDocs={this.props.tbox?.props.searchFilterDocs || returnEmptyDoclist} ContainingCollectionView={this._textBox.props.ContainingCollectionView} ContainingCollectionDoc={this._textBox.props.ContainingCollectionDoc} ContentScaling={this.contentScaling} diff --git a/src/client/views/nodes/formattedText/DashFieldView.scss b/src/client/views/nodes/formattedText/DashFieldView.scss index 23cf1e79b..e16036000 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.scss +++ b/src/client/views/nodes/formattedText/DashFieldView.scss @@ -18,7 +18,8 @@ .dashFieldView-labelSpan { position: relative; display: inline-block; - font-size: small; + font-weight: normal; + background: rgba(0,0,0,0.1); } .dashFieldView-fieldSpan { min-width: 20px; @@ -27,6 +28,7 @@ position: relative; display: inline; background-color: rgba(155, 155, 155, 0.24); + font-weight: bold; span { min-width: 100%; display: inline-block; diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx index 8ae71c035..b39a845db 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.tsx +++ b/src/client/views/nodes/formattedText/DashFieldView.tsx @@ -1,5 +1,5 @@ -import { IReactionDisposer, observable, runInAction, computed, action } from "mobx"; -import { Doc, DocListCast, Field } from "../../../../fields/Doc"; +import { IReactionDisposer, observable, computed, action } from "mobx"; +import { Doc, DocListCast, Field, DataSym } from "../../../../fields/Doc"; import { List } from "../../../../fields/List"; import { listSpec } from "../../../../fields/Schema"; import { SchemaHeaderField } from "../../../../fields/SchemaHeaderField"; @@ -70,7 +70,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna DocServer.GetRefField(this.props.docid). then(action(async dashDoc => dashDoc instanceof Doc && (this._dashDoc = dashDoc))); } else { - this._dashDoc = this.props.tbox.props.DataDoc || this.props.tbox.dataDoc; + this._dashDoc = this.props.tbox.rootDoc; } } componentWillUnmount() { @@ -82,7 +82,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna // set the display of the field's value (checkbox for booleans, span of text for strings) @computed get fieldValueContent() { if (this._dashDoc) { - const dashVal = this._dashDoc[this._fieldKey] || (this._fieldKey === "PARAMS" ? this._textBoxDoc[this._fieldKey] : ""); + const dashVal = this._dashDoc[DataSym][this._fieldKey] ?? this._dashDoc[this._fieldKey] ?? (this._fieldKey === "PARAMS" ? this._textBoxDoc[this._fieldKey] : ""); const fval = dashVal instanceof List ? dashVal.join(this.multiValueDelimeter) : StrCast(dashVal).startsWith(":=") || dashVal === "" ? Doc.Layout(this._textBoxDoc)[this._fieldKey] : dashVal; const boolVal = Cast(fval, "boolean", null); const strVal = Field.toString(fval as Field) || ""; @@ -92,7 +92,10 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna return <input className="dashFieldView-fieldCheck" type="checkbox" checked={boolVal} - onChange={e => this._dashDoc![this._fieldKey] = e.target.checked} + onChange={e => { + if (this._fieldKey.startsWith("_")) Doc.Layout(this._textBoxDoc)[this._fieldKey] = e.target.checked; + Doc.SetInPlace(this._dashDoc!, this._fieldKey, e.target.checked, true); + }} />; } else // field value is a string, so display it as an editable span @@ -106,7 +109,10 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna ref={r => { r?.addEventListener("keydown", e => this.fieldSpanKeyDown(e, r)); r?.addEventListener("blur", e => r && this.updateText(r.textContent!, false)); - r?.addEventListener("pointerdown", action((e) => this._showEnumerables = true)); + r?.addEventListener("pointerdown", action((e) => { + this._showEnumerables = true; + e.stopPropagation(); + })); }} > {strVal} </span>; @@ -149,16 +155,23 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna if (modText) { // elementfieldSpan.innerHTML = this._dashDoc![this._fieldKey as string] = modText; DocUtils.addFieldEnumerations(this._textBoxDoc, this._fieldKey, []); - this._dashDoc![this._fieldKey] = modText; + Doc.SetInPlace(this._dashDoc!, this._fieldKey, modText, true); } // if the text starts with a ':=' then treat it as an expression by making a computed field from its value storing it in the key else if (nodeText.startsWith(":=")) { - this._dashDoc![this._fieldKey] = ComputedField.MakeFunction(nodeText.substring(2)); + this._dashDoc![DataSym][this._fieldKey] = ComputedField.MakeFunction(nodeText.substring(2)); } else if (nodeText.startsWith("=:=")) { Doc.Layout(this._textBoxDoc)[this._fieldKey] = ComputedField.MakeFunction(nodeText.substring(3)); } else { - const splits = newText.split(this.multiValueDelimeter); - if (this._fieldKey !== "PARAMS" || !this._textBoxDoc[this._fieldKey] || this._dashDoc?.PARAMS) { - this._dashDoc![this._fieldKey] = splits.length > 1 ? new List<string>(splits) : newText; + if (Number(newText).toString() === newText) { + if (this._fieldKey.startsWith("_")) Doc.Layout(this._textBoxDoc)[this._fieldKey] = Number(newText); + Doc.SetInPlace(this._dashDoc!, this._fieldKey, newText, true); + } else { + const splits = newText.split(this.multiValueDelimeter); + if (this._fieldKey !== "PARAMS" || !this._textBoxDoc[this._fieldKey] || this._dashDoc?.PARAMS) { + const strVal = splits.length > 1 ? new List<string>(splits) : newText; + if (this._fieldKey.startsWith("_")) Doc.Layout(this._textBoxDoc)[this._fieldKey] = strVal; + Doc.SetInPlace(this._dashDoc!, this._fieldKey, strVal, true); + } } } }); @@ -169,7 +182,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna onPointerDownEnumerables = async (e: any) => { e.stopPropagation(); const collview = await DocUtils.addFieldEnumerations(this._textBoxDoc, this._fieldKey, [{ title: this._fieldKey }]); - collview instanceof Doc && this.props.tbox.props.addDocTab(collview, "onRight"); + collview instanceof Doc && this.props.tbox.props.addDocTab(collview, "add:right"); } @@ -183,7 +196,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna } if (container) { const alias = Doc.MakeAlias(container.props.Document); - alias.viewType = CollectionViewType.Time; + alias._viewType = CollectionViewType.Time; let list = Cast(alias._columnHeaders, listSpec(SchemaHeaderField)); if (!list) { alias._columnHeaders = list = new List<SchemaHeaderField>(); @@ -191,7 +204,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna list.map(c => c.heading).indexOf(this._fieldKey) === -1 && list.push(new SchemaHeaderField(this._fieldKey, "#f1efeb")); list.map(c => c.heading).indexOf("text") === -1 && list.push(new SchemaHeaderField("text", "#f1efeb")); alias._pivotField = this._fieldKey.startsWith("#") ? "#" : this._fieldKey; - this.props.tbox.props.addDocTab(alias, "onRight"); + this.props.tbox.props.addDocTab(alias, "add:right"); } } diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss index afdd8fea2..dbf98a5e9 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss @@ -32,8 +32,8 @@ .formattedTextBox-dictation { height: 12px; width: 10px; - top: 0px; - left: 0px; + bottom: 5px; + right: 8px; position: absolute; } } @@ -43,24 +43,21 @@ overflow: auto; display: inline-block; width: 100%; - height: 100%; + height: unset; } - + .formattedTextBox-sidebar-handle { position: absolute; - top: calc(50% - 17.5px); + top: 0; + //top: calc(50% - 17.5px); // use this to center vertically -- make sure it looks okay for slide views width: 10px; - height: 35px; + height: 100%; + max-height: 35px; background: lightgray; border-radius: 20px; cursor:grabbing; } -.formattedTextBox-cont>.formattedTextBox-sidebar-handle { - right: 0; - left: unset; -} - .formattedTextBox-sidebar, .formattedTextBox-sidebar-inking { border-left: dashed 1px black; @@ -73,11 +70,6 @@ .collectionfreeformview-container { position: relative; } - - >.formattedTextBox-sidebar-handle { - right: unset; - left: -5; - } } .formattedTextBox-sidebar-inking { @@ -94,7 +86,9 @@ } .formattedTextBox-inner-rounded, .formattedTextBox-inner-rounded-selected, -.formattedTextBox-inner, .formattedTextBox-inner-selected { +.formattedTextBox-inner, +.formattedTextBox-inner-minimal, +.formattedTextBox-inner-selected { height: 100%; white-space: pre-wrap; .ProseMirror:hover { @@ -112,6 +106,15 @@ border-width: 1px; } } +.formattedTextBox-inner-rounded-selected, +.formattedTextBox-inner-selected { + .ProseMirror { + padding:10px; + } + .ProseMirror:hover { + background: unset; + } +} // .menuicon { // display: inline-block; @@ -338,15 +341,6 @@ footnote::after { .multi4:before { transition: 0.5s;counter-increment: multi4; display: inline-block; vertical-align: top; margin-left: -4.2em; width: 4.2em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) "."counter(multi3, lower-alpha) "."counter(multi4, lower-roman) ". "; } } -.formattedTextBox-inner-rounded-selected, -.formattedTextBox-inner-selected { - .ProseMirror { - padding:10px; - } - .ProseMirror:hover { - background: unset; - } -} @media only screen and (max-width: 1000px) { @import "../../globalCssVariables"; @@ -396,10 +390,11 @@ footnote::after { width: 100%; height: 100%; } - + .formattedTextBox-sidebar-handle { position: absolute; - top: calc(50% - 17.5px); + top: 0; + //top: calc(50% - 17.5px); // use this to center vertically -- make sure it looks okay for slide views width: 10px; height: 35px; background: lightgray; @@ -407,11 +402,6 @@ footnote::after { cursor:grabbing; } - .formattedTextBox-cont>.formattedTextBox-sidebar-handle { - right: 0; - left: unset; - } - .formattedTextBox-sidebar, .formattedTextBox-sidebar-inking { border-left: dashed 1px black; @@ -423,11 +413,6 @@ footnote::after { .collectionfreeformview-container { position: relative; } - - >.formattedTextBox-sidebar-handle { - right: unset; - left: -5; - } } .formattedTextBox-sidebar-inking { @@ -443,6 +428,7 @@ footnote::after { left: 10%; } + .formattedTextBox-inner-minimal, .formattedTextBox-inner-rounded, .formattedTextBox-inner { height: 100%; diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index d4c9f74d5..a998386d8 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -1,7 +1,5 @@ -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faEdit, faSmile, faTextHeight, faUpload } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { isEqual } from "lodash"; +import { isEqual, max } from "lodash"; import { action, computed, IReactionDisposer, Lambda, observable, reaction, runInAction, trace } from "mobx"; import { observer } from "mobx-react"; import { baseKeymap, selectAll } from "prosemirror-commands"; @@ -17,15 +15,15 @@ import { DataSym, Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, Wid import { documentSchema } from '../../../../fields/documentSchemas'; import applyDevTools = require("prosemirror-dev-tools"); import { removeMarkWithAttrs } from "./prosemirrorPatches"; -import { Id } from '../../../../fields/FieldSymbols'; +import { Id, Copy } from '../../../../fields/FieldSymbols'; import { InkTool } from '../../../../fields/InkField'; import { PrefetchProxy } from '../../../../fields/Proxy'; import { RichTextField } from "../../../../fields/RichTextField"; import { RichTextUtils } from '../../../../fields/RichTextUtils'; -import { createSchema, makeInterface } from "../../../../fields/Schema"; +import { makeInterface } from "../../../../fields/Schema"; import { Cast, DateCast, NumCast, StrCast, ScriptCast, BoolCast } from "../../../../fields/Types"; -import { TraceMobx, OVERRIDE_ACL, GetEffectiveAcl } from '../../../../fields/util'; -import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnOne, returnZero, Utils, setupMoveUpEvents } from '../../../../Utils'; +import { TraceMobx, OVERRIDE_acl, GetEffectiveAcl } from '../../../../fields/util'; +import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnOne, returnZero, Utils, setupMoveUpEvents, OmitKeys } from '../../../../Utils'; import { GoogleApiClientUtils, Pulls, Pushes } from '../../../apis/google_docs/GoogleApiClientUtils'; import { DocServer } from "../../../DocServer"; import { Docs, DocUtils } from '../../../documents/Documents'; @@ -33,8 +31,8 @@ import { DocumentType } from '../../../documents/DocumentTypes'; import { DictationManager } from '../../../util/DictationManager'; import { DragManager } from "../../../util/DragManager"; import { makeTemplate } from '../../../util/DropConverter'; -import buildKeymap, { updateBullets } from "./ProsemirrorExampleTransfer"; -import RichTextMenu, { RichTextMenuPlugin } from './RichTextMenu'; +import { buildKeymap, updateBullets } from "./ProsemirrorExampleTransfer"; +import { RichTextMenu, RichTextMenuPlugin } from './RichTextMenu'; import { RichTextRules } from "./RichTextRules"; //import { DashDocView } from "./DashDocView"; @@ -60,15 +58,16 @@ import "./FormattedTextBox.scss"; import { FormattedTextBoxComment, formattedTextBoxCommentPlugin, findLinkMark } from './FormattedTextBoxComment'; import React = require("react"); import { DocumentManager } from '../../../util/DocumentManager'; - -library.add(faEdit); -library.add(faSmile, faTextHeight, faUpload); +import { CollectionStackingView } from '../../collections/CollectionStackingView'; +import { CollectionViewType } from '../../collections/CollectionView'; +import { SnappingManager } from '../../../util/SnappingManager'; export interface FormattedTextBoxProps { makeLink?: () => Opt<Doc>; // bcz: hack: notifies the text document when the container has made a link. allows the text doc to react and setup a hyeprlink for any selected text hideOnLeave?: boolean; // used by DocumentView for setting caption's hide on leave (bcz: would prefer to have caption-hideOnLeave field set or something similar) xMargin?: number; // used to override document's settings for xMargin --- see CollectionCarouselView yMargin?: number; + dontSelectOnLoad?: boolean; // suppress selecting the text box when loaded } export const GoogleRef = "googleDocId"; @@ -84,6 +83,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp public static Instance: FormattedTextBox; public ProseRef?: HTMLDivElement; public get EditorView() { return this._editorView; } + private _boxRef: React.RefObject<HTMLDivElement> = React.createRef(); private _ref: React.RefObject<HTMLDivElement> = React.createRef(); private _scrollRef: React.RefObject<HTMLDivElement> = React.createRef(); private _editorView: Opt<EditorView>; @@ -99,8 +99,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp private _linkTime: number | null = null; private _pause: boolean = false; - @computed get _recording() { return this.dataDoc.audioState === "recording"; } - set _recording(value) { this.dataDoc.audioState = value ? "recording" : undefined; } + @computed get _recording() { return this.dataDoc?.audioState === "recording"; } + set _recording(value) { + this.dataDoc.audioState = value ? "recording" : undefined; + } @observable private _entered = false; @@ -225,7 +227,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const id = Utils.GenerateDeterministicGuid(this.dataDoc[Id] + key); const allLinks = [{ href: Utils.prepend("/doc/" + id), title: value, targetId: id }]; - const link = this._editorView.state.schema.marks.linkAnchor.create({ allLinks, location: "onRight", title: value }); + const link = this._editorView.state.schema.marks.linkAnchor.create({ allLinks, location: "add:right", title: value }); const mval = this._editorView.state.schema.marks.metadataVal.create(); const offset = (tx.selection.to === range!.end - 1 ? -1 : 0); tx = tx.addMark(textEndSelection - value.length + offset, textEndSelection, link).addMark(textEndSelection - value.length + offset, textEndSelection, mval); @@ -239,20 +241,23 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const tsel = this._editorView.state.selection.$from; tsel.marks().filter(m => m.type === this._editorView!.state.schema.marks.user_mark).map(m => AudioBox.SetScrubTime(Math.max(0, m.attrs.modified * 1000))); const curText = state.doc.textBetween(0, state.doc.content.size, " \n"); - const curTemp = Cast(this.layoutDoc[this.props.fieldKey + "-textTemplate"], RichTextField); // the actual text in the text box + const curTemp = this.layoutDoc.resolvedDataDoc ? Cast(this.layoutDoc[this.props.fieldKey], RichTextField) : undefined; // the actual text in the text box const curProto = Cast(Cast(this.dataDoc.proto, Doc, null)?.[this.fieldKey], RichTextField, null); // the default text inherited from a prototype const curLayout = this.rootDoc !== this.layoutDoc ? Cast(this.layoutDoc[this.fieldKey], RichTextField, null) : undefined; // the default text stored in a layout template const json = JSON.stringify(state.toJSON()); let unchanged = true; const effectiveAcl = GetEffectiveAcl(this.dataDoc); + const removeSelection = (json: string | undefined) => { + return json?.indexOf("\"storedMarks\"") === -1 ? json?.replace(/"selection":.*/, "") : json?.replace(/"selection":"\"storedMarks\""/, "\"storedMarks\""); + }; if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) { - if (!this._applyingChange && json.replace(/"selection":.*/, "") !== curProto?.Data.replace(/"selection":.*/, "")) { + if (!this._applyingChange && removeSelection(json) !== removeSelection(curProto?.Data)) { this._applyingChange = true; (curText !== Cast(this.dataDoc[this.fieldKey], RichTextField)?.Text) && (this.dataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now()))); - if ((!curTemp && !curProto) || curText || curLayout?.Data.includes("dash")) { // if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended) - if (json.replace(/"selection":.*/, "") !== curLayout?.Data.replace(/"selection":.*/, "")) { + if ((!curTemp && !curProto) || curText || json.includes("dash")) { // if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended) + if (removeSelection(json) !== removeSelection(curLayout?.Data)) { if (!this._pause && !this.layoutDoc._timeStampOnEnter) { timeStamp = setTimeout(() => this.pause(), 10 * 1000); // 10 seconds delay for time stamp } @@ -265,7 +270,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp !curText && tx.storedMarks?.map(m => m.type.name === "pFontSize" && (Doc.UserDoc().fontSize = this.layoutDoc._fontSize = m.attrs.fontSize)); !curText && tx.storedMarks?.map(m => m.type.name === "pFontFamily" && (Doc.UserDoc().fontFamily = this.layoutDoc._fontFamily = m.attrs.fontFamily)); this.dataDoc[this.props.fieldKey] = new RichTextField(json, curText); - this.dataDoc[this.props.fieldKey + "-noTemplate"] = (curTemp?.Text || "") !== curText; // mark the data field as being split from the template if it has been edited + this.dataDoc[this.props.fieldKey + "-noTemplate"] = true;//(curTemp?.Text || "") !== curText; // mark the data field as being split from the template if it has been edited ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: curText }); unchanged = false; @@ -349,8 +354,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } updateTitle = () => { - if ((this.props.Document.isTemplateForField === "text" || !this.props.Document.isTemplateForField) && // only update the title if the data document's data field is changing - StrCast(this.dataDoc.title).startsWith("-") && this._editorView && !this.rootDoc.customTitle) { + if (!this.props.dontRegisterView && // (this.props.Document.isTemplateForField === "text" || !this.props.Document.isTemplateForField) && // only update the title if the data document's data field is changing + StrCast(this.dataDoc.title).startsWith("-") && this._editorView && !this.dataDoc["title-custom"] && + (Doc.LayoutFieldKey(this.rootDoc) === this.fieldKey || this.fieldKey === "text")) { let node = this._editorView.state.doc; while (node.firstChild && node.firstChild.type.name !== "text") node = node.firstChild; const str = node.textContent; @@ -374,10 +380,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this._editorView.dispatch(tr.addMark(flattened[lastSel].from, flattened[lastSel].to, link)); } } - public highlightSearchTerms = (terms: string[], alt: boolean) => { + public highlightSearchTerms = (terms: string[], backward: boolean) => { if (this._editorView && (this._editorView as any).docView && terms.some(t => t)) { - - const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight); const activeMark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight, { selected: true }); const res = terms.filter(t => t).map(term => this.findInNode(this._editorView!, this._editorView!.state.doc, term)); @@ -385,24 +389,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp let tr = this._editorView.state.tr; const flattened: TextSelection[] = []; res.map(r => r.map(h => flattened.push(h))); - if (BoolCast(Doc.GetProto(this.dataDoc).resetSearch) === true) { - this._searchIndex = 0; - Doc.GetProto(this.dataDoc).resetSearch = undefined; - } - else { - this._searchIndex = ++this._searchIndex > flattened.length - 1 ? 0 : this._searchIndex; - if (alt === true) { - if (this._searchIndex > 1) { - this._searchIndex += -2; - } - else if (this._searchIndex === 1) { - this._searchIndex = length - 1; - } - else if (this._searchIndex === 0 && length !== 1) { - this._searchIndex = length - 2; - } - + this._searchIndex = ++this._searchIndex > flattened.length - 1 ? 0 : this._searchIndex; + if (backward === true) { + if (this._searchIndex > 1) { + this._searchIndex += -2; + } + else if (this._searchIndex === 1) { + this._searchIndex = length - 1; + } + else if (this._searchIndex === 0 && length !== 1) { + this._searchIndex = length - 2; } + } const lastSel = Math.min(flattened.length - 1, this._searchIndex); @@ -470,12 +468,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } else if (de.complete.linkDragData) { de.complete.linkDragData.linkDropCallback = this.linkDrop; } + else if (de.complete.annoDragData) { + de.complete.annoDragData.linkDropCallback = this.linkDrop; + } } - linkDrop = (data: DragManager.LinkDragData) => { + linkDrop = (data: { linkDocument?: Doc }) => { const linkDoc = data.linkDocument!; const anchor1Title = linkDoc.anchor1 instanceof Doc ? StrCast(linkDoc.anchor1.title) : "-untitled-"; const anchor1Id = linkDoc.anchor1 instanceof Doc ? linkDoc.anchor1[Id] : ""; - this.makeLinkToSelection(linkDoc[Id], anchor1Title, "onRight", anchor1Id); + this.makeLinkToSelection(linkDoc[Id], anchor1Title, "add:right", anchor1Id); } getNodeEndpoints(context: Node, node: Node): { from: number, to: number } | null { @@ -509,10 +510,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp if (node.isTextblock) { let index = 0, foundAt; const ep = this.getNodeEndpoints(pm.state.doc, node); - while (ep && (foundAt = node.textContent.slice(index).search(RegExp(find, "i"))) > -1) { - const sel = new TextSelection(pm.state.doc.resolve(ep.from + index + foundAt + 1), pm.state.doc.resolve(ep.from + index + foundAt + find.length + 1)); - ret.push(sel); - index = index + foundAt + find.length; + const regexp = new RegExp(find.replace("*", ""), "i"); + if (regexp) { + while (ep && (foundAt = node.textContent.slice(index).search(regexp)) > -1) { + const sel = new TextSelection(pm.state.doc.resolve(ep.from + index + foundAt + 1), pm.state.doc.resolve(ep.from + index + foundAt + find.length + 1)); + ret.push(sel); + index = index + foundAt + find.length; + } } } else { node.content.forEach((child, i) => ret = ret.concat(this.findInNode(pm, child, find))); @@ -556,17 +560,22 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp sidebarDown = (e: React.PointerEvent) => { setupMoveUpEvents(this, e, this.sidebarMove, emptyFunction, - () => (this.layoutDoc._sidebarWidthPercent = StrCast(this.layoutDoc._sidebarWidthPercent, "0%") === "0%" ? "25%" : "0%")); + () => setTimeout(action(() => { + const prevWidth = this.sidebarWidth(); + this.layoutDoc._showSidebar = ((this.layoutDoc._sidebarWidthPercent = StrCast(this.layoutDoc._sidebarWidthPercent, "0%") === "0%" ? "50%" : "0%")) !== "0%"; + this.layoutDoc._width = this.layoutDoc._showSidebar ? NumCast(this.layoutDoc._width) * 2 : Math.max(20, NumCast(this.layoutDoc._width) - prevWidth); + })), false); } sidebarMove = (e: PointerEvent, down: number[], delta: number[]) => { const bounds = this.CurrentDiv.getBoundingClientRect(); - this.layoutDoc._sidebarWidthPercent = "" + 100 * (1 - (e.clientX - bounds.left) / bounds.width) + "%"; + this.layoutDoc._sidebarWidthPercent = "" + 100 * Math.max(0, (1 - (e.clientX - bounds.left) / bounds.width)) + "%"; + this.layoutDoc._showSidebar = this.layoutDoc._sidebarWidthPercent !== "0%"; return false; } @undoBatch @action toggleNativeDimensions = () => { - Doc.toggleNativeDimensions(this.layoutDoc, this.props.ContentScaling(), this.props.NativeWidth(), this.props.NativeHeight()); + Doc.toggleNativeDimensions(this.layoutDoc, this.props.ContentScaling(), this.props.NativeWidth?.() || 0, this.props.NativeHeight?.() || 0); } public static get DefaultLayout(): Doc | string | undefined { @@ -576,6 +585,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const cm = ContextMenu.Instance; const changeItems: ContextMenuProps[] = []; + changeItems.push({ description: "plain", event: undoBatch(() => Doc.setNativeView(this.rootDoc)), icon: "eye" }); const noteTypesDoc = Cast(Doc.UserDoc()["template-notes"], Doc, null); DocListCast(noteTypesDoc?.data).forEach(note => { changeItems.push({ @@ -585,9 +595,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp }), icon: "eye" }); }); - changeItems.push({ description: "FreeForm", event: () => DocUtils.makeCustomViewClicked(this.rootDoc, Docs.Create.FreeformDocument, "freeform"), icon: "eye" }); + !Doc.UserDoc().noviceMode && changeItems.push({ description: "FreeForm", event: () => DocUtils.makeCustomViewClicked(this.rootDoc, Docs.Create.FreeformDocument, "freeform"), icon: "eye" }); const highlighting: ContextMenuProps[] = []; - ["My Text", "Text from Others", "Todo Items", "Important Items", "Ignore Items", "Disagree Items", "By Recent Minute", "By Recent Hour"].forEach(option => + const noviceHighlighting = ["My Text", "Text from Others"]; + const expertHighlighting = ["My Text", "Text from Others", "Todo Items", "Important Items", "Ignore Items", "Disagree Items", "By Recent Minute", "By Recent Hour"]; + (Doc.UserDoc().noviceMode ? noviceHighlighting : expertHighlighting).forEach(option => highlighting.push({ description: (FormattedTextBox._highlights.indexOf(option) === -1 ? "Highlight " : "Unhighlight ") + option, event: () => { e.stopPropagation(); @@ -602,10 +614,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const uicontrols: ContextMenuProps[] = []; - uicontrols.push({ description: `${this.layoutDoc._showSidebar ? "Hide" : "Show"} Sidebar`, event: () => this.layoutDoc._showSidebar = !this.layoutDoc._showSidebar, icon: "expand-arrows-alt" }); uicontrols.push({ description: `${this.layoutDoc._showAudio ? "Hide" : "Show"} Dictation Icon`, event: () => this.layoutDoc._showAudio = !this.layoutDoc._showAudio, icon: "expand-arrows-alt" }); uicontrols.push({ description: "Show Highlights...", noexpand: true, subitems: highlighting, icon: "hand-point-right" }); - uicontrols.push({ description: `Create TimeStamp When ${this.layoutDoc._timeStampOnEnter ? "Pause" : "Enter"}`, event: () => this.layoutDoc._timeStampOnEnter = !this.layoutDoc._timeStampOnEnter, icon: "expand-arrows-alt" }); + !Doc.UserDoc().noviceMode && uicontrols.push({ description: `Create TimeStamp When ${this.layoutDoc._timeStampOnEnter ? "Pause" : "Enter"}`, event: () => this.layoutDoc._timeStampOnEnter = !this.layoutDoc._timeStampOnEnter, icon: "expand-arrows-alt" }); !Doc.UserDoc().noviceMode && uicontrols.push({ description: "Broadcast Message", event: () => DocServer.GetRefField("rtfProto").then(proto => proto instanceof Doc && (proto.BROADCAST_MESSAGE = Cast(this.rootDoc[this.fieldKey], RichTextField)?.Text)), icon: "expand-arrows-alt" @@ -615,15 +626,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const appearance = cm.findByDescription("Appearance..."); const appearanceItems = appearance && "subitems" in appearance ? appearance.subitems : []; appearanceItems.push({ description: "Change Perspective...", noexpand: true, subitems: changeItems, icon: "external-link-alt" }); - this.rootDoc.isTemplateDoc && appearanceItems.push({ description: "Make Default Layout", event: async () => Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.rootDoc), icon: "eye" }); - Doc.UserDoc().defaultTextLayout && appearanceItems.push({ description: "Reset default note style", event: () => Doc.UserDoc().defaultTextLayout = undefined, icon: "eye" }); - appearanceItems.push({ - description: "Convert to be a template style", event: () => { + // this.rootDoc.isTemplateDoc && appearanceItems.push({ description: "Make Default Layout", event: async () => Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.rootDoc), icon: "eye" }); + !Doc.UserDoc().noviceMode && appearanceItems.push({ + description: "Make Default Layout", event: () => { if (!this.layoutDoc.isTemplateDoc) { const title = StrCast(this.rootDoc.title); this.rootDoc.title = "text"; this.rootDoc.isTemplateDoc = makeTemplate(this.rootDoc, true, title); - } else { + } else if (!this.rootDoc.isTemplateDoc) { const title = StrCast(this.rootDoc.title); this.rootDoc.title = "text"; this.rootDoc.layout = (this.layoutDoc as Doc).layout as string; @@ -637,12 +647,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this.rootDoc._width = this.layoutDoc._width || 300; // are stored on the template, since we're getting rid of the old template this.rootDoc._height = this.layoutDoc._height || 200; // we need to copy them over to the root. This should probably apply to all '_' fields this.rootDoc._backgroundColor = Cast(this.layoutDoc._backgroundColor, "string", null); + this.rootDoc.backgroundColor = Cast(this.layoutDoc.backgroundColor, "string", null); }, 10); } + Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.rootDoc); Doc.AddDocToList(Cast(Doc.UserDoc()["template-notes"], Doc, null), "data", this.rootDoc); }, icon: "eye" }); - appearanceItems.push({ description: "Create progressivized slide...", event: this.progressivizeText, icon: "desktop" }); + !Doc.UserDoc().noviceMode && appearanceItems.push({ description: "Create progressivized slide...", event: this.progressivizeText, icon: "desktop" }); cm.addItem({ description: "Appearance...", subitems: appearanceItems, icon: "eye" }); const options = cm.findByDescription("Options..."); @@ -662,7 +674,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const newBullets: Doc[] = this.recursiveProgressivize(1, list)[0]; mainBulletList.push.apply(mainBulletList, newBullets); } - console.log(mainBulletList.length); const title = Docs.Create.TextDocument(StrCast(this.rootDoc.title), { title: "Title", _width: 800, _height: 70, x: 20, y: -10, _fontSize: '20pt', backgroundColor: "rgba(0,0,0,0)", appearFrame: 0, _fontWeight: 700 }); mainBulletList.push(title); const doc = Docs.Create.FreeformDocument(mainBulletList, { @@ -717,7 +728,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp recordDictation = () => { DictationManager.Controls.listen({ - interimHandler: this.setCurrentBulletContent, + interimHandler: this.setDictationContent, continuous: { indefinite: false }, }).then(results => { if (results && [DictationManager.Controls.Infringed].includes(results)) { @@ -728,22 +739,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } stopDictation = (abort: boolean) => { DictationManager.Controls.stop(!abort); }; - recordBullet = async () => { - const completedCue = "end session"; - const results = await DictationManager.Controls.listen({ - interimHandler: this.setCurrentBulletContent, - continuous: { indefinite: false }, - terminators: [completedCue, "bullet", "next"] - }); - if (results && [DictationManager.Controls.Infringed, completedCue].includes(results)) { - DictationManager.Controls.stop(); - return; - } - this.nextBullet(this._editorView!.state.selection.to); - setTimeout(this.recordBullet, 2000); - } - - setCurrentBulletContent = (value: string) => { + setDictationContent = (value: string) => { if (this._editorView) { const state = this._editorView.state; const now = Date.now(); @@ -758,33 +754,17 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } } } - const recordingStart = DateCast(this.props.Document.recordingStart).date.getTime(); - this._break = false; - value = "" + (mark.attrs.modified * 1000 - recordingStart) / 1000 + value; const from = state.selection.from; - const inserted = state.tr.insertText(value).addMark(from, from + value.length + 1, mark); - this._editorView.dispatch(inserted.setSelection(TextSelection.create(inserted.doc, from, from + value.length + 1))); - } - } - - nextBullet = (pos: number) => { - if (this._editorView) { - const frag = Fragment.fromArray(this.newListItems(2)); - if (this._editorView.state.doc.resolve(pos).depth >= 2) { - const slice = new Slice(frag, 2, 2); - let state = this._editorView.state; - this._editorView.dispatch(state.tr.step(new ReplaceStep(pos, pos, slice))); - pos += 4; - state = this._editorView.state; - this._editorView.dispatch(state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos, pos))); + this._break = false; + if (this.props.Document.recordingStart) { + const recordingStart = DateCast(this.props.Document.recordingStart)?.date.getTime(); + value = "" + (mark.attrs.modified * 1000 - recordingStart) / 1000 + value; } + const tr = state.tr.insertText(value).addMark(from, from + value.length + 1, mark); + this._editorView.dispatch(tr.setSelection(TextSelection.create(tr.doc, from, from + value.length + 1))); } } - private newListItems = (count: number) => { - return numberRange(count).map(x => schema.nodes.list_item.create(undefined, schema.nodes.paragraph.create())); - } - _keymap: any = undefined; _rules: RichTextRules | undefined; @computed get config() { @@ -823,9 +803,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp tr = tr.addMark(pos, pos + node.nodeSize, link); } }); - OVERRIDE_ACL(true); + OVERRIDE_acl(true); this._editorView!.dispatch(tr.removeMark(sel.from, sel.to, splitter)); - OVERRIDE_ACL(false); + OVERRIDE_acl(false); } } componentDidMount() { @@ -848,19 +828,23 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp () => this.props.makeLink?.(), (linkDoc: Opt<Doc>) => { if (linkDoc) { - const anchor2Title = linkDoc.anchor2 instanceof Doc ? StrCast(linkDoc.anchor2.title) : "-untitled-"; - const anchor2Id = linkDoc.anchor2 instanceof Doc ? linkDoc.anchor2[Id] : ""; - this.makeLinkToSelection(linkDoc[Id], anchor2Title, "onRight", anchor2Id); + const a1 = Cast(linkDoc.anchor1, Doc, null); + const a2 = Cast(linkDoc.anchor2, Doc, null); + const otherAnchor = Doc.AreProtosEqual(a1, this.rootDoc) ? a2 : a1; + const anchor2Title = StrCast(otherAnchor.title, "-untitled-"); + const anchor2Id = otherAnchor?.[Id] || ""; + this.makeLinkToSelection(linkDoc[Id], anchor2Title, "add:right", anchor2Id); } }, { fireImmediately: true } ); this._disposers.editorState = reaction( () => { - if (this.dataDoc?.[this.props.fieldKey + "-noTemplate"] || !this.layoutDoc[this.props.fieldKey + "-textTemplate"]) { + if (!this.dataDoc || !this.layoutDoc) return undefined; + if (this.dataDoc?.[this.props.fieldKey + "-noTemplate"] || !this.layoutDoc[this.props.fieldKey]) { return Cast(this.dataDoc[this.props.fieldKey], RichTextField, null)?.Data; } - return Cast(this.layoutDoc[this.props.fieldKey + "-textTemplate"], RichTextField, null)?.Data; + return Cast(this.layoutDoc[this.props.fieldKey], RichTextField, null)?.Data; }, incomingValue => { if (incomingValue !== undefined && this._editorView && !this._applyingChange) { @@ -870,7 +854,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this.tryUpdateHeight(); } } - } + }, ); this._disposers.pullDoc = reaction( () => this.props.Document[Pulls], @@ -892,13 +876,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } ); this._disposers.autoHeight = reaction( - () => [this.layoutDoc[WidthSym](), this.layoutDoc._autoHeight], - () => setTimeout(() => this.tryUpdateHeight(), 0) + () => ({ + width: NumCast(this.layoutDoc._width), + autoHeight: this.layoutDoc?._autoHeight + }), + ({ width, autoHeight }) => width !== undefined && setTimeout(() => this.tryUpdateHeight(), 0) ); this._disposers.height = reaction( - () => this.layoutDoc[HeightSym](), + () => NumCast(this.layoutDoc._height), action(height => { - if (height <= 20 && height < NumCast(this.layoutDoc._delayAutoHeight, 20)) { + if (height !== undefined && height <= 20 && height < NumCast(this.layoutDoc._delayAutoHeight, 20)) { this.layoutDoc._delayAutoHeight = height; } }) @@ -906,23 +893,24 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this.setupEditor(this.config, this.props.fieldKey); - this._disposers.searchAlt = reaction(() => this.rootDoc.searchMatchAlt, - search => search ? this.highlightSearchTerms([Doc.SearchQuery()], false) : this.unhighlightSearchTerms(), - { fireImmediately: true }); - this._disposers.search = reaction(() => this.rootDoc.searchMatch, - search => search ? this.highlightSearchTerms([Doc.SearchQuery()], true) : this.unhighlightSearchTerms(), - { fireImmediately: this.rootDoc.searchMatch ? true : false }); - - this._disposers.record = reaction(() => this._recording, - () => { - if (this._recording) { - setTimeout(action(() => { - this.stopDictation(true); - setTimeout(() => this.recordDictation(), 500); - }), 500); - } else setTimeout(() => this.stopDictation(true), 0); - } - ); + this._disposers.search = reaction(() => Doc.IsSearchMatch(this.rootDoc), + search => search ? this.highlightSearchTerms([Doc.SearchQuery()], search.searchMatch < 0) : this.unhighlightSearchTerms(), + { fireImmediately: Doc.IsSearchMatchUnmemoized(this.rootDoc) ? true : false }); + + this._disposers.selected = reaction(() => this.props.isSelected(), action(() => this._recording = false)); + + if (!this.props.dontRegisterView) { + this._disposers.record = reaction(() => this._recording, + () => { + if (this._recording) { + setTimeout(action(() => { + this.stopDictation(true); + setTimeout(() => this.recordDictation(), 500); + }), 500); + } else setTimeout(() => this.stopDictation(true), 0); + } + ); + } this._disposers.scrollToRegion = reaction( () => StrCast(this.layoutDoc.scrollToLinkID), async (scrollToLinkID) => { @@ -932,10 +920,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const examinedNode = findLinkNode(node, editor); if (examinedNode?.textContent) { nodes.push(examinedNode); - start += index; + !start && (start = index); } }); - return { frag: Fragment.fromArray(nodes), start: start }; + return { frag: Fragment.fromArray(nodes), start }; }; const findLinkNode = (node: Node, editor: EditorView) => { if (!node.isText) { @@ -972,7 +960,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp pos => this._scrollRef.current && this._scrollRef.current.scrollTo({ top: pos }), { fireImmediately: true } ); - setTimeout(() => this.tryUpdateHeight(NumCast(this.layoutDoc.limitHeight, 0))); + setTimeout(() => this.tryUpdateHeight(NumCast(this.layoutDoc.limitHeight))); } pushToGoogleDoc = async () => { @@ -1018,7 +1006,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp if (documentId) { exportState = await RichTextUtils.GoogleDocs.Import(documentId, dataDoc); } - UndoManager.RunInBatch(() => handler(exportState, dataDoc), Pulls); + exportState && UndoManager.RunInBatch(() => handler(exportState, dataDoc), Pulls); } updateState = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => { @@ -1034,7 +1022,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } }, 0); dataDoc.title = exportState.title; - this.rootDoc.customTitle = true; + this.dataDoc["title-custom"] = true; dataDoc.unchanged = true; } else { delete dataDoc[GoogleRef]; @@ -1129,7 +1117,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const marks = [...node.marks]; const linkIndex = marks.findIndex(mark => mark.type.name === "link"); const allLinks = [{ href: Utils.prepend(`/doc/${linkId}`), title, linkId }]; - const link = view.state.schema.mark(view.state.schema.marks.linkAnchor, { allLinks, location: "onRight", title, docref: true }); + const link = view.state.schema.mark(view.state.schema.marks.linkAnchor, { allLinks, location: "add:right", title, docref: true }); marks.splice(linkIndex === -1 ? 0 : linkIndex, 1, link); return node.mark(marks); } @@ -1137,8 +1125,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp private setupEditor(config: any, fieldKey: string) { const curText = Cast(this.dataDoc[this.props.fieldKey], RichTextField, null); - const useTemplate = !curText?.Text && this.layoutDoc[this.props.fieldKey + "-textTemplate"]; - const rtfField = Cast((useTemplate && this.layoutDoc[this.props.fieldKey + "-textTemplate"]) || this.dataDoc[fieldKey], RichTextField); + const rtfField = Cast((!curText?.Text && this.layoutDoc[this.props.fieldKey]) || this.dataDoc[fieldKey], RichTextField); if (this.ProseRef) { const self = this; this._editorView?.destroy(); @@ -1174,11 +1161,21 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } const selectOnLoad = this.rootDoc[Id] === FormattedTextBox.SelectOnLoad; - if (selectOnLoad && !this.props.dontRegisterView) { + if (selectOnLoad && !this.props.dontRegisterView && !this.props.dontSelectOnLoad) { FormattedTextBox.SelectOnLoad = ""; this.props.select(false); - FormattedTextBox.SelectOnLoadChar && this._editorView!.dispatch(this._editorView!.state.tr.insertText(FormattedTextBox.SelectOnLoadChar)); - FormattedTextBox.SelectOnLoadChar = ""; + if (FormattedTextBox.SelectOnLoadChar && this._editorView) { + const $from = this._editorView.state.selection.anchor ? this._editorView.state.doc.resolve(this._editorView.state.selection.anchor - 1) : undefined; + const mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) }); + const curMarks = this._editorView.state.storedMarks ?? $from?.marksAcross(this._editorView.state.selection.$head) ?? []; + const storedMarks = [...curMarks.filter(m => m.type !== mark.type), mark]; + const tr = this._editorView.state.tr.setStoredMarks(storedMarks).insertText(FormattedTextBox.SelectOnLoadChar, this._editorView.state.doc.content.size - 1, this._editorView.state.doc.content.size).setStoredMarks(storedMarks); + this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size)))); + FormattedTextBox.SelectOnLoadChar = ""; + } else if (curText?.Text) { + selectAll(this._editorView!.state, this._editorView?.dispatch); + this.startUndoTypingBatch(); + } } selectOnLoad && this._editorView!.focus(); @@ -1201,8 +1198,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } componentWillUnmount() { + this.endUndoTypingBatch(); Object.values(this._disposers).forEach(disposer => disposer?.()); this._editorView?.destroy(); + FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = "none"); } _downEvent: any; @@ -1230,7 +1229,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp if (this.props.onClick && e.button === 0 && !this.props.isSelected(false)) { e.preventDefault(); } - if (e.button === 0 && this.props.isSelected(true) && !e.altKey && !e.ctrlKey && !e.metaKey) { + if (e.button === 0 && (this.props.rootSelected(true) || this.props.isSelected(true)) && !e.altKey && !e.ctrlKey && !e.metaKey) { if (e.clientX < this.ProseRef!.getBoundingClientRect().right) { // stop propagation if not in sidebar e.stopPropagation(); // if the text box is selected, then it consumes all down events } @@ -1248,7 +1247,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp FormattedTextBoxComment.textBox = this; const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY }); !this.props.isSelected(true) && editor.dispatch(editor.state.tr.setSelection(new TextSelection(editor.state.doc.resolve(pcords?.pos || 0)))); - FormattedTextBoxComment.update(editor, undefined, (e.target as any)?.className === "prosemirror-dropdownlink" ? (e.target as any).href : ""); + const target = (e.target as any).parentElement; // hrefs are store don the database of the <a> node that wraps the hyerlink <span> + FormattedTextBoxComment.update(editor, undefined, target?.dataset?.targethrefs); } (e.nativeEvent as any).formattedHandled = true; @@ -1276,10 +1276,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp FormattedTextBoxComment.Hide(); if (FormattedTextBoxComment.linkDoc) { if (FormattedTextBoxComment.linkDoc.type !== DocumentType.LINK) { - this.props.addDocTab(FormattedTextBoxComment.linkDoc, e.ctrlKey ? "inTab" : "onRight"); + this.props.addDocTab(FormattedTextBoxComment.linkDoc, e.ctrlKey ? "add" : "add:right"); } else { DocumentManager.Instance.FollowLink(FormattedTextBoxComment.linkDoc, this.props.Document, - (doc: Doc, followLinkLocation: string) => this.props.addDocTab(doc, e.ctrlKey ? "inTab" : followLinkLocation)); + (doc: Doc, followLinkLocation: string) => this.props.addDocTab(doc, e.ctrlKey ? "add" : followLinkLocation)); } } @@ -1316,7 +1316,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } onPointerWheel = (e: React.WheelEvent): void => { // if a text note is not selected and scrollable, this prevents us from being able to scroll and zoom out at the same time - if (this.props.isSelected(true) || e.currentTarget.scrollHeight > e.currentTarget.clientHeight) { + if ((this.props.rootSelected(true) || this.props.isSelected(true)) || e.currentTarget.scrollHeight > e.currentTarget.clientHeight) { e.stopPropagation(); } } @@ -1339,7 +1339,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } if (!node && this.ProseRef) { const lastNode = this.ProseRef.children[this.ProseRef.children.length - 1].children[this.ProseRef.children[this.ProseRef.children.length - 1].children.length - 1]; // get the last prosemirror div - if (e.clientY > lastNode?.getBoundingClientRect().bottom) { // if we clicked below the last prosemirror div, then set the selection to be the end of the document + const boundsRect = lastNode?.getBoundingClientRect(); + if (e.clientX > boundsRect.left && e.clientX < boundsRect.right && + e.clientY > boundsRect.bottom) { // if we clicked below the last prosemirror div, then set the selection to be the end of the document + this._editorView?.focus(); this._editorView!.dispatch(this._editorView!.state.tr.setSelection(TextSelection.create(this._editorView!.state.doc, this._editorView!.state.doc.content.size))); } } else if ([this._editorView!.state.schema.nodes.ordered_list, this._editorView!.state.schema.nodes.listItem].includes(node?.type) && @@ -1415,14 +1418,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const self = this; return new Plugin({ view(newView) { - self.props.isSelected(true) && (RichTextMenu.Instance.view = newView); + self.props.isSelected(true) && RichTextMenu.Instance && (RichTextMenu.Instance.view = newView); return self.menuPlugin = new RichTextMenuPlugin({ editorProps: this.props }); } }); } public startUndoTypingBatch() { - this._undoTyping = UndoManager.StartBatch("undoTyping"); + !this._undoTyping && (this._undoTyping = UndoManager.StartBatch("undoTyping")); } public endUndoTypingBatch() { @@ -1433,19 +1436,25 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } return wasUndoing; } + public static LiveTextUndo: UndoManager.Batch | undefined; public static HadSelection: boolean = false; onBlur = (e: any) => { FormattedTextBox.HadSelection = window.getSelection()?.toString() !== ""; - //DictationManager.Controls.stop(false); this.endUndoTypingBatch(); this.doLinkOnDeselect(); + FormattedTextBox.LiveTextUndo?.end(); + FormattedTextBox.LiveTextUndo = undefined; // move the richtextmenu offscreen //if (!RichTextMenu.Instance.Pinned) RichTextMenu.Instance.delayHide(); } _lastTimedMark: Mark | undefined = undefined; - onKeyPress = (e: React.KeyboardEvent) => { + onKeyDown = (e: React.KeyboardEvent) => { + // single line text boxes need to pass through tab/enter/backspace so that their containers can respond (eg, an outline container) + if (this.rootDoc._singleLine && ((e.key === "Backspace" && !this.dataDoc[this.fieldKey]?.Text) || ["Tab", "Enter"].includes(e.key))) { + return; + } if (e.altKey) { e.preventDefault(); return; @@ -1478,9 +1487,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(mark)); } - if (!this._undoTyping) { - this.startUndoTypingBatch(); - } + this.startUndoTypingBatch(); } ondrop = (eve: React.DragEvent) => { @@ -1492,8 +1499,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } @action tryUpdateHeight(limitHeight?: number) { - let scrollHeight = this._ref.current?.scrollHeight; - if (this.props.renderDepth && this.layoutDoc._autoHeight && !this.props.ignoreAutoHeight && scrollHeight) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation + let scrollHeight = this.ProseRef?.scrollHeight || 0; + this.rootDoc[this.fieldKey + "-height"] = 0; + if (this.props.renderDepth && this.layoutDoc._autoHeight && !this.props.ignoreAutoHeight && scrollHeight && !this.props.dontRegisterView) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation scrollHeight = scrollHeight * NumCast(this.layoutDoc._viewScale, 1); if (limitHeight && scrollHeight > limitHeight) { scrollHeight = limitHeight; @@ -1503,7 +1511,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const nh = this.layoutDoc.isTemplateForField ? 0 : NumCast(this.layoutDoc._nativeHeight, 0); const dh = NumCast(this.rootDoc._height, 0); const newHeight = Math.max(10, (nh ? dh / nh * scrollHeight : scrollHeight) + (this.props.ChromeHeight ? this.props.ChromeHeight() : 0)); - if (this.rootDoc !== this.layoutDoc.doc && !this.layoutDoc.resolvedDataDoc) { + if (!this.props.LayoutTemplateString && this.rootDoc !== this.layoutDoc.doc && !this.layoutDoc.resolvedDataDoc) { // if we have a template that hasn't been resolved yet, we can't set the height or we'd be setting it on the unresolved template. So set a timeout and hope its arrived... console.log("Delayed height adjustment..."); setTimeout(() => { @@ -1511,52 +1519,115 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this.layoutDoc._nativeHeight = nh ? scrollHeight : undefined; }, 10); } else { - this.layoutDoc._height = newHeight; - this.layoutDoc._nativeHeight = nh ? scrollHeight : undefined; + try { + const boxHeight = Number(getComputedStyle(this._boxRef.current!).height.replace("px", "")); + const outer = this.rootDoc[HeightSym]() - boxHeight - (this.props.ChromeHeight ? this.props.ChromeHeight() : 0); + const finalHeight = newHeight + Math.max(0, outer); + const maxsidebar = !this.sidebarWidth() ? 0 : Array.from(this._boxRef.current!.getElementsByClassName("collectionStackingViewFieldColumn")).reduce((prev, ele) => Math.max(prev, Number(getComputedStyle(ele).height.replace("px", ""))), 0); + if (this.rootDoc._height !== finalHeight && finalHeight > maxsidebar) { + this.rootDoc._height = finalHeight; + this.layoutDoc._nativeHeight = nh ? scrollHeight : undefined; + } + this.rootDoc[this.fieldKey + "-height"] = finalHeight; + } catch (e) { console.log("Error in tryUpdateHeight"); } } } } + @computed get audioHandle() { + return !this.layoutDoc._showAudio ? (null) : + <div className="formattedTextBox-dictation" onClick={action(e => this._recording = !this._recording)} > + <FontAwesomeIcon className="formattedTextBox-audioFont" style={{ color: this._recording ? "red" : "blue", transitionDelay: "0.6s", opacity: this._recording ? 1 : 0.25, }} icon={"microphone"} size="sm" /> + </div>; + } + + @computed get sidebarHandle() { + const annotated = DocListCast(this.dataDoc[this.annotationKey]).filter(d => d?.title).length; + return !this.props.isSelected() && !(annotated && !this.sidebarWidth()) ? (null) : + <div className="formattedTextBox-sidebar-handle" + style={{ left: `max(0px, calc(100% - ${this.sidebarWidthPercent} ${this.sidebarWidth() ? "- 5px" : "- 10px"}))`, background: annotated ? "lightBlue" : undefined }} + onPointerDown={this.sidebarDown} + onClick={e => { + console.log(e); + }} + />; + } + + @computed get sidebarCollection() { + const fitToBox = this.props.Document._fitToBox; + const collectionProps = { + ...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit, + PanelHeight: this.props.PanelHeight, + PanelWidth: this.sidebarWidth, + xMargin: 0, + yMargin: 0, + chromeStatus: "enabled", + scaleField: this.annotationKey + "-scale", + annotationsKey: this.annotationKey, + isAnnotationOverlay: true, + fitToBox: fitToBox, + focus: this.props.focus, + isSelected: this.props.isSelected, + select: emptyFunction, + active: this.annotationsActive, + ContentScaling: returnOne, + whenActiveChanged: this.whenActiveChanged, + removeDocument: this.removeDocument, + moveDocument: this.moveDocument, + addDocument: this.addDocument, + CollectionView: undefined, + ScreenToLocalTransform: this.sidebarScreenToLocal, + renderDepth: this.props.renderDepth + 1, + ContainingCollectionDoc: this.props.ContainingCollectionDoc, + }; + return !this.layoutDoc._showSidebar || this.sidebarWidthPercent === "0%" ? (null) : + <div className={"formattedTextBox-sidebar" + (Doc.GetSelectedTool() !== InkTool.None ? "-inking" : "")} + style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> + {this.layoutDoc.sidebarViewType === CollectionViewType.Freeform ? <CollectionFreeFormView {...collectionProps} /> : <CollectionStackingView {...collectionProps} />} + </div>; + } + @computed get sidebarWidthPercent() { return StrCast(this.layoutDoc._sidebarWidthPercent, "0%"); } sidebarWidth = () => Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100 * this.props.PanelWidth(); sidebarScreenToLocal = () => this.props.ScreenToLocalTransform().translate(-(this.props.PanelWidth() - this.sidebarWidth()) / this.props.ContentScaling(), 0); - @computed get sidebarColor() { return StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], "transparent")); } + @computed get sidebarColor() { return StrCast(this.layoutDoc.sidebarColor, StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], "#e4e4e4")); } render() { TraceMobx(); + const selected = this.props.isSelected(); + const active = this.active(); const scale = this.props.hideOnLeave ? 1 : this.props.ContentScaling() * NumCast(this.layoutDoc._viewScale, 1); const rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : ""; - const interactive = Doc.GetSelectedTool() === InkTool.None && !this.layoutDoc.isBackground; - this.props.isSelected() && setTimeout(() => this._editorView && RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props), 0); // need to make sure that we update a text box that is selected after updating the one that was deselected - if (!this.props.isSelected() && FormattedTextBoxComment.textBox === this) { - setTimeout(() => FormattedTextBoxComment.Hide(), 0); - } - const selPad = this.props.isSelected() ? -10 : 0; - const selclass = this.props.isSelected() ? "-selected" : ""; + const interactive = (Doc.GetSelectedTool() === InkTool.None || SnappingManager.GetIsDragging()) && !this.layoutDoc._isBackground; + selected && setTimeout(() => this._editorView && RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props)); // need to make sure that we update a text box that is selected after updating the one that was deselected + if (!selected && FormattedTextBoxComment.textBox === this) { FormattedTextBoxComment.Hide(); } + const minimal = this.props.ignoreAutoHeight; + const margins = NumCast(this.layoutDoc._yMargin, this.props.yMargin || 0); + const selPad = Math.min(margins, 10); + const padding = Math.max(margins + ((selected && !this.layoutDoc._singleLine) || minimal ? -selPad : 0), 0); + const selclass = selected && !this.layoutDoc._singleLine && margins >= 10 ? "-selected" : ""; return ( - <div className={"formattedTextBox-cont"} + <div className={"formattedTextBox-cont"} ref={this._boxRef} style={{ transform: `scale(${scale})`, transformOrigin: "top left", width: `${100 / scale}%`, height: `calc(${100 / scale}% - ${this.props.ChromeHeight?.() || 0}px)`, - ...this.styleFromLayoutString(scale) + ...this.styleFromLayoutString(scale) // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._headerHeight}px' > }}> <div className={`formattedTextBox-cont`} ref={this._ref} style={{ overflow: this.layoutDoc._autoHeight ? "hidden" : undefined, width: "100%", - height: this.props.height ? this.props.height : this.layoutDoc._autoHeight && this.props.renderDepth ? "max-content" : undefined, + height: this.props.height || (this.layoutDoc._autoHeight && this.props.renderDepth ? "max-content" : undefined), background: Doc.UserDoc().renderStyle === "comic" ? "transparent" : this.props.background ? this.props.background : StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], this.props.hideOnLeave ? "rgba(0,0,0 ,0.4)" : ""), - opacity: this.props.hideOnLeave ? (this._entered ? 1 : 0.1) : 1, color: this.props.color ? this.props.color : StrCast(this.layoutDoc[this.props.fieldKey + "-color"], this.props.hideOnLeave ? "white" : "inherit"), pointerEvents: interactive ? undefined : "none", - fontSize: Cast(this.layoutDoc._fontSize, "string", null), + fontSize: this.props.fontSize || Cast(this.layoutDoc._fontSize, "string", null), fontWeight: Cast(this.layoutDoc._fontWeight, "number", null), fontFamily: StrCast(this.layoutDoc._fontFamily, "inherit"), - transition: "opacity 1s" }} onContextMenu={this.specificContextMenu} - onKeyDown={this.onKeyPress} + onKeyDown={this.onKeyDown} onFocus={this.onFocused} onClick={this.onClick} onPointerMove={e => this.hitBulletTargets(e.clientX, e.clientY, e.shiftKey, true)} @@ -1566,66 +1637,29 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp onMouseUp={this.onMouseUp} onWheel={this.onPointerWheel} onPointerEnter={action(() => this._entered = true)} - onPointerLeave={action((e: React.PointerEvent<HTMLDivElement>) => { + onPointerLeave={action(e => { this._entered = false; const target = document.elementFromPoint(e.nativeEvent.x, e.nativeEvent.y); for (let child: any = target; child; child = child?.parentElement) { - if (child === this._ref.current!) { - this._entered = true; - } + child === this._ref.current! && (this._entered = true); } })} onDoubleClick={this.onDoubleClick} > <div className={`formattedTextBox-outer`} ref={this._scrollRef} - style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: !this.props.active() ? "none" : undefined }} + style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: !active && !SnappingManager.GetIsDragging() ? "none" : undefined }} onScroll={this.onscrolled} onDrop={this.ondrop} > - <div className={`formattedTextBox-inner${rounded}${selclass}`} ref={this.createDropTarget} + <div className={minimal ? "formattedTextBox-minimal" : `formattedTextBox-inner${rounded}${selclass}`} ref={this.createDropTarget} style={{ overflow: this.layoutDoc._singleLine ? "hidden" : undefined, - padding: this.layoutDoc._textBoxPadding ? StrCast(this.layoutDoc._textBoxPadding) : `${Math.max(0, NumCast(this.layoutDoc._yMargin, this.props.yMargin || 0) + selPad)}px ${NumCast(this.layoutDoc._xMargin, this.props.xMargin || 0) + selPad}px`, - pointerEvents: !this.props.active() ? ((this.layoutDoc.isLinkButton || this.props.onClick) ? "none" : undefined) : undefined + padding: this.layoutDoc._textBoxPadding ? StrCast(this.layoutDoc._textBoxPadding) : `${padding}px`, + pointerEvents: !active && !SnappingManager.GetIsDragging() ? ((this.layoutDoc.isLinkButton || this.props.onClick) ? "none" : undefined) : undefined }} /> </div> - {!this.layoutDoc._showSidebar ? (null) : this.sidebarWidthPercent === "0%" ? - <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} /> : - <div className={"formattedTextBox-sidebar" + (Doc.GetSelectedTool() !== InkTool.None ? "-inking" : "")} - style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> - <CollectionFreeFormView {...this.props} - PanelHeight={this.props.PanelHeight} - PanelWidth={this.sidebarWidth} - NativeHeight={returnZero} - NativeWidth={returnZero} - scaleField={this.annotationKey + "-scale"} - annotationsKey={this.annotationKey} - isAnnotationOverlay={false} - focus={this.props.focus} - isSelected={this.props.isSelected} - select={emptyFunction} - active={this.annotationsActive} - ContentScaling={returnOne} - whenActiveChanged={this.whenActiveChanged} - removeDocument={this.removeDocument} - moveDocument={this.moveDocument} - addDocument={this.addDocument} - CollectionView={undefined} - ScreenToLocalTransform={this.sidebarScreenToLocal} - renderDepth={this.props.renderDepth + 1} - ContainingCollectionDoc={this.props.ContainingCollectionDoc}> - </CollectionFreeFormView> - <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} /> - </div>} - {!this.layoutDoc._showAudio ? (null) : - <div className="formattedTextBox-dictation" - onPointerDown={e => { - runInAction(() => this._recording = !this._recording); - setTimeout(() => this._editorView!.focus(), 500); - e.stopPropagation(); - }} > - <FontAwesomeIcon className="formattedTextBox-audioFont" - style={{ color: this._recording ? "red" : "blue", opacity: this._recording ? 1 : 0.5, display: this.props.isSelected() ? "" : "none" }} icon={"microphone"} size="sm" /> - </div>} + {this.sidebarCollection} + {this.sidebarHandle} + {this.audioHandle} </div> </div> ); diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss b/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss index 6a403cb17..582ada6ce 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss @@ -13,11 +13,12 @@ .FormattedTextBoxComment { background-color: white; border: 8px solid white; + //width: 200px; //display: flex; .FormattedTextBoxComment-info { - margin-bottom: 9px; + margin-bottom: 37px; .FormattedTextBoxComment-title { padding-right: 4px; @@ -61,7 +62,7 @@ } .FormattedTextBoxComment-preview-wrapper { - width: 170px; + //width: 170px; height: 170px; overflow: hidden; //padding-top: 5px; diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx index 6f3984f39..5183e193b 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx @@ -1,28 +1,28 @@ +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Tooltip } from "@material-ui/core"; +import { action, observable } from "mobx"; import { Mark, ResolvedPos } from "prosemirror-model"; import { EditorState, Plugin } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import * as ReactDOM from 'react-dom'; +import wiki from "wikijs"; import { Doc, DocCastAsync, Opt } from "../../../../fields/Doc"; import { Cast, FieldValue, NumCast, StrCast } from "../../../../fields/Types"; -import { emptyFunction, returnEmptyString, returnFalse, Utils, emptyPath, returnZero, returnOne, returnEmptyFilter } from "../../../../Utils"; +import { emptyFunction, emptyPath, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, Utils } from "../../../../Utils"; import { DocServer } from "../../../DocServer"; -import { DocumentManager } from "../../../util/DocumentManager"; -import { schema } from "./schema_rts"; +import { Docs } from "../../../documents/Documents"; +import { DocumentType } from "../../../documents/DocumentTypes"; +import { LinkManager } from "../../../util/LinkManager"; import { Transform } from "../../../util/Transform"; +import { undoBatch } from "../../../util/UndoManager"; import { ContentFittingDocumentView } from "../ContentFittingDocumentView"; +import { DocumentLinksButton } from "../DocumentLinksButton"; +import { DocumentView } from "../DocumentView"; +import { LinkDocPreview } from "../LinkDocPreview"; import { FormattedTextBox } from "./FormattedTextBox"; import './FormattedTextBoxComment.scss'; +import { schema } from "./schema_rts"; import React = require("react"); -import { Docs } from "../../../documents/Documents"; -import wiki from "wikijs"; -import { DocumentType } from "../../../documents/DocumentTypes"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action } from "mobx"; -import { LinkManager } from "../../../util/LinkManager"; -import { LinkDocPreview } from "../LinkDocPreview"; -import { DocumentLinksButton } from "../DocumentLinksButton"; -import { Tooltip } from "@material-ui/core"; -import { undoBatch } from "../../../util/UndoManager"; export let formattedTextBoxCommentPlugin = new Plugin({ view(editorView) { return new FormattedTextBoxComment(editorView); } @@ -60,6 +60,7 @@ export function findEndOfMark(rpos: ResolvedPos, view: EditorView, finder: (mark // this view appears when clicking on text that has a hyperlink which is configured to show a preview of its target. // this will also display metadata information about text when the view is configured to display things like other people who authored text. // + export class FormattedTextBoxComment { static tooltip: HTMLElement; static tooltipText: HTMLElement; @@ -72,6 +73,13 @@ export class FormattedTextBoxComment { static _deleteRef: Opt<HTMLDivElement | null>; static _followRef: Opt<HTMLDivElement | null>; + static _nextRef: Opt<HTMLDivElement | null>; + + static _lastState?: EditorState; + static _lastView?: EditorView; + + @observable static _hrefInd = 0; + static _hrefs: string[] | undefined = []; constructor(view: any) { if (!FormattedTextBoxComment.tooltip) { @@ -80,84 +88,67 @@ export class FormattedTextBoxComment { FormattedTextBoxComment.tooltipInput.type = "checkbox"; FormattedTextBoxComment.tooltip = document.createElement("div"); FormattedTextBoxComment.tooltipText = document.createElement("div"); - FormattedTextBoxComment.tooltipText.style.width = "100%"; + //FormattedTextBoxComment.tooltipText.style.width = "100%"; FormattedTextBoxComment.tooltipText.style.height = "100%"; FormattedTextBoxComment.tooltipText.style.textOverflow = "ellipsis"; FormattedTextBoxComment.tooltip.appendChild(FormattedTextBoxComment.tooltipText); FormattedTextBoxComment.tooltip.className = "FormattedTextBox-tooltip"; FormattedTextBoxComment.tooltip.style.pointerEvents = "all"; - FormattedTextBoxComment.tooltip.style.maxWidth = "200px"; + FormattedTextBoxComment.tooltip.style.maxWidth = "400px"; FormattedTextBoxComment.tooltip.style.maxHeight = "235px"; - FormattedTextBoxComment.tooltip.style.width = "100%"; + //FormattedTextBoxComment.tooltip.style.width = "100%"; FormattedTextBoxComment.tooltip.style.height = "100%"; FormattedTextBoxComment.tooltip.style.overflow = "hidden"; FormattedTextBoxComment.tooltip.style.display = "none"; - FormattedTextBoxComment.tooltip.appendChild(FormattedTextBoxComment.tooltipInput); + // FormattedTextBoxComment.tooltip.appendChild(FormattedTextBoxComment.tooltipInput); FormattedTextBoxComment.tooltip.onpointerdown = async (e: PointerEvent) => { const keep = e.target && (e.target as any).type === "checkbox" ? true : false; const textBox = FormattedTextBoxComment.textBox; - if (FormattedTextBoxComment.linkDoc && !keep && textBox) { - if (FormattedTextBoxComment.linkDoc.author) { - - if (FormattedTextBoxComment._deleteRef && FormattedTextBoxComment._deleteRef.contains(e.target as any)) { + const linkDoc = FormattedTextBoxComment.linkDoc; + if (linkDoc && !keep && textBox) { + if (linkDoc.author) { + if (FormattedTextBoxComment._deleteRef?.contains(e.target as any)) { this.deleteLink(); - } else if (FormattedTextBoxComment._followRef && FormattedTextBoxComment._followRef.contains(e.target as any)) { - if (FormattedTextBoxComment.linkDoc.type !== DocumentType.LINK) { - textBox.props.addDocTab(FormattedTextBoxComment.linkDoc, e.ctrlKey ? "inTab" : "onRight"); - } else { - const anchor = FieldValue(Doc.AreProtosEqual(FieldValue(Cast(FormattedTextBoxComment.linkDoc.anchor1, Doc)), textBox.dataDoc) ? - Cast(FormattedTextBoxComment.linkDoc.anchor2, Doc) : (Cast(FormattedTextBoxComment.linkDoc.anchor1, Doc)) - || FormattedTextBoxComment.linkDoc); - const target = anchor?.annotationOn ? await DocCastAsync(anchor.annotationOn) : anchor; - - if (FormattedTextBoxComment.linkDoc.follow) { - if (FormattedTextBoxComment.linkDoc.follow === "Default") { - DocumentManager.Instance.FollowLink(FormattedTextBoxComment.linkDoc, textBox.props.Document, doc => textBox.props.addDocTab(doc, "onRight"), false); - } else if (FormattedTextBoxComment.linkDoc.follow === "Always open in right tab") { - if (target) { textBox.props.addDocTab(target, "onRight"); } - } else if (FormattedTextBoxComment.linkDoc.follow === "Always open in new tab") { - if (target) { textBox.props.addDocTab(target, "inTab"); } - } - } else { - DocumentManager.Instance.FollowLink(FormattedTextBoxComment.linkDoc, textBox.props.Document, doc => textBox.props.addDocTab(doc, "onRight"), false); - } - } + } else if (FormattedTextBoxComment._nextRef?.contains(e.target as any)) { + FormattedTextBoxComment.showPreview(FormattedTextBoxComment._lastView!, FormattedTextBoxComment._lastState, FormattedTextBoxComment._hrefs?.[(++FormattedTextBoxComment._hrefInd) % FormattedTextBoxComment._hrefs?.length]); } else { - if (FormattedTextBoxComment.linkDoc.type !== DocumentType.LINK) { - textBox.props.addDocTab(FormattedTextBoxComment.linkDoc, e.ctrlKey ? "inTab" : "onRight"); + FormattedTextBoxComment.linkDoc = undefined; + if (linkDoc.type !== DocumentType.LINK) { + textBox.props.addDocTab(linkDoc, e.ctrlKey ? "add" : "add:right"); } else { - DocumentManager.Instance.FollowLink(FormattedTextBoxComment.linkDoc, textBox.props.Document, - (doc: Doc, followLinkLocation: string) => textBox.props.addDocTab(doc, e.ctrlKey ? "inTab" : followLinkLocation)); + const target = LinkManager.getOppositeAnchor(linkDoc, textBox.dataDoc); + target && DocumentView.followLinkClick(linkDoc, textBox.dataDoc, textBox.props, e.shiftKey, e.altKey); } } - } } else if (textBox && (FormattedTextBoxComment.tooltipText as any).href) { - textBox.props.addDocTab(Docs.Create.WebDocument((FormattedTextBoxComment.tooltipText as any).href, { title: (FormattedTextBoxComment.tooltipText as any).href, _width: 200, _height: 400, UseCors: true }), "onRight"); + textBox.props.addDocTab(Docs.Create.WebDocument((FormattedTextBoxComment.tooltipText as any).href, { title: (FormattedTextBoxComment.tooltipText as any).href, _fitWidth: true, _width: 200, _height: 400, useCors: true }), "add:right"); } keep && textBox && FormattedTextBoxComment.start !== undefined && textBox.adoptAnnotation( FormattedTextBoxComment.start, FormattedTextBoxComment.end, FormattedTextBoxComment.mark); e.stopPropagation(); e.preventDefault(); }; - root && root.appendChild(FormattedTextBoxComment.tooltip); + root?.appendChild(FormattedTextBoxComment.tooltip); } } @undoBatch - @action - deleteLink = () => { + deleteLink = action(() => { FormattedTextBoxComment.linkDoc ? LinkManager.Instance.deleteLink(FormattedTextBoxComment.linkDoc) : null; LinkDocPreview.LinkInfo = undefined; DocumentLinksButton.EditLink = undefined; - //FormattedTextBoxComment.tooltipText = undefined; FormattedTextBoxComment.Hide(); - } + }); public static Hide() { FormattedTextBoxComment.textBox = undefined; + FormattedTextBoxComment.linkDoc = undefined; FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = "none"); - ReactDOM.unmountComponentAtNode(FormattedTextBoxComment.tooltipText); + try { + ReactDOM.unmountComponentAtNode(FormattedTextBoxComment.tooltipText); + FormattedTextBoxComment.tooltip.removeChild(FormattedTextBoxComment.tooltipText); + } catch (e) { } } public static SetState(textBox: any, start: number, end: number, mark: Mark) { FormattedTextBoxComment.textBox = textBox; @@ -185,14 +176,20 @@ export class FormattedTextBoxComment { } static update(view: EditorView, lastState?: EditorState, forceUrl: string = "") { - const state = view.state; // Don't do anything if the document/selection didn't change - if (lastState && lastState.doc.eq(state.doc) && - lastState.selection.eq(state.selection)) { + if (!forceUrl && lastState?.doc.eq(view.state.doc) && lastState?.selection.eq(view.state.selection)) { return; } + FormattedTextBoxComment._lastState = lastState; + FormattedTextBoxComment._lastView = view; + FormattedTextBoxComment._hrefs = forceUrl ? forceUrl.trim().split(" ") : undefined; + FormattedTextBoxComment._hrefInd = 0; FormattedTextBoxComment.linkDoc = undefined; + FormattedTextBoxComment.showPreview(view, lastState, FormattedTextBoxComment._hrefs?.[FormattedTextBoxComment._hrefInd]); + } + static showPreview(view: EditorView, lastState?: EditorState, forceUrl: string = "") { + const state = view.state; const textBox = FormattedTextBoxComment.textBox; if (!textBox || !textBox.props) { return; @@ -217,7 +214,7 @@ export class FormattedTextBoxComment { FormattedTextBoxComment.SetState(FormattedTextBoxComment.textBox, state.selection.$from.pos - nbef, state.selection.$from.pos + naft, mark); } if (mark && child && ((nbef && naft) || !noselection)) { - FormattedTextBoxComment.tooltipText.textContent = mark.attrs.userid + " date=" + (new Date(mark.attrs.modified * 5000)).toDateString(); + FormattedTextBoxComment.tooltipText.textContent = mark.attrs.userid + " on " + (new Date(mark.attrs.modified * 1000)).toLocaleString(); set = ""; FormattedTextBoxComment.tooltipInput.style.display = ""; } @@ -230,10 +227,22 @@ export class FormattedTextBoxComment { state.doc.nodesBetween(state.selection.from, state.selection.to, (node: any, pos: number, parent: any) => !child && node.marks.length && (child = node)); child = child || (nbef && state.selection.$from.nodeBefore); const mark = child ? findLinkMark(child.marks) : undefined; - const href = (!mark?.attrs.docref || naft === nbef) && mark?.attrs.allLinks.find((item: { href: string }) => item.href)?.href || forceUrl; + const href = forceUrl || (!mark?.attrs.docref || naft === nbef) && mark?.attrs.allLinks.find((item: { href: string }) => item.href)?.href; if (forceUrl || (href && child && nbef && naft && mark?.attrs.showPreview)) { - FormattedTextBoxComment.tooltipText.textContent = "external => " + href; + try { + ReactDOM.unmountComponentAtNode(FormattedTextBoxComment.tooltipText); + FormattedTextBoxComment.tooltip.removeChild(FormattedTextBoxComment.tooltipText); + } catch (e) { } + FormattedTextBoxComment.tooltipText = document.createElement("div"); + FormattedTextBoxComment.tooltipText.className = "FormattedTextBoxComment-toolTipText"; + FormattedTextBoxComment.tooltipText.style.width = "100%"; + FormattedTextBoxComment.tooltipText.style.height = "100%"; + FormattedTextBoxComment.tooltipText.style.textOverflow = "ellipsis"; + FormattedTextBoxComment.tooltipText.style.cursor = "pointer"; + FormattedTextBoxComment.tooltipText.textContent = "URL: " + href; (FormattedTextBoxComment.tooltipText as any).href = href; + FormattedTextBoxComment.tooltip.appendChild(FormattedTextBoxComment.tooltipText); + if (href.startsWith("https://en.wikipedia.org/wiki/")) { wiki().page(href.replace("https://en.wikipedia.org/wiki/", "")).then(page => page.summary().then(summary => FormattedTextBoxComment.tooltipText.textContent = summary.substring(0, 500))); } else { @@ -241,12 +250,9 @@ export class FormattedTextBoxComment { FormattedTextBoxComment.tooltipText.style.overflow = "hidden"; } if (href.indexOf(Utils.prepend("/doc/")) === 0) { + const docTarget = href.replace(Utils.prepend("/doc/"), "").split("?")[0]; FormattedTextBoxComment.tooltipText.textContent = "target not found..."; (FormattedTextBoxComment.tooltipText as any).href = ""; - const docTarget = href.replace(Utils.prepend("/doc/"), "").split("?")[0]; - try { - ReactDOM.unmountComponentAtNode(FormattedTextBoxComment.tooltipText); - } catch (e) { } docTarget && DocServer.GetRefField(docTarget).then(async linkDoc => { if (linkDoc instanceof Doc) { (FormattedTextBoxComment.tooltipText as any).href = href; @@ -259,34 +265,35 @@ export class FormattedTextBoxComment { if (target?.author) { FormattedTextBoxComment.showCommentbox("", view, nbef); - const title = StrCast(target.title).length > 16 ? - StrCast(target.title).substr(0, 16) + "..." : target.title; - + const title = StrCast(target.title).length > 16 ? StrCast(target.title).substr(0, 16) + "..." : target.title; const docPreview = <div className="FormattedTextBoxComment"> <div className="FormattedTextBoxComment-info"> <div className="FormattedTextBoxComment-title"> {title} - {FormattedTextBoxComment.linkDoc.description !== "" ? <p className="FormattedTextBoxComment-description"> - {StrCast(FormattedTextBoxComment.linkDoc.description)}</p> : null} + {FormattedTextBoxComment.linkDoc.description === "" ? (null) : + <p className="FormattedTextBoxComment-description"> {StrCast(FormattedTextBoxComment.linkDoc.description)}</p>} </div> <div className="wrapper" style={{ float: "right" }}> + {(FormattedTextBoxComment._hrefs?.length || 0) <= 1 ? (null) : <Tooltip title={<><div className="dash-tooltip">Next Link</div></>} placement="top"> + <div className="FormattedTextBoxComment-button" ref={(r) => this._nextRef = r}> + <FontAwesomeIcon className="FormattedTextBoxComment-fa-icon" icon="chevron-right" color="white" size="sm" /> + </div> + </Tooltip>} <Tooltip title={<><div className="dash-tooltip">Delete Link</div></>} placement="top"> - <div className="FormattedTextBoxComment-button" - ref={(r) => this._deleteRef = r}> - <FontAwesomeIcon className="FormattedTextBoxComment-fa-icon" icon="trash" color="white" - size="sm" /></div> + <div className="FormattedTextBoxComment-button" ref={(r) => this._deleteRef = r}> + <FontAwesomeIcon className="FormattedTextBoxComment-fa-icon" icon="trash" color="white" size="sm" /> + </div> </Tooltip> <Tooltip title={<><div className="dash-tooltip">Follow Link</div></>} placement="top"> - <div className="FormattedTextBoxComment-button" - ref={(r) => this._followRef = r}> - <FontAwesomeIcon className="FormattedTextBoxComment-fa-icon" icon="arrow-right" color="white" - size="sm" /> + <div className="FormattedTextBoxComment-button" ref={(r) => this._followRef = r}> + <FontAwesomeIcon className="FormattedTextBoxComment-fa-icon" icon="arrow-right" color="white" size="sm" /> </div> </Tooltip> - </div> </div> + </div> + </div> <div className="FormattedTextBoxComment-preview-wrapper"> <ContentFittingDocumentView Document={target} @@ -302,29 +309,29 @@ export class FormattedTextBoxComment { pinToPres={returnFalse} dontRegisterView={true} docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} ContainingCollectionDoc={undefined} ContainingCollectionView={undefined} - renderDepth={0} + renderDepth={-1} PanelWidth={() => 175} //Math.min(350, NumCast(target._width, 350))} PanelHeight={() => 175} //Math.min(250, NumCast(target._height, 250))} focus={emptyFunction} whenActiveChanged={returnFalse} bringToFront={returnFalse} ContentScaling={returnOne} - NativeWidth={() => target._nativeWidth ? NumCast(target._nativeWidth) : 0} - NativeHeight={() => target._nativeHeight ? NumCast(target._nativeHeight) : 0} + NativeWidth={target._nativeWidth ? (() => NumCast(target._nativeWidth)) : undefined} + NativeHeight={target._natvieHeight ? (() => NumCast(target._nativeHeight)) : undefined} /> </div> </div>; - - FormattedTextBoxComment.showCommentbox("", view, nbef); ReactDOM.render(docPreview, FormattedTextBoxComment.tooltipText); - FormattedTextBoxComment.tooltip.style.width = NumCast(target._width) ? `${NumCast(target._width)}` : "100%"; - FormattedTextBoxComment.tooltip.style.height = NumCast(target._height) ? `${NumCast(target._height)}` : "100%"; + //FormattedTextBoxComment.tooltip.style.width = "100%"; + FormattedTextBoxComment.tooltip.style.height = "100%"; } } }); diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts index 8faf752b4..cb5823e86 100644 --- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts +++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts @@ -7,11 +7,13 @@ import { splitListItem, wrapInList, } from "prosemirror-schema-list"; import { EditorState, Transaction, TextSelection } from "prosemirror-state"; import { SelectionManager } from "../../../util/SelectionManager"; import { NumCast, BoolCast, Cast, StrCast } from "../../../../fields/Types"; -import { Doc, DataSym } from "../../../../fields/Doc"; +import { Doc, DataSym, DocListCast } from "../../../../fields/Doc"; import { FormattedTextBox } from "./FormattedTextBox"; import { Id } from "../../../../fields/FieldSymbols"; import { Docs } from "../../../documents/Documents"; import { Utils } from "../../../../Utils"; +import { listSpec } from "../../../../fields/Schema"; +import { List } from "../../../../fields/List"; const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false; @@ -33,7 +35,7 @@ export let updateBullets = (tx2: Transaction, schema: Schema, assignedMapStyle?: return tx2; }; -export default function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKeys?: KeyMap): KeyMap { +export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKeys?: KeyMap): KeyMap { const keys: { [key: string]: any } = {}; function bind(key: string, cmd: any) { @@ -45,6 +47,29 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, props: any keys[key] = cmd; } + /// bcz; Argh!! replace with an onEnter func that conditionally handles Enter + const addTextBox = (below: boolean, force?: boolean) => { + if (props.Document.treeViewOutlineMode) return true; // bcz: Arghh .. need to determine if this is an treeViewOutlineBox in which case Enter's are ignored.. + const layoutDoc = props.Document; + const originalDoc = layoutDoc.rootDocument || layoutDoc; + if (force || props.Document._singleLine) { + const layoutKey = StrCast(originalDoc.layoutKey); + const newDoc = Doc.MakeCopy(originalDoc, true); + const dataField = originalDoc[Doc.LayoutFieldKey(newDoc)]; + newDoc[DataSym][Doc.LayoutFieldKey(newDoc)] = dataField === undefined || Cast(dataField, listSpec(Doc), null)?.length !== undefined ? new List<Doc>([]) : undefined; + if (below) newDoc.y = NumCast(originalDoc.y) + NumCast(originalDoc._height) + 10; + else newDoc.x = NumCast(originalDoc.x) + NumCast(originalDoc._width) + 10; + if (layoutKey !== "layout" && originalDoc[layoutKey] instanceof Doc) { + newDoc[layoutKey] = originalDoc[layoutKey]; + } + Doc.GetProto(newDoc).text = undefined; + FormattedTextBox.SelectOnLoad = newDoc[Id]; + props.addDocument(newDoc); + return true; + } + return false; + }; + //History commands bind("Mod-z", undo); bind("Shift-Mod-z", redo); @@ -66,6 +91,11 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, props: any bind("Ctrl-i", wrapInList(schema.nodes.ordered_list)); bind("Tab", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { + /// bcz; Argh!! replace layotuTEmpalteString with a onTab prop conditionally handles Tab); + if (props.Document._singleLine) { + if (!props.LayoutTemplateString) return addTextBox(false, true); + return true; + } const ref = state.selection; const range = ref.$from.blockRange(ref.$to); const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks()); @@ -89,6 +119,8 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, props: any }); bind("Shift-Tab", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { + /// bcz; Argh!! replace with a onShiftTab prop conditionally handles Tab); + if (props.Document._singleLine) return true; const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks()); if (!liftListItem(schema.nodes.list_item)(state.tr, (tx2: Transaction) => { @@ -104,7 +136,7 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, props: any //Command to create a new Tab with a PDF of all the command shortcuts bind("Mod-/", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { const newDoc = Docs.Create.PdfDocument(Utils.prepend("/assets/cheat-sheet.pdf"), { _fitWidth: true, _width: 300, _height: 300 }); - props.addDocTab(newDoc, "onRight"); + props.addDocTab(newDoc, "add:right"); }); //Commands to modify BlockType @@ -136,47 +168,11 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, props: any return tx; }; - const addTextOnRight = (force: boolean) => { - const layoutDoc = props.Document; - const originalDoc = layoutDoc.rootDocument || layoutDoc; - if (force || props.Document._singleLine) { - const layoutKey = StrCast(originalDoc.layoutKey); - const newDoc = Doc.MakeCopy(originalDoc, true); - newDoc[DataSym][Doc.LayoutFieldKey(newDoc)] = undefined; - newDoc.y = NumCast(originalDoc.y) + NumCast(originalDoc._height) + 10; - if (layoutKey !== "layout" && originalDoc[layoutKey] instanceof Doc) { - newDoc[layoutKey] = originalDoc[layoutKey]; - } - Doc.GetProto(newDoc).text = undefined; - FormattedTextBox.SelectOnLoad = newDoc[Id]; - props.addDocument(newDoc); - return true; - } - return false; - }; - //Command to create a text document to the right of the selected textbox - bind("Alt-Enter", (state: EditorState<S>, dispatch: (tx: Transaction<Schema<any, any>>) => void) => { - return addTextOnRight(true); - }); + bind("Alt-Enter", (state: EditorState<S>, dispatch: (tx: Transaction<Schema<any, any>>) => void) => addTextBox(false, true)); //Command to create a text document to the bottom of the selected textbox - bind("Ctrl-Enter", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { - const layoutDoc = props.Document; - const originalDoc = layoutDoc.rootDocument || layoutDoc; - if (originalDoc instanceof Doc) { - const layoutKey = StrCast(originalDoc.layoutKey); - const newDoc = Doc.MakeCopy(originalDoc, true); - newDoc[DataSym][Doc.LayoutFieldKey(newDoc)] = undefined; - newDoc.x = NumCast(originalDoc.x) + NumCast(originalDoc._width) + 10; - if (layoutKey !== "layout" && originalDoc[layoutKey] instanceof Doc) { - newDoc[layoutKey] = originalDoc[layoutKey]; - } - Doc.GetProto(newDoc).text = undefined; - FormattedTextBox.SelectOnLoad = newDoc[Id]; - props.addDocument(newDoc); - } - }); + bind("Ctrl-Enter", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => addTextBox(true, true)); // backspace = chainCommands(deleteSelection, joinBackward, selectNodeBackward); bind("Backspace", (state: EditorState<S>, dispatch: (tx: Transaction<Schema<any, any>>) => void) => { @@ -199,7 +195,7 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, props: any //newlineInCode, createParagraphNear, liftEmptyBlock, splitBlock //command to break line bind("Enter", (state: EditorState<S>, dispatch: (tx: Transaction<Schema<any, any>>) => void) => { - if (addTextOnRight(false)) return true; + if (addTextBox(true, false)) return true; const trange = state.selection.$from.blockRange(state.selection.$to); const path = (state.selection.$from as any).path; const depth = trange ? liftTarget(trange) : undefined; @@ -221,11 +217,13 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, props: any const fromattrs = state.selection.$from.node().attrs; if (!splitBlockKeepMarks(state, (tx3: Transaction) => { const tonode = tx3.selection.$to.node(); - const tx4 = tx3.setNodeMarkup(tx3.selection.to - 1, tonode.type, fromattrs, tonode.marks); - splitMetadata(marks, tx4); - if (!liftListItem(schema.nodes.list_item)(tx4, dispatch as ((tx: Transaction<Schema<any, any>>) => void))) { - dispatch(tx4); - } + if (tx3.selection.to && tx3.doc.nodeAt(tx3.selection.to - 1)) { + const tx4 = tx3.setNodeMarkup(tx3.selection.to - 1, tonode.type, fromattrs, tonode.marks); + splitMetadata(marks, tx4); + if (!liftListItem(schema.nodes.list_item)(tx4, dispatch as ((tx: Transaction<Schema<any, any>>) => void))) { + dispatch(tx4); + } + } else dispatch(tx3.insertText("\r\n")); })) { return false; } diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 213b341e8..68239a8f1 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -1,8 +1,8 @@ import React = require("react"); -import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; -import { faBold, faCaretDown, faChevronLeft, faEyeDropper, faHighlighter, faOutdent, faIndent, faHandPointLeft, faHandPointRight, faItalic, faLink, faPaintRoller, faPalette, faStrikethrough, faSubscript, faSuperscript, faUnderline } from "@fortawesome/free-solid-svg-icons"; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, observable, IReactionDisposer, reaction } from "mobx"; +import { Tooltip } from "@material-ui/core"; +import { action, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import { lift, wrapIn } from "prosemirror-commands"; import { Mark, MarkType, Node as ProsNode, NodeType, ResolvedPos } from "prosemirror-model"; @@ -11,27 +11,24 @@ import { EditorState, NodeSelection, TextSelection } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import { Doc } from "../../../../fields/Doc"; import { DarkPastelSchemaPalette, PastelSchemaPalette } from '../../../../fields/SchemaHeaderField'; -import { Cast, StrCast, BoolCast, NumCast } from "../../../../fields/Types"; +import { Cast, StrCast } from "../../../../fields/Types"; +import { TraceMobx } from "../../../../fields/util"; import { unimplementedFunction, Utils } from "../../../../Utils"; import { DocServer } from "../../../DocServer"; import { LinkManager } from "../../../util/LinkManager"; import { SelectionManager } from "../../../util/SelectionManager"; -import AntimodeMenu, { AntimodeMenuProps } from "../../AntimodeMenu"; +import { undoBatch, UndoManager } from "../../../util/UndoManager"; +import { AntimodeMenu, AntimodeMenuProps } from "../../AntimodeMenu"; import { FieldViewProps } from "../FieldView"; import { FormattedTextBox, FormattedTextBoxProps } from "./FormattedTextBox"; import { updateBullets } from "./ProsemirrorExampleTransfer"; import "./RichTextMenu.scss"; import { schema } from "./schema_rts"; -import { TraceMobx } from "../../../../fields/util"; -import { UndoManager, undoBatch } from "../../../util/UndoManager"; -import { Tooltip } from "@material-ui/core"; const { toggleMark } = require("prosemirror-commands"); -library.add(faBold, faItalic, faChevronLeft, faUnderline, faStrikethrough, faSuperscript, faSubscript, faOutdent, faIndent, faHandPointLeft, faHandPointRight, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink, faPaintRoller); - @observer -export default class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { +export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { static Instance: RichTextMenu; public overMenu: boolean = false; // kind of hacky way to prevent selects not being selectable @@ -78,24 +75,24 @@ export default class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { RichTextMenu.Instance = this; this._canFade = false; //this.Pinned = BoolCast(Doc.UserDoc()["menuRichText-pinned"]); - this.Pinned = true; + runInAction(() => this.Pinned = true); this.fontSizeOptions = [ - { mark: schema.marks.pFontSize.create({ fontSize: 7 }), title: "Set font size", label: "7pt", command: this.changeFontSize }, - { mark: schema.marks.pFontSize.create({ fontSize: 8 }), title: "Set font size", label: "8pt", command: this.changeFontSize }, - { mark: schema.marks.pFontSize.create({ fontSize: 9 }), title: "Set font size", label: "9pt", command: this.changeFontSize }, - { mark: schema.marks.pFontSize.create({ fontSize: 10 }), title: "Set font size", label: "10pt", command: this.changeFontSize }, - { mark: schema.marks.pFontSize.create({ fontSize: 12 }), title: "Set font size", label: "12pt", command: this.changeFontSize }, - { mark: schema.marks.pFontSize.create({ fontSize: 14 }), title: "Set font size", label: "14pt", command: this.changeFontSize }, - { mark: schema.marks.pFontSize.create({ fontSize: 16 }), title: "Set font size", label: "16pt", command: this.changeFontSize }, - { mark: schema.marks.pFontSize.create({ fontSize: 18 }), title: "Set font size", label: "18pt", command: this.changeFontSize }, - { mark: schema.marks.pFontSize.create({ fontSize: 20 }), title: "Set font size", label: "20pt", command: this.changeFontSize }, - { mark: schema.marks.pFontSize.create({ fontSize: 24 }), title: "Set font size", label: "24pt", command: this.changeFontSize }, - { mark: schema.marks.pFontSize.create({ fontSize: 32 }), title: "Set font size", label: "32pt", command: this.changeFontSize }, - { mark: schema.marks.pFontSize.create({ fontSize: 48 }), title: "Set font size", label: "48pt", command: this.changeFontSize }, - { mark: schema.marks.pFontSize.create({ fontSize: 72 }), title: "Set font size", label: "72pt", command: this.changeFontSize }, + { mark: schema.marks.pFontSize.create({ fontSize: 7 }), title: "Set font size", label: "7px", command: this.changeFontSize }, + { mark: schema.marks.pFontSize.create({ fontSize: 8 }), title: "Set font size", label: "8px", command: this.changeFontSize }, + { mark: schema.marks.pFontSize.create({ fontSize: 9 }), title: "Set font size", label: "9px", command: this.changeFontSize }, + { mark: schema.marks.pFontSize.create({ fontSize: 10 }), title: "Set font size", label: "10px", command: this.changeFontSize }, + { mark: schema.marks.pFontSize.create({ fontSize: 12 }), title: "Set font size", label: "12px", command: this.changeFontSize }, + { mark: schema.marks.pFontSize.create({ fontSize: 14 }), title: "Set font size", label: "14px", command: this.changeFontSize }, + { mark: schema.marks.pFontSize.create({ fontSize: 16 }), title: "Set font size", label: "16px", command: this.changeFontSize }, + { mark: schema.marks.pFontSize.create({ fontSize: 18 }), title: "Set font size", label: "18px", command: this.changeFontSize }, + { mark: schema.marks.pFontSize.create({ fontSize: 20 }), title: "Set font size", label: "20px", command: this.changeFontSize }, + { mark: schema.marks.pFontSize.create({ fontSize: 24 }), title: "Set font size", label: "24px", command: this.changeFontSize }, + { mark: schema.marks.pFontSize.create({ fontSize: 32 }), title: "Set font size", label: "32px", command: this.changeFontSize }, + { mark: schema.marks.pFontSize.create({ fontSize: 48 }), title: "Set font size", label: "48px", command: this.changeFontSize }, + { mark: schema.marks.pFontSize.create({ fontSize: 72 }), title: "Set font size", label: "72px", command: this.changeFontSize }, { mark: null, title: "", label: "...", command: unimplementedFunction, hidden: true }, - { mark: null, title: "", label: "13pt", command: unimplementedFunction, hidden: true }, // this is here because the default size is 13, but there is no actual 13pt option + { mark: null, title: "", label: "13px", command: unimplementedFunction, hidden: true }, // this is here because the default size is 13, but there is no actual 13pt option ]; this.fontFamilyOptions = [ @@ -180,7 +177,7 @@ export default class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this.activeListType = this.getActiveListStyle(); this.activeAlignment = this.getActiveAlignment(); this.activeFontFamily = !activeFamilies.length ? "Arial" : activeFamilies.length === 1 ? String(activeFamilies[0]) : "various"; - this.activeFontSize = !activeSizes.length ? "13pt" : activeSizes.length === 1 ? String(activeSizes[0]) : "..."; + this.activeFontSize = !activeSizes.length ? "13px" : activeSizes.length === 1 ? String(activeSizes[0]) : "..."; this.activeFontColor = !activeColors.length ? "black" : activeColors.length === 1 ? String(activeColors[0]) : "..."; this.activeHighlightColor = !activeHighlights.length ? "" : activeHighlights.length === 1 ? String(activeHighlights[0]) : "..."; @@ -258,7 +255,7 @@ export default class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { marks.forEach(m => { m.type === state.schema.marks.pFontFamily && activeFamilies.push(m.attrs.family); m.type === state.schema.marks.pFontColor && activeColors.push(m.attrs.color); - m.type === state.schema.marks.pFontSize && activeSizes.push(String(m.attrs.fontSize) + "pt"); + m.type === state.schema.marks.pFontSize && activeSizes.push(String(m.attrs.fontSize) + "px"); m.type === state.schema.marks.marker && activeHighlights.push(String(m.attrs.highlight)); }); } @@ -387,7 +384,7 @@ export default class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { if (!self.TextView.props.isSelected(true)) { switch (mark.type) { case schema.marks.pFontFamily: setter(Doc.UserDoc().fontFamily = mark.attrs.family); break; - case schema.marks.pFontSize: setter(Doc.UserDoc().fontSize = mark.attrs.fontSize.toString() + "pt"); break; + case schema.marks.pFontSize: setter(Doc.UserDoc().fontSize = mark.attrs.fontSize.toString() + "px"); break; } } else UndoManager.RunInBatch(() => self.view && mark && command(mark, self.view), "text mark dropdown"); @@ -428,7 +425,7 @@ export default class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { changeFontSize = (mark: Mark, view: EditorView) => { if ((this.view?.state.selection.$from.pos || 0) < 2) { - this.TextView.layoutDoc._fontSize = mark.attrs.fontSize; + this.TextView.layoutDoc._fontSize = mark.attrs.fontSize === Number(mark.attrs.fontSize) ? `${mark.attrs.fontSize}pt` : mark.attrs.fontSize; } const fmark = view.state.schema.marks.pFontSize.create({ fontSize: mark.attrs.fontSize }); this.setMark(fmark, view.state, (tx: any) => view.dispatch(tx.addStoredMark(fmark)), true); @@ -678,6 +675,7 @@ export default class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { @action toggleColorDropdown() { this.showColorDropdown = !this.showColorDropdown; } @action setActiveColor(color: string) { this.activeFontColor = color; } get TextView() { return (this.view as any)?.TextView as FormattedTextBox; } + get TextViewFieldKey() { return this.TextView?.props.fieldKey; } createColorButton() { const self = this; @@ -813,7 +811,7 @@ export default class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { <div className="dropdown link-menu"> <p>Linked to:</p> <input value={link} placeholder="Enter URL" onChange={onLinkChange} /> - <button className="make-button" onPointerDown={e => this.makeLinkToURL(link, "onRight")}>Apply hyperlink</button> + <button className="make-button" onPointerDown={e => this.makeLinkToURL(link, "add:right")}>Apply hyperlink</button> <div className="divider"></div> <button className="remove-button" onPointerDown={e => this.deleteLink()}>Remove link</button> </div>; @@ -860,7 +858,7 @@ export default class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { // TODO: should check for valid URL @undoBatch makeLinkToURL = (target: string, lcoation: string) => { - ((this.view as any)?.TextView as FormattedTextBox).makeLinkToSelection("", target, "onRight", "", target); + ((this.view as any)?.TextView as FormattedTextBox).makeLinkToSelection("", target, "onRadd:rightight", "", target); } @undoBatch @@ -1021,6 +1019,7 @@ interface ButtonDropdownProps { dropdownContent: JSX.Element; openDropdownOnButton?: boolean; link?: boolean; + pdf?: boolean; } @observer @@ -1060,13 +1059,22 @@ export class ButtonDropdown extends React.Component<ButtonDropdownProps> { }, 0); } + render() { return ( <div className="button-dropdown-wrapper" ref={node => this.ref = node}> - <div className="antimodeMenu-button dropdown-button-combined" onPointerDown={this.onDropdownClick}> - {this.props.button} - <div style={{ marginTop: "-8.5" }}><FontAwesomeIcon icon="caret-down" size="sm" /></div> - </div> + {!this.props.pdf ? + <div className="antimodeMenu-button dropdown-button-combined" onPointerDown={this.onDropdownClick}> + {this.props.button} + <div style={{ marginTop: "-8.5" }}><FontAwesomeIcon icon="caret-down" size="sm" /></div> + </div> + : + <> + {this.props.button} + <button className="dropdown-button antimodeMenu-button" key="antimodebutton" onPointerDown={this.onDropdownClick}> + <FontAwesomeIcon icon="caret-down" size="sm" /> + </button> + </>} {this.showDropdown ? this.props.dropdownContent : (null)} </div> ); diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index dc1d8a2c8..921c0e128 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -3,13 +3,13 @@ import { NodeSelection, TextSelection } from "prosemirror-state"; import { DataSym, Doc } from "../../../../fields/Doc"; import { Id } from "../../../../fields/FieldSymbols"; import { ComputedField } from "../../../../fields/ScriptField"; -import { Cast, NumCast } from "../../../../fields/Types"; +import { Cast, NumCast, StrCast } from "../../../../fields/Types"; import { returnFalse, Utils } from "../../../../Utils"; import { DocServer } from "../../../DocServer"; import { Docs, DocUtils } from "../../../documents/Documents"; import { FormattedTextBox } from "./FormattedTextBox"; import { wrappingInputRule } from "./prosemirrorPatches"; -import RichTextMenu from "./RichTextMenu"; +import { RichTextMenu } from "./RichTextMenu"; import { schema } from "./schema_rts"; import { List } from "../../../../fields/List"; @@ -90,9 +90,9 @@ export class RichTextRules { textDoc.inlineTextCount = numInlines + 1; const inlineFieldKey = "inline" + numInlines; // which field on the text document this annotation will write to const inlineLayoutKey = "layout_" + inlineFieldKey; // the field holding the layout string that will render the inline annotation - const textDocInline = Docs.Create.TextDocument("", { layoutKey: inlineLayoutKey, _width: 75, _height: 35, annotationOn: textDoc, _autoHeight: true, _fontSize: "9pt", title: "inline comment" }); + const textDocInline = Docs.Create.TextDocument("", { layoutKey: inlineLayoutKey, _width: 75, _height: 35, annotationOn: textDoc, _autoHeight: true, _fontSize: "9px", title: "inline comment" }); textDocInline.title = inlineFieldKey; // give the annotation its own title - textDocInline.customTitle = true; // And make sure that it's 'custom' so that editing text doesn't change the title of the containing doc + textDocInline["title-custom"] = true; // And make sure that it's 'custom' so that editing text doesn't change the title of the containing doc textDocInline.isTemplateForField = inlineFieldKey; // this is needed in case the containing text doc is converted to a template at some point textDocInline.proto = textDoc; // make the annotation inherit from the outer text doc so that it can resolve any nested field references, e.g., [[field]] textDocInline._textContext = ComputedField.MakeFunction(`copyField(self.${inlineFieldKey})`); @@ -267,19 +267,21 @@ export class RichTextRules { // [[fieldKey]] => show field // [[fieldKey:Doc]] => show field of doc new InputRule( - new RegExp(/\[\[([a-zA-Z_@\? \-0-9]*)(=[a-zA-Z_@\? /\-0-9]*)?(:[a-zA-Z_@\? \-0-9]+)?\]\]$/), + new RegExp(/\[\[([a-zA-Z_@\? \-0-9]*)(=[a-zA-Z_@\? /\-0-9]*)?(:[a-zA-Z_@\.\? \-0-9]+)?\]\]$/), (state, match, start, end) => { const fieldKey = match[1]; - const docid = match[3]?.substring(1); + const rawdocid = match[3]?.substring(1); + const docid = rawdocid ? (!rawdocid.includes("@") ? Doc.CurrentUserEmail + "@" + rawdocid : rawdocid).replace(".", "_") : undefined; const value = match[2]?.substring(1); if (!fieldKey) { + const linkId = Utils.GenerateGuid(); if (docid) { DocServer.GetRefField(docid).then(docx => { - const target = ((docx instanceof Doc) && docx) || Docs.Create.FreeformDocument([], { title: docid, _width: 500, _height: 500, }, docid); - DocUtils.Publish(target, docid, returnFalse, returnFalse); - DocUtils.MakeLink({ doc: this.Document }, { doc: target }, "portal to"); + const target = ((docx instanceof Doc) && docx) || Docs.Create.FreeformDocument([], { title: rawdocid, _width: 500, _height: 500, }, docid); + DocUtils.MakeLink({ doc: this.Document }, { doc: target }, "portal to", undefined, linkId); }); - const link = state.schema.marks.linkAnchor.create({ href: Utils.prepend("/doc/" + docid), location: "onRight", title: docid, targetId: docid }); + const allLinks = [{ href: Utils.prepend("/doc/" + docid), title: docid, targetId: docid, linkId }]; + const link = state.schema.marks.linkAnchor.create({ allLinks, title: rawdocid, location: "add:right" }); return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 2).addMark(start, end - 3, link); } return state.tr; @@ -297,15 +299,16 @@ export class RichTextRules { // {{<layout>}} => show layout for this doc // {{<layout> : Doc}} => show layout for another doc new InputRule( - new RegExp(/\{\{([a-zA-Z_ \-0-9]*)(\([a-zA-Z0-9…._/\-]*\))?(:[a-zA-Z_ \-0-9]+)?\}\}$/), + new RegExp(/\{\{([a-zA-Z_ \-0-9]*)(\([a-zA-Z0-9…._/\-]*\))?(:[a-zA-Z_@\.\? \-0-9]+)?\}\}$/), (state, match, start, end) => { const fieldKey = match[1] || ""; const fieldParam = match[2]?.replace("…", "...") || ""; - const docid = match[3]?.substring(1); + const rawdocid = match[3]?.substring(1); + const docid = rawdocid ? (!rawdocid.includes("@") ? Doc.CurrentUserEmail + "@" + rawdocid : rawdocid).replace(".", "_") : undefined; if (!fieldKey && !docid) return state.tr; docid && DocServer.GetRefField(docid).then(docx => { if (!(docx instanceof Doc && docx)) { - const docx = Docs.Create.FreeformDocument([], { title: docid, _width: 500, _height: 500 }, docid); + const docx = Docs.Create.FreeformDocument([], { title: rawdocid, _width: 500, _height: 500 }, docid); DocUtils.Publish(docx, docid, returnFalse, returnFalse); } }); @@ -321,7 +324,11 @@ export class RichTextRules { (state, match, start, end) => { const tag = match[1]; if (!tag) return state.tr; - this.Document[DataSym]["#" + tag] = "."; + this.Document[DataSym]["#" + tag] = "#" + tag; + const tags = StrCast(this.Document[DataSym].tags, ":"); + if (!tags.includes(`#${tag}:`)) { + this.Document[DataSym].tags = `${tags + "#" + tag + ':'}`; + } const fieldView = state.schema.nodes.dashField.create({ fieldKey: "#" + tag }); return state.tr.deleteRange(start, end).insert(start, fieldView); }), diff --git a/src/client/views/nodes/formattedText/RichTextSchema.tsx b/src/client/views/nodes/formattedText/RichTextSchema.tsx index 33a080fe4..1767a04de 100644 --- a/src/client/views/nodes/formattedText/RichTextSchema.tsx +++ b/src/client/views/nodes/formattedText/RichTextSchema.tsx @@ -13,6 +13,7 @@ import { Transform } from "../../../util/Transform"; import { DocumentView } from "../DocumentView"; import { FormattedTextBox } from "./FormattedTextBox"; import React = require("react"); +import { CurrentUserUtils } from "../../../util/CurrentUserUtils"; export class DashDocView { @@ -43,7 +44,7 @@ export class DashDocView { this._outer = document.createElement("span"); this._outer.style.position = "relative"; this._outer.style.textIndent = "0"; - this._outer.style.border = "1px solid " + StrCast(tbox.layoutDoc.color, (Cast(Doc.UserDoc().activeWorkspace, Doc, null).darkScheme ? "dimGray" : "lightGray")); + this._outer.style.border = "1px solid " + StrCast(tbox.layoutDoc.color, (CurrentUserUtils.ActiveDashboard.darkScheme ? "dimGray" : "lightGray")); this._outer.style.width = node.attrs.width; this._outer.style.height = node.attrs.height; this._outer.style.display = node.attrs.hidden ? "none" : "inline-block"; @@ -126,7 +127,7 @@ export class DashDocView { this._reactionDisposer = reaction(() => ({ dim: [finalLayout[WidthSym](), finalLayout[HeightSym]()], color: finalLayout.color }), ({ dim, color }) => { this._dashSpan.style.width = this._outer.style.width = Math.max(20, dim[0]) + "px"; this._dashSpan.style.height = this._outer.style.height = Math.max(20, dim[1]) + "px"; - this._outer.style.border = "1px solid " + StrCast(finalLayout.color, (Cast(Doc.UserDoc().activeWorkspace, Doc, null).darkScheme ? "dimGray" : "lightGray")); + this._outer.style.border = "1px solid " + StrCast(finalLayout.color, (CurrentUserUtils.ActiveDashboard.darkScheme ? "dimGray" : "lightGray")); }, { fireImmediately: true }); const doReactRender = (finalLayout: Doc, resolvedDataDoc: Doc) => { @@ -144,8 +145,6 @@ export class DashDocView { addDocTab={this._textBox.props.addDocTab} pinToPres={returnFalse} renderDepth={self._textBox.props.renderDepth + 1} - NativeHeight={returnZero} - NativeWidth={returnZero} PanelWidth={finalLayout[WidthSym]} PanelHeight={finalLayout[HeightSym]} focus={this.outerFocus} @@ -155,6 +154,8 @@ export class DashDocView { bringToFront={emptyFunction} dontRegisterView={false} docFilters={this._textBox.props.docFilters} + docRangeFilters={this._textBox.props.docRangeFilters} + searchFilterDocs={this._textBox.props.searchFilterDocs} ContainingCollectionView={this._textBox.props.ContainingCollectionView} ContainingCollectionDoc={this._textBox.props.ContainingCollectionDoc} ContentScaling={this.contentScaling} @@ -177,17 +178,6 @@ export class DashDocView { this._renderDisposer?.(); this._renderDisposer = reaction(() => { - // if (!Doc.AreProtosEqual(finalLayout, dashDoc)) { - // finalLayout.rootDocument = dashDoc.aliasOf; // bcz: check on this ... why is it here? - // } - const layoutKey = StrCast(finalLayout.layoutKey); - const finalKey = layoutKey && StrCast(finalLayout[layoutKey]).split("'")?.[1]; - if (finalLayout !== dashDoc && finalKey) { - const finalLayoutField = finalLayout[finalKey]; - if (finalLayoutField instanceof ObjectField) { - finalLayout[finalKey + "-textTemplate"] = ComputedField.MakeFunction(`copyField(this.${finalKey})`, { this: Doc.name }); - } - } return { finalLayout, resolvedDataDoc: Cast(finalLayout.resolvedDataDoc, Doc, null) }; }, (res) => doReactRender(res.finalLayout, res.resolvedDataDoc), diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts index ce784c3d9..cca7ea013 100644 --- a/src/client/views/nodes/formattedText/marks_rts.ts +++ b/src/client/views/nodes/formattedText/marks_rts.ts @@ -36,20 +36,21 @@ export const marks: { [index: string]: MarkSpec } = { }], toDOM(node: any) { const targetids = node.attrs.allLinks.reduce((p: string, item: { href: string, title: string, targetId: string, linkId: string }) => p + " " + item.targetId, ""); + const targethrefs = node.attrs.allLinks.reduce((p: string, item: { href: string, title: string, targetId: string, linkId: string }) => p + " " + item.href, ""); const linkids = node.attrs.allLinks.reduce((p: string, item: { href: string, title: string, targetId: string, linkId: string }) => p + " " + item.linkId, ""); return node.attrs.docref && node.attrs.title ? ["div", ["span", `"`], ["span", 0], ["span", `"`], ["br"], ["a", { ...node.attrs, href: node.attrs.allLinks[0].href, class: "prosemirror-attribution" }, node.attrs.title], ["br"]] : - node.attrs.allLinks.length === 1 ? - ["a", { ...node.attrs, class: linkids, dataTargetids: targetids, title: `${node.attrs.title}`, href: node.attrs.allLinks[0].href, style: `text-decoration: ${linkids === " " ? "underline" : undefined}` }, 0] : - ["div", { class: "prosemirror-anchor" }, - ["span", { class: "prosemirror-linkBtn" }, - ["a", { ...node.attrs, class: linkids, dataTargetids: targetids, title: `${node.attrs.title}` }, 0], - ["input", { class: "prosemirror-hrefoptions" }], - ], - ["div", { class: "prosemirror-links" }, ...node.attrs.allLinks.map((item: { href: string, title: string }) => - ["a", { class: "prosemirror-dropdownlink", href: item.href }, item.title] - )] - ]; + //node.attrs.allLinks.length === 1 ? + ["a", { ...node.attrs, class: linkids, "data-targetids": targetids, "data-targethrefs": targethrefs, title: `${node.attrs.title}`, href: node.attrs.allLinks[0].href, style: `text-decoration: ${linkids === " " ? "underline" : undefined}` }, 0]; + // ["div", { class: "prosemirror-anchor" }, + // ["span", { class: "prosemirror-linkBtn" }, + // ["a", { ...node.attrs, class: linkids, "data-targetids": targetids, title: `${node.attrs.title}` }, 0], + // ["input", { class: "prosemirror-hrefoptions" }], + // ], + // ["div", { class: "prosemirror-links" }, ...node.attrs.allLinks.map((item: { href: string, title: string }) => + // ["a", { class: "prosemirror-dropdownlink", href: item.href }, item.title] + // )] + // ]; } }, diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts index 1616500f6..64f7d27e5 100644 --- a/src/client/views/nodes/formattedText/nodes_rts.ts +++ b/src/client/views/nodes/formattedText/nodes_rts.ts @@ -148,7 +148,7 @@ export const nodes: { [index: string]: NodeSpec } = { alt: { default: null }, title: { default: null }, float: { default: "left" }, - location: { default: "onRight" }, + location: { default: "add:right" }, docid: { default: "" } }, group: "inline", @@ -177,7 +177,7 @@ export const nodes: { [index: string]: NodeSpec } = { height: { default: 100 }, title: { default: null }, float: { default: "right" }, - location: { default: "onRight" }, + location: { default: "add:right" }, hidden: { default: false }, fieldKey: { default: "" }, docid: { default: "" }, diff --git a/src/client/views/nodes/formattedText/prosemirrorPatches.js b/src/client/views/nodes/formattedText/prosemirrorPatches.js index 0969ea4ef..746c93868 100644 --- a/src/client/views/nodes/formattedText/prosemirrorPatches.js +++ b/src/client/views/nodes/formattedText/prosemirrorPatches.js @@ -146,7 +146,9 @@ function isInSetWithAttrs(mark, set, attrs) { for (var i = 0; i < set.length; i++) { if (set[i].type == mark) { if (Array.from(Object.keys(attrs)).reduce((p, akey) => { - return p && JSON.stringify(set[i].attrs[akey]) === JSON.stringify(attrs[akey]); + if (p && JSON.stringify(set[i].attrs[akey]) === JSON.stringify(attrs[akey])) return true; + set[i].attrs.allLinks = Array.from(set[i].attrs.allLinks).filter(a => !Array.from(attrs.allLinks.map(al => al.targetId)).includes(a.targetId) || !Array.from(attrs.allLinks.map(al => al.linkId).includes(a.linkId))) + return false; }, true)) { return set[i]; } @@ -178,7 +180,7 @@ function removeMarkWithAttrs(tr, from, to, mark, attrs) { var style = toRemove[i], found$1 = (void 0); for (var j = 0; j < matched.length; j++) { var m = matched[j]; - if (m.step == step - 1 && style.eq(matched[j].style)) { found$1 = m; } + if (m.step == step - 1 /*&& style.eq(matched[j].style)*/) { found$1 = m; } // bcz: not sure what this is even trying to do, but style.eq doesn't seem to exist anymore } if (found$1) { found$1.to = end; diff --git a/src/client/views/pdf/Annotation.scss b/src/client/views/pdf/Annotation.scss index cc326eb93..8b242854d 100644 --- a/src/client/views/pdf/Annotation.scss +++ b/src/client/views/pdf/Annotation.scss @@ -3,4 +3,5 @@ user-select: none; position: absolute; background-color: rgba(146, 245, 95, 0.467); + transition: opacity 0.5s; }
\ No newline at end of file diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx index d29b638e6..84b14cd61 100644 --- a/src/client/views/pdf/Annotation.tsx +++ b/src/client/views/pdf/Annotation.tsx @@ -1,42 +1,46 @@ import React = require("react"); import { action, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCast, HeightSym, WidthSym } from "../../../fields/Doc"; +import { Doc, DocListCast, HeightSym, WidthSym, Field, Opt } from "../../../fields/Doc"; import { Id } from "../../../fields/FieldSymbols"; import { List } from "../../../fields/List"; -import { Cast, FieldValue, NumCast, StrCast } from "../../../fields/Types"; +import { Cast, FieldValue, BoolCast, NumCast, StrCast, PromiseValue } from "../../../fields/Types"; import { DocumentManager } from "../../util/DocumentManager"; -import PDFMenu from "./PDFMenu"; +import { PDFMenu } from "./PDFMenu"; import "./Annotation.scss"; +import { undoBatch } from "../../util/UndoManager"; interface IAnnotationProps { anno: Doc; addDocTab: (document: Doc, where: string) => boolean; - pinToPres: (document: Doc) => void; + pinToPres: (document: Doc, unpin?: boolean) => void; focus: (doc: Doc) => void; dataDoc: Doc; fieldKey: string; + showInfo: (anno: Opt<Doc>) => void; } @observer -export default +export class Annotation extends React.Component<IAnnotationProps> { render() { - return DocListCast(this.props.anno.annotations).map(a => ( - <RegionAnnotation {...this.props} document={a} x={NumCast(a.x)} y={NumCast(a.y)} width={a[WidthSym]()} height={a[HeightSym]()} key={a[Id]} />)); + return DocListCast(this.props.anno.annotations).map(a => + <RegionAnnotation {...this.props} showInfo={this.props.showInfo} pinToPres={this.props.pinToPres} document={a} x={NumCast(a.x)} y={NumCast(a.y)} width={a[WidthSym]()} height={a[HeightSym]()} key={a[Id]} />); } } interface IRegionAnnotationProps { + anno: Doc; x: number; y: number; width: number; height: number; addDocTab: (document: Doc, where: string) => boolean; - pinToPres: (document: Doc) => void; + pinToPres: (document: Doc, unpin: boolean) => void; document: Doc; dataDoc: Doc; fieldKey: string; + showInfo: (anno: Opt<Doc>) => void; } @observer @@ -82,26 +86,37 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> { pinToPres = () => { const group = FieldValue(Cast(this.props.document.group, Doc)); - group && this.props.pinToPres(group); + const isPinned = group && Doc.isDocPinned(group) ? true : false; + group && this.props.pinToPres(group, isPinned); } + @undoBatch + makePushpin = action(() => { + const group = Cast(this.props.document.group, Doc, null); + group.isPushpin = !group.isPushpin; + }); + + isPushpin = () => BoolCast(Cast(this.props.document.group, Doc, null).isPushpin); + @action - onPointerDown = async (e: React.PointerEvent) => { + onPointerDown = (e: React.PointerEvent) => { if (e.button === 2 || e.ctrlKey) { PDFMenu.Instance.Status = "annotation"; PDFMenu.Instance.Delete = this.deleteAnnotation.bind(this); PDFMenu.Instance.Pinned = false; PDFMenu.Instance.AddTag = this.addTag.bind(this); PDFMenu.Instance.PinToPres = this.pinToPres; + PDFMenu.Instance.MakePushpin = this.makePushpin; + PDFMenu.Instance.IsPushpin = this.isPushpin; PDFMenu.Instance.jumpTo(e.clientX, e.clientY, true); e.stopPropagation(); } else if (e.button === 0) { - const annoGroup = await Cast(this.props.document.group, Doc); - if (annoGroup) { - DocumentManager.Instance.FollowLink(undefined, annoGroup, (doc, followLinkLocation) => this.props.addDocTab(doc, e.ctrlKey ? "inTab" : followLinkLocation), false, undefined); - e.stopPropagation(); - } + e.persist(); + e.stopPropagation(); + PromiseValue(this.props.document.group).then(annoGroup => annoGroup instanceof Doc && + DocumentManager.Instance.FollowLink(undefined, annoGroup, (doc, followLinkLocation) => this.props.addDocTab(doc, e.ctrlKey ? "add" : followLinkLocation), false, undefined) + ); } } @@ -116,16 +131,17 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> { return false; } + @observable _showInfo = false; render() { - return (<div className="pdfAnnotation" onPointerDown={this.onPointerDown} ref={this._mainCont} + return (<div className="pdfAnnotation" onPointerEnter={action(() => this.props.showInfo(this.props.anno))} onPointerLeave={action(() => this.props.showInfo(undefined))} onPointerDown={this.onPointerDown} ref={this._mainCont} style={{ top: this.props.y, left: this.props.x, width: this.props.width, height: this.props.height, - opacity: this._brushed ? 0.5 : undefined, + opacity: !this._showInfo && this._brushed ? 0.5 : undefined, backgroundColor: this._brushed ? "orange" : StrCast(this.props.document.backgroundColor), - transition: "opacity 0.5s", - }} />); + }} > + </div>); } }
\ No newline at end of file diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx index bee282d9b..1f8872e3d 100644 --- a/src/client/views/pdf/PDFMenu.tsx +++ b/src/client/views/pdf/PDFMenu.tsx @@ -1,17 +1,17 @@ import React = require("react"); -import "./PDFMenu.scss"; -import { observable, action, computed, } from "mobx"; -import { observer } from "mobx-react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { unimplementedFunction, returnFalse, Utils } from "../../../Utils"; -import AntimodeMenu, { AntimodeMenuProps } from "../AntimodeMenu"; -import { Doc, Opt } from "../../../fields/Doc"; +import { Tooltip } from "@material-ui/core"; +import { action, computed, observable } from "mobx"; +import { observer } from "mobx-react"; import { ColorState } from "react-color"; +import { Doc, Opt } from "../../../fields/Doc"; +import { returnFalse, setupMoveUpEvents, unimplementedFunction, Utils } from "../../../Utils"; +import { AntimodeMenu, AntimodeMenuProps } from "../AntimodeMenu"; import { ButtonDropdown } from "../nodes/formattedText/RichTextMenu"; - +import "./PDFMenu.scss"; @observer -export default class PDFMenu extends AntimodeMenu<AntimodeMenuProps> { +export class PDFMenu extends AntimodeMenu<AntimodeMenuProps> { static Instance: PDFMenu; private _commentCont = React.createRef<HTMLButtonElement>(); @@ -46,6 +46,8 @@ export default class PDFMenu extends AntimodeMenu<AntimodeMenuProps> { public Delete: () => void = unimplementedFunction; public AddTag: (key: string, value: string) => boolean = returnFalse; public PinToPres: () => void = unimplementedFunction; + public MakePushpin: () => void = unimplementedFunction; + public IsPushpin: () => boolean = returnFalse; public Marquee: { left: number; top: number; width: number; height: number; } | undefined; public get Active() { return this._left > 0; } @@ -57,31 +59,10 @@ export default class PDFMenu extends AntimodeMenu<AntimodeMenuProps> { } pointerDown = (e: React.PointerEvent) => { - document.removeEventListener("pointermove", this.pointerMove); - document.addEventListener("pointermove", this.pointerMove); - document.removeEventListener("pointerup", this.pointerUp); - document.addEventListener("pointerup", this.pointerUp); - - e.stopPropagation(); - e.preventDefault(); - } - - pointerMove = (e: PointerEvent) => { - e.stopPropagation(); - e.preventDefault(); - - if (!this._dragging) { + setupMoveUpEvents(this, e, (e: PointerEvent) => { this.StartDrag(e, this._commentCont.current!); - this._dragging = true; - } - } - - pointerUp = (e: PointerEvent) => { - this._dragging = false; - document.removeEventListener("pointermove", this.pointerMove); - document.removeEventListener("pointerup", this.pointerUp); - e.stopPropagation(); - e.preventDefault(); + return true; + }, returnFalse, returnFalse); } @action @@ -93,7 +74,7 @@ export default class PDFMenu extends AntimodeMenu<AntimodeMenuProps> { @computed get highlighter() { const button = - <button className="antimodeMenu-button color-preview-button" title="" key="highlighter-button" onPointerDown={this.highlightClicked}> + <button className="antimodeMenu-button color-preview-button" title="" key="highlighter-button" onClick={this.highlightClicked}> <FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: "transform 0.1s", transform: this.Highlighting ? "" : "rotate(-45deg)" }} /> <div className="color-preview" style={{ backgroundColor: this.highlightColor }}></div> </button>; @@ -112,12 +93,13 @@ export default class PDFMenu extends AntimodeMenu<AntimodeMenuProps> { </div> </div>; return ( - <ButtonDropdown key={"highlighter"} button={button} dropdownContent={dropdownContent} /> + <Tooltip key="highlighter" title={<div className="dash-tooltip">{"Click to Highlight"}</div>}> + <ButtonDropdown key={"highlighter"} button={button} dropdownContent={dropdownContent} pdf={true} /> + </Tooltip> ); } - @action - changeHighlightColor = (color: string, e: React.PointerEvent) => { + @action changeHighlightColor = (color: string, e: React.PointerEvent) => { const col: ColorState = { hex: color, hsl: { a: 0, h: 0, s: 0, l: 0, source: "" }, hsv: { a: 0, h: 0, s: 0, v: 0, source: "" }, rgb: { a: 0, r: 0, b: 0, g: 0, source: "" }, oldHue: 0, source: "", @@ -127,25 +109,11 @@ export default class PDFMenu extends AntimodeMenu<AntimodeMenuProps> { this.highlightColor = Utils.colorString(col); } - deleteClicked = (e: React.PointerEvent) => { - this.Delete(); - } - - @action - keyChanged = (e: React.ChangeEvent<HTMLInputElement>) => { - this._keyValue = e.currentTarget.value; - } - - @action - valueChanged = (e: React.ChangeEvent<HTMLInputElement>) => { - this._valueValue = e.currentTarget.value; - } - - @action - addTag = (e: React.PointerEvent) => { + @action keyChanged = (e: React.ChangeEvent<HTMLInputElement>) => { this._keyValue = e.currentTarget.value; }; + @action valueChanged = (e: React.ChangeEvent<HTMLInputElement>) => { this._valueValue = e.currentTarget.value; }; + @action addTag = (e: React.PointerEvent) => { if (this._keyValue.length > 0 && this._valueValue.length > 0) { this._added = this.AddTag(this._keyValue, this._valueValue); - setTimeout(action(() => this._added = false), 1000); } } @@ -154,19 +122,34 @@ export default class PDFMenu extends AntimodeMenu<AntimodeMenuProps> { const buttons = this.Status === "pdf" ? [ this.highlighter, - <button key="2" className="antimodeMenu-button" title="Drag to Annotate" ref={this._commentCont} onPointerDown={this.pointerDown}> - <FontAwesomeIcon icon="comment-alt" size="lg" /></button>, + + <Tooltip key="annotate" title={<div className="dash-tooltip">{"Drag to Place Annotation"}</div>}> + <button className="antimodeMenu-button annotate" ref={this._commentCont} onPointerDown={this.pointerDown} style={{ cursor: "grab" }}> + <FontAwesomeIcon icon="comment-alt" size="lg" /> + </button> + </Tooltip>, ] : [ - <button key="5" className="antimodeMenu-button" title="Delete Anchor" onPointerDown={this.deleteClicked}> - <FontAwesomeIcon icon="trash-alt" size="lg" /></button>, - <button key="6" className="antimodeMenu-button" title="Pin to Presentation" onPointerDown={this.PinToPres}> - <FontAwesomeIcon icon="map-pin" size="lg" /></button>, - <div key="7" className="pdfMenu-addTag" > - <input onChange={this.keyChanged} placeholder="Key" style={{ gridColumn: 1 }} /> - <input onChange={this.valueChanged} placeholder="Value" style={{ gridColumn: 3 }} /> - </div>, - <button key="8" className="antimodeMenu-button" title={`Add tag: ${this._keyValue} with value: ${this._valueValue}`} onPointerDown={this.addTag}> - <FontAwesomeIcon style={{ transition: "all .2s" }} color={this._added ? "#42f560" : "white"} icon="check" size="lg" /></button>, + <Tooltip key="trash" title={<div className="dash-tooltip">{"Remove Link Anchor"}</div>}> + <button className="antimodeMenu-button" onPointerDown={this.Delete}> + <FontAwesomeIcon icon="trash-alt" size="lg" /> + </button> + </Tooltip>, + <Tooltip key="Pin" title={<div className="dash-tooltip">{"Pin to Presentation"}</div>}> + <button className="antimodeMenu-button" onPointerDown={this.PinToPres}> + <FontAwesomeIcon icon="map-pin" size="lg" /> + </button> + </Tooltip>, + <Tooltip key="pushpin" title={<div className="dash-tooltip">{"toggle pushpin behavior"}</div>}> + <button className="antimodeMenu-button" style={{ color: this.IsPushpin() ? "black" : "white", backgroundColor: this.IsPushpin() ? "white" : "black" }} onPointerDown={this.MakePushpin}> + <FontAwesomeIcon icon="thumbtack" size="lg" /> + </button> + </Tooltip>, + // <div key="7" className="pdfMenu-addTag" > + // <input onChange={this.keyChanged} placeholder="Key" style={{ gridColumn: 1 }} /> + // <input onChange={this.valueChanged} placeholder="Value" style={{ gridColumn: 3 }} /> + // </div>, + // <button key="8" className="antimodeMenu-button" title={`Add tag: ${this._keyValue} with value: ${this._valueValue}`} onPointerDown={this.addTag}> + // <FontAwesomeIcon style={{ transition: "all .2s" }} color={this._added ? "#42f560" : "white"} icon="check" size="lg" /></button>, ]; return this.getElement(buttons); diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss index 86c73bfee..0ecb4dba4 100644 --- a/src/client/views/pdf/PDFViewer.scss +++ b/src/client/views/pdf/PDFViewer.scss @@ -2,6 +2,8 @@ .pdfViewerDash, .pdfViewerDash-interactive { width: 100%; height: 100%; + top: 0; + left:0; position: absolute; overflow-y: auto; overflow-x: hidden; @@ -57,10 +59,22 @@ left: 0px; display: inline-block; width:100%; + pointer-events: all; } .pdfViewerDash-overlay { pointer-events: none; } + + .pdfViewerDash-overlayAnno { + top: -17px; + pointer-events: none; + width: max-content; + height: 20px; + position: absolute; + background: #b8b8b8; + border-radius: 5px; + display: block; + } .pdfViewerDash-annotationLayer { position: absolute; diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index f9ae78778..77dd40f2a 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -1,42 +1,47 @@ import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; -const pdfjs = require('pdfjs-dist/es5/build/pdf.js'); import * as Pdfjs from "pdfjs-dist"; import "pdfjs-dist/web/pdf_viewer.css"; import { Dictionary } from "typescript-collections"; -import { Doc, DocListCast, FieldResult, HeightSym, Opt, WidthSym, AclAddonly, AclEdit, AclAdmin, DataSym } from "../../../fields/Doc"; +import { AclAddonly, AclAdmin, AclEdit, DataSym, Doc, DocListCast, HeightSym, Opt, WidthSym, Field } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; import { Id } from "../../../fields/FieldSymbols"; import { InkTool } from "../../../fields/InkField"; import { List } from "../../../fields/List"; -import { createSchema, makeInterface, listSpec } from "../../../fields/Schema"; -import { ScriptField, ComputedField } from "../../../fields/ScriptField"; +import { createSchema, makeInterface } from "../../../fields/Schema"; +import { ScriptField } from "../../../fields/ScriptField"; import { Cast, NumCast } from "../../../fields/Types"; import { PdfField } from "../../../fields/URLField"; -import { TraceMobx, GetEffectiveAcl } from "../../../fields/util"; -import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, emptyPath, intersectRect, returnZero, smoothScroll, Utils } from "../../../Utils"; +import { GetEffectiveAcl, TraceMobx } from "../../../fields/util"; +import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, emptyPath, intersectRect, returnZero, smoothScroll, Utils, OmitKeys } from "../../../Utils"; import { Docs, DocUtils } from "../../documents/Documents"; import { DocumentType } from "../../documents/DocumentTypes"; +import { Networking } from "../../Network"; import { DragManager } from "../../util/DragManager"; import { CompiledScript, CompileScript } from "../../util/Scripting"; import { SelectionManager } from "../../util/SelectionManager"; +import { SnappingManager } from "../../util/SnappingManager"; import { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; import { CollectionView } from "../collections/CollectionView"; import { ViewBoxAnnotatableComponent } from "../DocComponent"; -import { DocumentDecorations } from "../DocumentDecorations"; -import Annotation from "./Annotation"; -import PDFMenu from "./PDFMenu"; +import { Annotation } from "./Annotation"; +import { PDFMenu } from "./PDFMenu"; import "./PDFViewer.scss"; +const pdfjs = require('pdfjs-dist/es5/build/pdf.js'); import React = require("react"); -import { SnappingManager } from "../../util/SnappingManager"; +import { LinkDocPreview } from "../nodes/LinkDocPreview"; +import { FormattedTextBoxComment } from "../nodes/formattedText/FormattedTextBoxComment"; +import { CurrentUserUtils } from "../../util/CurrentUserUtils"; +import { SharingManager } from "../../util/SharingManager"; +import { FormattedTextBox } from "../nodes/formattedText/FormattedTextBox"; const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer"); const pdfjsLib = require("pdfjs-dist"); -import { Networking } from "../../Network"; +const _global = (window /* browser */ || global /* node */) as any; export const pageSchema = createSchema({ - curPage: "number", + _curPage: "number", rotation: "number", scrollHeight: "number", serachMatch: "boolean" @@ -55,8 +60,10 @@ interface IViewerProps { fieldKey: string; Document: Doc; DataDoc?: Doc; - docFilters: () => string[]; + searchFilterDocs: () => Doc[]; ContainingCollectionView: Opt<CollectionView>; + docFilters: () => string[]; + docRangeFilters: () => string[]; PanelWidth: () => number; PanelHeight: () => number; ContentScaling: () => number; @@ -70,7 +77,7 @@ interface IViewerProps { active: (outsideReaction?: boolean) => boolean; isChildActive: (outsideReaction?: boolean) => boolean; addDocTab: (document: Doc, where: string) => boolean; - pinToPres: (document: Doc) => void; + pinToPres: (document: Doc, unpin?: boolean) => void; addDocument?: (doc: Doc) => boolean; setPdfViewer: (view: PDFViewer) => void; ScreenToLocalTransform: () => Transform; @@ -95,18 +102,14 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu @observable private _showWaiting = true; @observable private _showCover = false; @observable private _zoomed = 1; + @observable private _overlayAnnoInfo: Opt<Doc>; private _pdfViewer: any; + private _styleRule: any; // stylesheet rule for making hyperlinks clickable private _retries = 0; // number of times tried to create the PDF viewer private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean) => void); private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); - private _reactionDisposer?: IReactionDisposer; - private _selectionReactionDisposer?: IReactionDisposer; - private _annotationReactionDisposer?: IReactionDisposer; - private _scrollTopReactionDisposer?: IReactionDisposer; - private _filterReactionDisposer?: IReactionDisposer; - private _searchReactionDisposer?: IReactionDisposer; - private _searchReactionDisposer2?: IReactionDisposer; + private _disposers: { [name: string]: IReactionDisposer } = {}; private _viewer: React.RefObject<HTMLDivElement> = React.createRef(); private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); private _selectionText: string = ""; @@ -119,8 +122,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu private _viewerIsSetup = false; @computed get allAnnotations() { - return DocListCast(this.dataDoc[this.props.fieldKey + "-annotations"]). - filter(anno => this._script.run({ this: anno }, console.log, true).result); + return DocUtils.FilterDocs(DocListCast(this.dataDoc[this.props.fieldKey + "-annotations"]), this.props.docFilters(), this.props.docRangeFilters(), undefined); } @computed get nonDocAnnotations() { return this.allAnnotations.filter(a => a.annotations); } @@ -134,7 +136,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu const coreFilename = pathComponents.pop()!.split(".")[0]; const params: any = { coreFilename, - pageNum: this.Document.curPage || 1, + pageNum: Math.min(this.props.pdf.numPages, Math.max(1, this.Document._curPage || 1)), }; if (pathComponents.length) { params.subtree = `${pathComponents.join("/")}/`; @@ -143,20 +145,25 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu } else { const params: any = { coreFilename: relative.split("/")[relative.split("/").length - 1], - pageNum: this.Document.curPage || 1, + pageNum: Math.min(this.props.pdf.numPages, Math.max(1, this.Document._curPage || 1)), }; this._coverPath = "http://cs.brown.edu/~bcz/face.gif";//href.startsWith(window.location.origin) ? await Networking.PostToServer("/thumbnail", params) : { width: 100, height: 100, path: "" }; } - runInAction(() => this._showWaiting = this._showCover = true); + runInAction(() => this._showWaiting = true); this.props.startupLive && this.setupPdfJsViewer(); - this._mainCont.current && (this._mainCont.current.scrollTop = this.layoutDoc._scrollTop || 0); - this._searchReactionDisposer = reaction(() => this.Document.searchMatch, + if (this._mainCont.current) { + this._mainCont.current.scrollTop = this.layoutDoc._scrollTop || 0; + const observer = new _global.ResizeObserver(action((entries: any) => this._mainCont.current && (this._mainCont.current.scrollTop = this.layoutDoc._scrollTop || 0))); + observer.observe(this._mainCont.current); + } + + this._disposers.searchMatch = reaction(() => Doc.IsSearchMatch(this.rootDoc), m => { - if (m) (this._lastSearch = true) && this.search(Doc.SearchQuery(), true); + if (m) (this._lastSearch = true) && this.search(Doc.SearchQuery(), m.searchMatch > 0); else !(this._lastSearch = false) && setTimeout(() => !this._lastSearch && this.search("", false, true), 200); }, { fireImmediately: true }); - this._selectionReactionDisposer = reaction(() => this.props.isSelected(), + this._disposers.selected = reaction(() => this.props.isSelected(), selected => { if (!selected) { this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove())); @@ -166,26 +173,30 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu (SelectionManager.SelectedDocuments().length === 1) && this.setupPdfJsViewer(); }, { fireImmediately: true }); - this._reactionDisposer = reaction( + this._disposers.scrollY = reaction( () => this.Document._scrollY, (scrollY) => { if (scrollY !== undefined) { (this._showCover || this._showWaiting) && this.setupPdfJsViewer(); - this._mainCont.current && smoothScroll(1000, this._mainCont.current, (this.Document._scrollY || 0)); + if ((this.props.renderDepth === -1 || (!LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc)) && this._mainCont.current) { + smoothScroll(1000, this._mainCont.current, (this.Document._scrollY || 0)); + } else { + console.log("Waiting for preview"); + } setTimeout(() => this.Document._scrollY = undefined, 1000); } }, { fireImmediately: true } ); + this._disposers.curPage = reaction( + () => this.Document._curPage, + (page) => page !== undefined && page !== this._pdfViewer?.currentPageNumber && this.gotoPage(page), + { fireImmediately: true } + ); } componentWillUnmount = () => { - this._reactionDisposer?.(); - this._scrollTopReactionDisposer?.(); - this._annotationReactionDisposer?.(); - this._filterReactionDisposer?.(); - this._selectionReactionDisposer?.(); - this._searchReactionDisposer?.(); + Object.values(this._disposers).forEach(disposer => disposer?.()); document.removeEventListener("copy", this.copy); } @@ -226,11 +237,15 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu this.props.setPdfViewer(this); await this.initialLoad(); - this._scrollTopReactionDisposer = reaction(() => Cast(this.layoutDoc._scrollTop, "number", null), - (stop) => (stop !== undefined && this.layoutDoc._scrollY === undefined && this._mainCont.current) && (this._mainCont.current.scrollTop = stop), + this._disposers.scrollTop = reaction(() => Cast(this.layoutDoc._scrollTop, "number", null), + (stop) => { + if (stop !== undefined && this.layoutDoc._scrollY === undefined && this._mainCont.current) { + (this._mainCont.current.scrollTop = stop); + } + }, { fireImmediately: true }); - this._filterReactionDisposer = reaction( + this._disposers.filterScript = reaction( () => Cast(this.Document.filterScript, ScriptField), action(scriptField => { const oldScript = this._script.originalScript; @@ -245,8 +260,10 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu } pagesinit = action(() => { - this._pdfViewer.currentScaleValue = this._zoomed = 1; - this.gotoPage(this.Document.curPage || 1); + if (this._pdfViewer._setDocumentViewerElement.offsetParent) { + this._pdfViewer.currentScaleValue = this._zoomed = 1; + this.gotoPage(this.Document._curPage || 1); + } document.removeEventListener("pagesinit", this.pagesinit); }); @@ -262,7 +279,9 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu document.addEventListener("copy", this.copy); const eventBus = new PDFJSViewer.EventBus(true); eventBus._on("pagesinit", this.pagesinit); - eventBus._on("pagerendered", action(() => this._showCover = this._showWaiting = false)); + eventBus._on("pagerendered", action(() => { + this._showWaiting = false; + })); const pdfLinkService = new PDFJSViewer.PDFLinkService({ eventBus }); const pdfFindController = new PDFJSViewer.PDFFindController({ linkService: pdfLinkService, eventBus }); this._pdfViewer = new PDFJSViewer.PDFViewer({ @@ -282,14 +301,15 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu @action makeAnnotationDocument = (color: string): Opt<Doc> => { if (this._savedAnnotations.size() === 0) return undefined; - let mainAnnoDoc = Docs.Create.InstanceFromProto(new Doc(), "", {}); + // let mainAnnoDoc = Docs.Create.InstanceFromProto(new Doc(), "", {}); + let mainAnnoDoc = Docs.Create.FreeformDocument([], { title: "anno", _width: 1, _height: 1 }); let mainAnnoDocProto = Doc.GetProto(mainAnnoDoc); const annoDocs: Doc[] = []; let maxX = -Number.MAX_VALUE; let minY = Number.MAX_VALUE; if ((this._savedAnnotations.values()[0][0] as any).marqueeing) { const anno = this._savedAnnotations.values()[0][0]; - const annoDoc = Docs.Create.FreeformDocument([], { backgroundColor: color, title: "Annotation on " + this.Document.title }); + const annoDoc = Docs.Create.FreeformDocument([], { backgroundColor: color.replace(/[0-9.]*\)/, ".3)"), title: "Annotation on " + this.Document.title }); if (anno.style.left) annoDoc.x = parseInt(anno.style.left); if (anno.style.top) annoDoc.y = parseInt(anno.style.top); if (anno.style.height) annoDoc._height = parseInt(anno.style.height); @@ -313,7 +333,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu annoDocs.push(annoDoc); anno.remove(); (annoDoc.y !== undefined) && (minY = Math.min(NumCast(annoDoc.y), minY)); - (annoDoc.x !== undefined) && (maxX = Math.max(NumCast(annoDoc.x) + NumCast(annoDoc.width), maxX)); + (annoDoc.x !== undefined) && (maxX = Math.max(NumCast(annoDoc.x) + NumCast(annoDoc._width), maxX)); })); mainAnnoDocProto.y = Math.max(minY, 0); @@ -342,7 +362,9 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu @action gotoPage = (p: number) => { - this._pdfViewer?.scrollPageIntoView({ pageNumber: Math.min(Math.max(1, p), this._pageSizes.length) }); + if (this._pdfViewer?._setDocumentViewerElement?.offsetParent) { + this._pdfViewer?.scrollPageIntoView({ pageNumber: Math.min(Math.max(1, p), this._pageSizes.length) }); + } } @action @@ -360,10 +382,17 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu } + pageDelay: any; @action onScroll = (e: React.UIEvent<HTMLElement>) => { - this.Document._scrollY === undefined && (this.layoutDoc._scrollTop = this._mainCont.current!.scrollTop); - this._pdfViewer && (this.Document.curPage = this._pdfViewer.currentPageNumber); + if (!LinkDocPreview.TargetDoc && !FormattedTextBoxComment.linkDoc) { + this.Document._scrollY === undefined && (this.layoutDoc._scrollTop = this._mainCont.current!.scrollTop); + this.pageDelay && clearTimeout(this.pageDelay); + this.pageDelay = setTimeout(() => { + this.pageDelay = undefined; + this._pdfViewer && (this.Document._curPage = this._pdfViewer.currentPageNumber); + }, 250); + } } // get the page index that the vertical offset passed in is on @@ -436,11 +465,10 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu // if alt+left click, drag and annotate this._downX = e.clientX; this._downY = e.clientY; - addStyleSheetRule(PDFViewer._annotationStyle, "pdfAnnotation", { "pointer-events": "none" }); + (e.target as any).tagName === "SPAN" && (this._styleRule = addStyleSheetRule(PDFViewer._annotationStyle, "pdfAnnotation", { "pointer-events": "none" })); if ((this.Document._viewScale || 1) !== 1) return; if ((e.button !== 0 || e.altKey) && this.active(true)) { this._setPreviewCursor?.(e.clientX, e.clientY, true); - //e.stopPropagation(); } this._marqueeing = false; if (!e.altKey && e.button === 0 && this.active(true)) { @@ -462,12 +490,15 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu this._marqueeHeight = this._marqueeWidth = 0; this._marqueeing = true; } - document.removeEventListener("pointermove", this.onSelectMove); document.addEventListener("pointermove", this.onSelectMove); - document.removeEventListener("pointerup", this.onSelectEnd); document.addEventListener("pointerup", this.onSelectEnd); + document.addEventListener("pointerup", this.removeStyle, true); } } + removeStyle = () => { + clearStyleSheetRules(PDFViewer._annotationStyle); + document.removeEventListener("pointerup", this.removeStyle); + } @action onSelectMove = (e: PointerEvent): void => { @@ -560,7 +591,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu } if (PDFMenu.Instance.Highlighting) {// when highlighter has been toggled when menu is pinned, we auto-highlight immediately on mouse up - this.highlight("rgba(245, 230, 95, 0.616)"); // yellowish highlight color for highlighted text (should match PDFMenu's highlight color) + this.highlight("rgba(245, 230, 95, 0.75)"); // yellowish highlight color for highlighted text (should match PDFMenu's highlight color) } else { PDFMenu.Instance.StartDrag = this.startDrag; @@ -593,24 +624,26 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu clipDoc._width = this.marqueeWidth(); clipDoc._height = this.marqueeHeight(); clipDoc._scrollTop = this.marqueeY(); - const targetDoc = Docs.Create.TextDocument("", { _width: 200, _height: 200, title: "Note linked to " + this.props.Document.title }); + const targetDoc = CurrentUserUtils.GetNewTextDoc("Note linked to " + this.props.Document.title, 0, 0, 100, 100); + FormattedTextBox.SelectOnLoad = targetDoc[Id]; Doc.GetProto(targetDoc).data = new List<Doc>([clipDoc]); clipDoc.rootDocument = targetDoc; - DocUtils.makeCustomViewClicked(targetDoc, Docs.Create.StackingDocument, "slideView", undefined); - targetDoc.layoutKey = "layout"; + // DocUtils.makeCustomViewClicked(targetDoc, Docs.Create.StackingDocument, "slideView", undefined); + // targetDoc.layoutKey = "layout"; // const targetDoc = Docs.Create.TextDocument("", { _width: 200, _height: 200, title: "Note linked to " + this.props.Document.title }); // Doc.GetProto(targetDoc).snipped = this.dataDoc[this.props.fieldKey][Copy](); // const snipLayout = Docs.Create.PdfDocument("http://www.msn.com", { title: "snippetView", isTemplateDoc: true, isTemplateForField: "snipped", _fitWidth: true, _width: this.marqueeWidth(), _height: this.marqueeHeight(), _scrollTop: this.marqueeY() }); // Doc.GetProto(snipLayout).layout = PDFBox.LayoutString("snipped"); - const annotationDoc = this.highlight("rgba(146, 245, 95, 0.467)"); // yellowish highlight color when dragging out a text selection + const annotationDoc = this.highlight("rgba(173, 216, 230, 0.75)"); // hyperlink color if (annotationDoc) { DragManager.StartPdfAnnoDrag([ele], new DragManager.PdfAnnoDragData(this.props.Document, annotationDoc, targetDoc), e.pageX, e.pageY, { dragComplete: e => { - if (!e.aborted && e.annoDragData && !e.annoDragData.linkedToDoc) { - const link = DocUtils.MakeLink({ doc: annotationDoc }, { doc: e.annoDragData.dropDocument }, "Annotation"); - annotationDoc.isLinkButton = true; - if (link) link.followLinkLocation = "onRight"; + if (!e.aborted && e.annoDragData && !e.annoDragData.linkDocument) { + e.annoDragData.linkDocument = DocUtils.MakeLink({ doc: annotationDoc }, { doc: e.annoDragData.dropDocument }, "Annotation"); } + annotationDoc.isLinkButton = true; // prevents link button fro showing up --- maybe not a good thing? + annotationDoc.isPushpin = e.annoDragData?.dropDocument.annotationOn === this.props.Document; + e.annoDragData && e.annoDragData.linkDocument && e.annoDragData?.linkDropCallback?.({ linkDocument: e.annoDragData.linkDocument }); } }); } @@ -660,24 +693,33 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu TraceMobx(); return <div className="pdfViewerDash-annotationLayer" style={{ height: NumCast(this.Document._nativeHeight), transform: `scale(${this._zoomed})` }} ref={this._annotationLayer}> {this.nonDocAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map(anno => - <Annotation {...this.props} focus={this.props.focus} dataDoc={this.dataDoc} fieldKey={this.props.fieldKey} anno={anno} key={`${anno[Id]}-annotation`} />) + <Annotation {...this.props} showInfo={this.showInfo} focus={this.props.focus} dataDoc={this.dataDoc} fieldKey={this.props.fieldKey} anno={anno} key={`${anno[Id]}-annotation`} />) } </div>; } + + @computed get overlayInfo() { + return !this._overlayAnnoInfo || this._overlayAnnoInfo.author === Doc.CurrentUserEmail ? (null) : + <div className="pdfViewerDash-overlayAnno" style={{ top: NumCast(this._overlayAnnoInfo.y), left: NumCast(this._overlayAnnoInfo.x) }}> + <div className="pdfViewerDash-overlayAnno" style={{ right: -50, background: SharingManager.Instance.users.find(users => users.user.email === this._overlayAnnoInfo!.author)?.userColor }}> + {this._overlayAnnoInfo.author + " " + Field.toString(this._overlayAnnoInfo.creationDate as Field)} + </div> + </div>; + } + + showInfo = action((anno: Opt<Doc>) => this._overlayAnnoInfo = anno); overlayTransform = () => this.scrollXf().scale(1 / this._zoomed); panelWidth = () => (this.Document.scrollHeight || this.Document._nativeHeight || 0); panelHeight = () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : (this.Document._nativeWidth || 0); @computed get overlayLayer() { - return <div className={`pdfViewerDash-overlay${Doc.GetSelectedTool() !== InkTool.None || SnappingManager.GetIsDragging() ? "-inking" : ""}`} id="overlay" - style={{ transform: `scale(${this._zoomed})` }}> - <CollectionFreeFormView {...this.props} + return <div className={`pdfViewerDash-overlay${Doc.GetSelectedTool() !== InkTool.None || SnappingManager.GetIsDragging() ? "-inking" : ""}`} + style={{ pointerEvents: SnappingManager.GetIsDragging() ? "all" : undefined, transform: `scale(${this._zoomed})` }}> + <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} LibraryPath={this.props.ContainingCollectionView?.props.LibraryPath ?? emptyPath} annotationsKey={this.annotationKey} setPreviewCursor={this.setPreviewCursor} PanelHeight={this.panelWidth} PanelWidth={this.panelHeight} - NativeHeight={returnZero} - NativeWidth={returnZero} dropAction={"alias"} VisibleHeight={this.visibleHeight} focus={this.props.focus} @@ -692,6 +734,8 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu removeDocument={this.removeDocument} moveDocument={this.moveDocument} addDocument={this.addDocument} + docFilters={this.props.docFilters} + docRangeFilters={this.props.docRangeFilters} CollectionView={undefined} ScreenToLocalTransform={this.overlayTransform} renderDepth={this.props.renderDepth + 1} @@ -700,7 +744,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu </div>; } @computed get pdfViewerDiv() { - return <div className={"pdfViewerDash-text" + ((!DocumentDecorations.Instance?.Interacting && (this.props.isSelected() || this.props.isChildActive())) ? "-selected" : "")} ref={this._viewer} />; + return <div className={"pdfViewerDash-text" + ((this.props.isSelected() || this.props.isChildActive()) ? "-selected" : "")} ref={this._viewer} />; } @computed get contentScaling() { return this.props.ContentScaling(); } @computed get standinViews() { @@ -729,6 +773,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu {this.pdfViewerDiv} {this.overlayLayer} {this.annotationLayer} + {this.overlayInfo} {this.standinViews} <PdfViewerMarquee isMarqueeing={this.marqueeing} width={this.marqueeWidth} height={this.marqueeHeight} x={this.marqueeX} y={this.marqueeY} /> </div >; diff --git a/src/client/views/presentationview/PresElementBox.scss b/src/client/views/presentationview/PresElementBox.scss index 6ee190b82..4642caeb2 100644 --- a/src/client/views/presentationview/PresElementBox.scss +++ b/src/client/views/presentationview/PresElementBox.scss @@ -114,6 +114,7 @@ $light-background: #ececec; } .presElementBox-name { + z-index: 300; align-self: center; font-size: 13px; font-family: Roboto; diff --git a/src/client/views/presentationview/PresElementBox.tsx b/src/client/views/presentationview/PresElementBox.tsx index a25a8ee33..aa44c13d1 100644 --- a/src/client/views/presentationview/PresElementBox.tsx +++ b/src/client/views/presentationview/PresElementBox.tsx @@ -1,5 +1,5 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, IReactionDisposer, reaction } from "mobx"; +import { action, computed, IReactionDisposer, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import { Doc, DataSym, DocListCast } from "../../../fields/Doc"; import { documentSchema } from '../../../fields/documentSchemas'; @@ -15,11 +15,13 @@ import { FieldView, FieldViewProps } from '../nodes/FieldView'; import "./PresElementBox.scss"; import React = require("react"); import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; -import { PresBox } from "../nodes/PresBox"; +import { PresBox, PresMovement } from "../nodes/PresBox"; import { DocumentType } from "../../documents/DocumentTypes"; import { Tooltip } from "@material-ui/core"; import { DragManager } from "../../util/DragManager"; import { CurrentUserUtils } from "../../util/CurrentUserUtils"; +import { undoBatch } from "../../util/UndoManager"; +import { EditableView } from "../EditableView"; export const presSchema = createSchema({ presentationTargetDoc: Doc, @@ -102,11 +104,11 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc bringToFront={returnFalse} opacity={returnOne} docFilters={this.props.docFilters} + docRangeFilters={this.props.docRangeFilters} + searchFilterDocs={this.props.searchFilterDocs} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} ContentScaling={returnOne} - NativeHeight={returnZero} - NativeWidth={returnZero} /> <div className="presElementBox-embeddedMask" /> </div>; @@ -114,20 +116,23 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc @computed get duration() { let durationInS: number; - if (this.targetDoc.presDuration) durationInS = NumCast(this.targetDoc.presDuration) / 1000; + if (this.rootDoc.type === DocumentType.AUDIO) { durationInS = NumCast(this.rootDoc.presEndTime) - NumCast(this.rootDoc.presStartTime); durationInS = Math.round(durationInS * 10) / 10; } + else if (this.rootDoc.presDuration) durationInS = NumCast(this.rootDoc.presDuration) / 1000; else durationInS = 2; - return "D: " + durationInS + "s"; + return this.rootDoc.presMovement === PresMovement.Jump ? (null) : "D: " + durationInS + "s"; } @computed get transition() { let transitionInS: number; - if (this.targetDoc.presTransition) transitionInS = NumCast(this.targetDoc.presTransition) / 1000; + if (this.rootDoc.presTransition) transitionInS = NumCast(this.rootDoc.presTransition) / 1000; else transitionInS = 0.5; return "M: " + transitionInS + "s"; } private _itemRef: React.RefObject<HTMLDivElement> = React.createRef(); private _dragRef: React.RefObject<HTMLDivElement> = React.createRef(); + private _titleRef: React.RefObject<EditableView> = React.createRef(); + @action headerDown = (e: React.PointerEvent<HTMLDivElement>) => { @@ -180,19 +185,19 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc onPointerTop = (e: React.PointerEvent<HTMLDivElement>) => { - if (DragManager.docsBeingDragged.length > 0) { + if (DragManager.docsBeingDragged.length > 1) { this._highlightTopRef.current!.style.borderTop = "solid 2px #5B9FDD"; } } onPointerBottom = (e: React.PointerEvent<HTMLDivElement>) => { - if (DragManager.docsBeingDragged.length > 0) { + if (DragManager.docsBeingDragged.length > 1) { this._highlightBottomRef.current!.style.borderBottom = "solid 2px #5B9FDD"; } } onPointerLeave = (e: React.PointerEvent<HTMLDivElement>) => { - if (DragManager.docsBeingDragged.length > 0) { + if (DragManager.docsBeingDragged.length > 1) { this._highlightBottomRef.current!.style.borderBottom = "0px"; this._highlightTopRef.current!.style.borderTop = "0px"; } @@ -200,14 +205,28 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc @action toggleProperties = () => { - if (CurrentUserUtils.propertiesWidth === 0) { - CurrentUserUtils.propertiesWidth = 250; + if (CurrentUserUtils.propertiesWidth < 5) { + action(() => (CurrentUserUtils.propertiesWidth = 250)); + } + } + + @undoBatch + removeItem = action((e: React.MouseEvent) => { + this.props.removeDocument?.(this.rootDoc); + if (PresBox.Instance._selectedArray.includes(this.rootDoc)) { + PresBox.Instance._selectedArray.splice(PresBox.Instance._selectedArray.indexOf(this.rootDoc), 1); } + e.stopPropagation(); + }); + + @action + onSetValue = (value: string) => { + this.rootDoc.title = value; + return true; } render() { const className = "presElementBox-item" + (PresBox.Instance._selectedArray.includes(this.rootDoc) ? " presElementBox-active" : ""); - const pbi = "presElementBox-interaction"; return !(this.rootDoc instanceof Doc) || this.targetDoc instanceof Promise ? (null) : ( <div className={className} key={this.props.Document[Id] + this.indexInPres} ref={this._itemRef} @@ -231,6 +250,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc } }} onDoubleClick={e => { + console.log('double click to open'); this.toggleProperties(); this.props.focus(this.rootDoc); PresBox.Instance._eleArray = []; @@ -246,17 +266,22 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc {`${this.indexInPres + 1}.`} </div> <div ref={this._dragRef} className="presElementBox-name" style={{ maxWidth: (PresBox.Instance.toolbarWidth - 70) }}> - {`${this.targetDoc?.title}`} + <EditableView + ref={this._titleRef} + contents={this.rootDoc.title} + GetValue={() => StrCast(this.rootDoc.title)} + SetValue={action((value: string) => { + this.onSetValue(value); + return true; + })} + /> </div> <Tooltip title={<><div className="dash-tooltip">{"Movement speed"}</div></>}><div className="presElementBox-time" style={{ display: PresBox.Instance.toolbarWidth > 300 ? "block" : "none" }}>{this.transition}</div></Tooltip> <Tooltip title={<><div className="dash-tooltip">{"Duration"}</div></>}><div className="presElementBox-time" style={{ display: PresBox.Instance.toolbarWidth > 300 ? "block" : "none" }}>{this.duration}</div></Tooltip> <Tooltip title={<><div className="dash-tooltip">{"Presentation pin view"}</div></>}><div className="presElementBox-time" style={{ fontWeight: 700, display: this.rootDoc.presPinView && PresBox.Instance.toolbarWidth > 300 ? "block" : "none" }}>V</div></Tooltip> <Tooltip title={<><div className="dash-tooltip">{"Remove from presentation"}</div></>}><div className="presElementBox-closeIcon" - onClick={e => { - this.props.removeDocument?.(this.rootDoc); - e.stopPropagation(); - }}> + onClick={this.removeItem}> <FontAwesomeIcon icon={"trash"} onPointerDown={e => e.stopPropagation()} /> </div></Tooltip> <Tooltip title={<><div className="dash-tooltip">{this.rootDoc.presExpandInlineButton ? "Minimize" : "Expand"}</div></>}><div className={"presElementBox-expand" + (this.rootDoc.presExpandInlineButton ? "-selected" : "")} onClick={e => { e.stopPropagation(); this.presExpandDocumentClick(); }}> diff --git a/src/client/views/search/IconBar.tsx b/src/client/views/search/IconBar.tsx index 9b7cf2fc6..f1dd106a7 100644 --- a/src/client/views/search/IconBar.tsx +++ b/src/client/views/search/IconBar.tsx @@ -1,28 +1,11 @@ -import * as React from 'react'; +import { action, observable } from 'mobx'; import { observer } from 'mobx-react'; -import { observable, action } from 'mobx'; +import * as React from 'react'; +import { DocumentType } from "../../documents/DocumentTypes"; // import "./SearchBox.scss"; import "./IconBar.scss"; -import "./IconButton.scss"; -import { faSearch, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote, faMusic, faLink, faChartBar, faGlobeAsia, faBan, faTimesCircle, faCheckCircle } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { library } from '@fortawesome/fontawesome-svg-core'; -import * as _ from "lodash"; import { IconButton } from './IconButton'; -import { DocumentType } from "../../documents/DocumentTypes"; - - -library.add(faSearch); -library.add(faObjectGroup); -library.add(faImage); -library.add(faStickyNote); -library.add(faFilePdf); -library.add(faFilm); -library.add(faMusic); -library.add(faLink); -library.add(faChartBar); -library.add(faGlobeAsia); -library.add(faBan); +import "./IconButton.scss"; export interface IconBarProps { setIcons: (icons: string[]) => void; diff --git a/src/client/views/search/IconButton.tsx b/src/client/views/search/IconButton.tsx index 52641c543..349690b20 100644 --- a/src/client/views/search/IconButton.tsx +++ b/src/client/views/search/IconButton.tsx @@ -1,30 +1,14 @@ -import * as React from 'react'; -import { observer } from 'mobx-react'; -import { observable, action, runInAction, IReactionDisposer, reaction } from 'mobx'; -import "./SearchBox.scss"; -import "./IconButton.scss"; -import { faSearch, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote, faMusic, faLink, faChartBar, faGlobeAsia, faBan, faVideo, faCaretDown } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { library, icon } from '@fortawesome/fontawesome-svg-core'; +import * as _ from "lodash"; +import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; import { DocumentType } from "../../documents/DocumentTypes"; import '../globalCssVariables.scss'; -import * as _ from "lodash"; import { IconBar } from './IconBar'; -import { props } from 'bluebird'; -import { Search } from '../../../server/Search'; -import { gravity } from 'sharp'; - -library.add(faSearch); -library.add(faObjectGroup); -library.add(faImage); -library.add(faStickyNote); -library.add(faFilePdf); -library.add(faFilm); -library.add(faMusic); -library.add(faLink); -library.add(faChartBar); -library.add(faGlobeAsia); -library.add(faBan); +import "./IconButton.scss"; +import "./SearchBox.scss"; +import { Font } from '@react-pdf/renderer'; interface IconButtonProps { type: string; @@ -47,59 +31,46 @@ export class IconButton extends React.Component<IconButtonProps>{ componentDidMount = () => { this._resetReaction = reaction( () => IconBar.Instance._resetClicked, - () => { + action(() => { if (IconBar.Instance._resetClicked) { - runInAction(() => { - this.reset(); - IconBar.Instance._reset++; - if (IconBar.Instance._reset === 9) { - IconBar.Instance._reset = 0; - IconBar.Instance._resetClicked = false; - } - }); + this._isSelected = false; + IconBar.Instance._reset++; + if (IconBar.Instance._reset === 9) { + IconBar.Instance._reset = 0; + IconBar.Instance._resetClicked = false; + } } - }, + }), ); + this._selectAllReaction = reaction( () => IconBar.Instance._selectAllClicked, - () => { + action(() => { if (IconBar.Instance._selectAllClicked) { - runInAction(() => { - this.select(); - IconBar.Instance._select++; - if (IconBar.Instance._select === 9) { - IconBar.Instance._select = 0; - IconBar.Instance._selectAllClicked = false; - } - }); + this._isSelected = true; + IconBar.Instance._select++; + if (IconBar.Instance._select === 9) { + IconBar.Instance._select = 0; + IconBar.Instance._selectAllClicked = false; + } } - }, + }), ); } @action.bound getIcon() { switch (this.props.type) { - case (DocumentType.NONE): - return faBan; - case (DocumentType.AUDIO): - return faMusic; - case (DocumentType.COL): - return faObjectGroup; - case (DocumentType.IMG): - return faImage; - case (DocumentType.LINK): - return faLink; - case (DocumentType.PDF): - return faFilePdf; - case (DocumentType.RTF): - return faStickyNote; - case (DocumentType.VID): - return faVideo; - case (DocumentType.WEB): - return faGlobeAsia; - default: - return faCaretDown; + case (DocumentType.NONE): return "ban"; + case (DocumentType.AUDIO): return "music"; + case (DocumentType.COL): return "object-group"; + case (DocumentType.IMG): return "image"; + case (DocumentType.LINK): return "link"; + case (DocumentType.PDF): return "file-pdf"; + case (DocumentType.RTF): return "sticky-note"; + case (DocumentType.VID): return "video"; + case (DocumentType.WEB): return "globe-asia"; + default: return "caret-down"; } } @@ -136,53 +107,16 @@ export class IconButton extends React.Component<IconButtonProps>{ //backgroundColor: "rgb(178, 206, 248)" //$darker-alt-accent }; - @action.bound - public reset() { this._isSelected = false; } - - @action.bound - public select() { this._isSelected = true; } - - @action - onMouseLeave = () => { this._hover = false; } - - @action - onMouseEnter = () => { this._hover = true; } - - getFA = () => { - switch (this.props.type) { - case (DocumentType.NONE): - return (<FontAwesomeIcon className="fontawesome-icon" icon={faBan} />); - case (DocumentType.AUDIO): - return (<FontAwesomeIcon className="fontawesome-icon" icon={faMusic} />); - case (DocumentType.COL): - return (<FontAwesomeIcon className="fontawesome-icon" icon={faObjectGroup} />); - case (DocumentType.IMG): - return (<FontAwesomeIcon className="fontawesome-icon" icon={faImage} />); - case (DocumentType.LINK): - return (<FontAwesomeIcon className="fontawesome-icon" icon={faLink} />); - case (DocumentType.PDF): - return (<FontAwesomeIcon className="fontawesome-icon" icon={faFilePdf} />); - case (DocumentType.RTF): - return (<FontAwesomeIcon className="fontawesome-icon" icon={faStickyNote} />); - case (DocumentType.VID): - return (<FontAwesomeIcon className="fontawesome-icon" icon={faVideo} />); - case (DocumentType.WEB): - return (<FontAwesomeIcon className="fontawesome-icon" icon={faGlobeAsia} />); - default: - return (<FontAwesomeIcon className="fontawesome-icon" icon={faCaretDown} />); - } - } - render() { return ( <div className="type-outer" id={this.props.type + "-filter"} - onMouseEnter={this.onMouseEnter} - onMouseLeave={this.onMouseLeave} + onMouseEnter={() => this._hover = true} + onMouseLeave={() => this._hover = false} onClick={this.onClick}> <div className="type-icon" id={this.props.type + "-icon"} style={this._hover ? this.hoverStyle : this._isSelected ? this.selected : this.notSelected} > - {this.getFA()} + <FontAwesomeIcon className="fontawesome-icon" icon={this.getIcon()} /> </div> {/* <div className="filter-description">{this.props.type}</div> */} </div> diff --git a/src/client/views/search/SearchBox.scss b/src/client/views/search/SearchBox.scss index 3f06ba7d3..13d4a6d5a 100644 --- a/src/client/views/search/SearchBox.scss +++ b/src/client/views/search/SearchBox.scss @@ -6,71 +6,132 @@ flex-direction: column; width: 100%; height: 100%; - position: absolute; + position: relative; font-size: 10px; line-height: 1; overflow-y: auto; overflow-x: visible; - background: lightgrey, -} + background: lightgrey; + overflow: visible; + z-index: 10000; -.searchBox-bar { - height: 32px; - display: flex; - justify-content: center; - align-items: center; - background-color: black; - .searchBox-barChild { + .searchBox-bar { + height: $searchpanel-height; + display: flex; + justify-content: center; + align-items: center; + background-color: black; - &.searchBox-collection { - flex: 0 1 auto; - margin-left: 2px; - margin-right: 2px - } + .searchBox-lozenges { + position: absolute; + left: 15; + display: flex; - &.searchBox-input { - margin:5px; - border-radius:20px; - border:black; - display: block; - width: 130px; - -webkit-transition: width 0.4s; - transition: width 0.4s; - align-self: stretch; - outline:none; + .searchBox-lozenge-user, + .searchBox-lozenge-dashboard, + .searchBox-lozenge { + background-color: #313131; + border-radius: 5px; + height: 18px; + padding: 4px; + box-shadow: lightgrey 0.15em 0.15em 0.1em; + margin: 2px; + margin-bottom: 4px; + border-top: dimgrey 1px solid; + border-left: dimgrey 1px solid; + display: flex; + .searchBox-logoff, + .searchBox-dashboards { + border-radius: 3px; + background: olivedrab; + color: white; + position: relative; + display: none; + margin-left: 3px; + padding-left: 2px; + padding-right: 2px; + padding-bottom: 11px; + cursor: default; + } + .searchBox-logoff { + background: red; + } + + .searchBox-dashSelect{ + background-color: black; + color: white; + font-size: 9; + margin-right: 6; + border-radius: 5px; + position: relative; + height: 15px; + transform: translate(0,-3px); + + &:hover { + cursor: pointer; + } + } + } + .searchBox-lozenge-user:hover { + .searchBox-logoff { + display:inline-block; + } + } + .searchBox-lozenge-dashboard:hover { + .searchBox-dashboards { + display:inline-block; + } + } } - - .searchBox-input:focus { - width: 500px; - outline:none; + .searchBox-query { + position: relative; + display: flex; + width: 450; } + .searchBox-barChild { - &.searchBox-filter { - align-self: stretch; - button{ - transform:none; + &.searchBox-collection { + flex: 0 1 auto; + margin-left: 2px; + margin-right: 2px } - button:hover{ - transform:none; + + &.searchBox-input { + margin:5px; + border-radius:20px; + border:black; + display: block; + width: 130px; + -webkit-transition: width 0.4s; + transition: width 0.4s; + align-self: stretch; + outline:none; + &:focus { + width: 500px; + outline:none; + } + } + &.searchBox-filter { + align-self: stretch; + button{ + transform:none; + &:hover { + transform: none; + } + } } - } - &.searchBox-submit { - margin-left: 2px; - margin-right: 2px - } + &.searchBox-submit { + margin-left: 2px; + margin-right: 2px + } - &.searchBox-close { - color: $light-color; - max-height: 32px; + &.searchBox-close { + color: $light-color; + max-height: $searchpanel-height; + } } - } -} - -.searchBox-quickFilter { - width: 100%; - height: 40px; - margin-top: 10px; + } } .searchBox-results { @@ -91,241 +152,4 @@ text-align: left; font-weight: bold; } -} - -.filter-form { - position: relative; - background: #121721; - flex-direction: column; - transform-origin: top; - transition: height 0.3s ease, display 0.6s ease, overflow 0.6s ease; - height:0px; - overflow:hidden; - - - .filter-header { - //display: flex; - position: relative; - //flex-wrap:wrap; - right: 1px; - color: grey; - //flex-direction: row-reverse; - transform-origin: top; - //justify-content: space-evenly; - margin-bottom: 5px; - overflow:hidden; - transition:height 0.3s ease-out; - - - - .filter-item { - position: relative; - border:1px solid grey; - border-radius: 16px; - - } - } - - .filter-body { - position: relative; - right: 1px; - color: grey; - transform-origin: top; - border-top: 0px; - - overflow:hidden; - transition:height 0.3s ease-out; - height:0px; - - } - .filter-key { - position: relative; - right: 1px; - color: grey; - transform-origin: top; - border-top: 0px; - overflow:hidden; - transition:height 0.3s ease-out; - height:0px; - - // .filter-keybar { - // display: flex; - // flex-wrap: wrap; - // justify-content: space-evenly; - // height: auto; - // width: 100%; - // flex-direction: row-reverse; - // margin-top:5px; - - // .filter-item { - // position: relative; - // border:1px solid grey; - // border-radius: 16px; - // } - // } - } -} - -// .top-filter-header { - -// #header { -// text-transform: uppercase; -// letter-spacing: 2px; -// font-size: 13; -// width: 80%; -// } - -// .close-icon { -// width: 20%; -// opacity: .6; -// position: relative; -// display: block; - -// .line { -// display: block; -// background: $alt-accent; -// width: 20; -// height: 3; -// position: absolute; -// right: 0; -// border-radius: ($height-line / 2); - -// &.line-1 { -// transform: rotate(45deg); -// top: 45%; -// } - -// &.line-2 { -// transform: rotate(-45deg); -// top: 45%; -// } -// } -// } - -// .close-icon:hover { -// opacity: 1; -// } - -// } - -// .filter-options { - -// .filter-div { -// margin-top: 10px; -// margin-bottom: 10px; -// display: inline-block; -// width: 100%; -// border-color: rgba(178, 206, 248, .2); // $darker-alt-accent -// border-top-style: solid; - -// .filter-header { -// display: flex; -// align-items: center; -// margin-bottom: 10px; -// letter-spacing: 2px; - -// .filter-title { -// font-size: 13; -// text-transform: uppercase; -// margin-top: 10px; -// margin-bottom: 10px; -// -webkit-transition: all 0.2s ease-in-out; -// -moz-transition: all 0.2s ease-in-out; -// -o-transition: all 0.2s ease-in-out; -// transition: all 0.2s ease-in-out; -// } -// } - -// .filter-header:hover .filter-title { -// transform: scale(1.05); -// } - -// .filter-panel { -// max-height: 0px; -// width: 100%; -// overflow: hidden; -// opacity: 0; -// transform-origin: top; -// -webkit-transition: all 0.2s ease-in-out; -// -moz-transition: all 0.2s ease-in-out; -// -o-transition: all 0.2s ease-in-out; -// transition: all 0.2s ease-in-out; -// text-align: center; -// } -// } -// } - -// .filter-buttons { -// border-color: rgba(178, 206, 248, .2); // $darker-alt-accent -// border-top-style: solid; -// padding-top: 10px; -// } - - -.active-filters { - display: flex; - flex-direction: row-reverse; - justify-content: flex-end; - width: 100%; - margin-right: 30px; - position: relative; - - .active-icon { - max-width: 40px; - flex: initial; - - &.icon { - width: 40px; - text-align: center; - margin-bottom: 5px; - position: absolute; - } - - &.container { - display: flex; - flex-direction: column; - width: 40px; - } - - &.description { - text-align: center; - top: 40px; - position: absolute; - width: 40px; - font-size: 9px; - opacity: 0; - -webkit-transition: all 0.2s ease-in-out; - -moz-transition: all 0.2s ease-in-out; - -o-transition: all 0.2s ease-in-out; - transition: all 0.2s ease-in-out; - } - - &.icon:hover+.description { - opacity: 1; - } - } - - .col-icon { - height: 35px; - margin-left: 5px; - width: 35px; - background-color: black; - color: white; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - - .save-filter, - .reset-filter, - .all-filter { - background-color: gray; - } - - .save-filter:hover, - .reset-filter:hover, - .all-filter:hover { - background-color: $darker-alt-accent; - } - } }
\ No newline at end of file diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index 847bda137..b381bbfa9 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -1,184 +1,99 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Tooltip } from '@material-ui/core'; -import { action, computed, observable, runInAction, reaction, IReactionDisposer } from 'mobx'; +import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import * as rp from 'request-promise'; -import { Doc, DocListCast, Opt } from '../../../fields/Doc'; +import { Doc, DocListCast, Field, Opt } from '../../../fields/Doc'; import { documentSchema } from "../../../fields/documentSchemas"; -import { Id } from '../../../fields/FieldSymbols'; +import { Copy, Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { createSchema, listSpec, makeInterface } from '../../../fields/Schema'; import { SchemaHeaderField } from '../../../fields/SchemaHeaderField'; import { Cast, NumCast, StrCast } from '../../../fields/Types'; -import { returnFalse, Utils } from '../../../Utils'; +import { emptyFunction, returnFalse, returnZero, setupMoveUpEvents, Utils } from '../../../Utils'; import { Docs } from '../../documents/Documents'; import { DocumentType } from "../../documents/DocumentTypes"; -import { CurrentUserUtils } from '../../util/CurrentUserUtils'; +import { CurrentUserUtils } from "../../util/CurrentUserUtils"; import { SetupDrag } from '../../util/DragManager'; import { SearchUtil } from '../../util/SearchUtil'; -import { SelectionManager } from '../../util/SelectionManager'; import { Transform } from '../../util/Transform'; -import { CollectionView, CollectionViewType } from '../collections/CollectionView'; +import { CollectionDockingView } from "../collections/CollectionDockingView"; +import { CollectionSchemaView, ColumnType } from "../collections/CollectionSchemaView"; +import { CollectionViewType } from '../collections/CollectionView'; import { ViewBoxBaseComponent } from "../DocComponent"; -import { DocumentView } from '../nodes/DocumentView'; import { FieldView, FieldViewProps } from '../nodes/FieldView'; import "./SearchBox.scss"; -import { ColumnType } from "../collections/CollectionSchemaView"; +import { undoBatch } from "../../util/UndoManager"; -export const searchSchema = createSchema({ - id: "string", - Document: Doc, - searchQuery: "string", -}); - -export enum Keys { - TITLE = "title", - AUTHOR = "author", - DATA = "data", - TEXT = "text" -} +export const searchSchema = createSchema({ Document: Doc }); type SearchBoxDocument = makeInterface<[typeof documentSchema, typeof searchSchema]>; const SearchBoxDocument = makeInterface(documentSchema, searchSchema); -//React.Component<SearchProps> @observer export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDocument>(SearchBoxDocument) { + public static LayoutString(fieldKey: string) { return FieldView.LayoutString(SearchBox, fieldKey); } + public static Instance: SearchBox; - get _searchString() { return this.layoutDoc.searchQuery; } - @computed set _searchString(value) { this.layoutDoc.searchQuery = (value); } - @observable private _resultsOpen: boolean = false; - @observable _searchbarOpen: boolean = false; - @observable private _results: [Doc, string[], string[]][] = []; - @observable private _openNoResults: boolean = false; - @observable private _visibleElements: JSX.Element[] = []; - @observable private _visibleDocuments: Doc[] = []; - - static NUM_SEARCH_RESULTS_PER_PAGE = 25; - - private _resultsSet = new Map<Doc, number>(); - private _resultsRef = React.createRef<HTMLDivElement>(); - public inputRef = React.createRef<HTMLInputElement>(); - - private _isSearch: ("search" | "placeholder" | undefined)[] = []; - private _isSorted: ("sorted" | "placeholder" | undefined)[] = []; - + private _allIcons: string[] = [DocumentType.INK, DocumentType.AUDIO, DocumentType.COL, DocumentType.IMG, DocumentType.LINK, DocumentType.PDF, DocumentType.RTF, DocumentType.VID, DocumentType.WEB]; + private _numResultsPerPage = 500; private _numTotalResults = -1; private _endIndex = -1; - - static Instance: SearchBox; - + private _lockPromise?: Promise<void>; + private _resultsSet = new Map<Doc, number>(); + private _inputRef = React.createRef<HTMLInputElement>(); private _maxSearchIndex: number = 0; private _curRequest?: Promise<any> = undefined; - public static LayoutString(fieldKey: string) { return FieldView.LayoutString(SearchBox, fieldKey); } + private _disposers: { [name: string]: IReactionDisposer } = {}; + private _blockedTypes = [DocumentType.PRESELEMENT, DocumentType.KVP, DocumentType.FILTER, DocumentType.DOCHOLDER, DocumentType.SEARCH, DocumentType.SEARCHITEM, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING]; - private new_buckets: { [characterName: string]: number } = {}; - //if true, any keywords can be used. if false, all keywords are required. - //this also serves as an indicator if the word status filter is applied - @observable private _basicWordStatus: boolean = false; - @observable private _nodeStatus: boolean = false; - @observable private _keyStatus: boolean = false; + private docsforfilter: Doc[] | undefined = []; + private realTotalResults: number = 0; + private newsearchstring = ""; + private collectionRef = React.createRef<HTMLDivElement>(); - @observable private newAssign: boolean = true; + @observable _icons: string[] = this._allIcons; + @observable _results: [Doc, string[], string[]][] = []; + @observable _visibleElements: JSX.Element[] = []; + @observable _visibleDocuments: Doc[] = []; + @observable _deletedDocsStatus: boolean = false; + @observable _onlyAliases: boolean = true; + @observable _searchbarOpen = false; + @observable _searchFullDB = "DB"; + @observable _noResults = ""; + @observable _pageStart = 0; + @observable open = false; + @observable children = 0; + @computed get filter() { return this._results?.length && (this.currentSelectedCollection?.props.Document._searchFilterDocs || this.currentSelectedCollection?.props.Document._docFilters); } constructor(props: any) { super(props); SearchBox.Instance = this; - this.resultsScrolled = this.resultsScrolled.bind(this); - } - @observable setupButtons = false; - private _disposers: { [name: string]: IReactionDisposer } = {}; - - componentDidMount = () => { - this._disposers.filters = reaction(() => Cast(this.props.Document._docFilters, listSpec("string")), // if a link is deleted, then remove all hyperlinks that reference it from the text's marks - newFilters => { - if (this.searchFullDB) { - runInAction(() => this._pageStart = 0); - this.submitSearch(); - // newFilters?.forEach(f => { - // console.log(f); - // }) - } - }); - if (this.setupButtons === false) { - runInAction(() => this.setupButtons = true); + componentDidMount = action(() => { + if (this._inputRef.current) { + this._inputRef.current.focus(); } - if (this.inputRef.current) { - this.inputRef.current.focus(); - runInAction(() => { this._searchbarOpen = true; }); - } - if (this.rootDoc.searchQuery && this.newAssign) { - const sq = this.rootDoc.searchQuery; - runInAction(() => { - - // this._deletedDocsStatus=this.props.filterQuery!.deletedDocsStatus; - // this._authorFieldStatus=this.props.filterQuery!.authorFieldStatus - // this._titleFieldStatus=this.props.filterQuery!.titleFieldStatus; - // this._basicWordStatus=this.props.filterQuery!.basicWordStatus; - // this._icons=this.props.filterQuery!.icons; - this.newAssign = false; - }); - runInAction(() => { - this.layoutDoc._searchString = StrCast(sq); - this.submitSearch(); - }); - } - } + this._disposers.filters = reaction(() => this.props.Document._docFilters, + (filters: any) => this.setSearchFilter(this.currentSelectedCollection, !this.filter ? undefined : this.docsforfilter)); + }); componentWillUnmount() { Object.values(this._disposers).forEach(disposer => disposer?.()); } + @computed get currentSelectedCollection() { return CollectionDockingView.Instance; } - @action - getViews = (doc: Doc) => SearchUtil.GetViewsOfDocument(doc) - - - @observable newsearchstring: string = ""; - @action.bound - onChange(e: React.ChangeEvent<HTMLInputElement>) { - this.layoutDoc._searchString = e.target.value; + onChange = action((e: React.ChangeEvent<HTMLInputElement>) => { this.newsearchstring = e.target.value; if (e.target.value === "") { - if (this.currentSelectedCollection !== undefined) { - let newarray: Doc[] = []; - let docs: Doc[] = []; - docs = DocListCast(this.currentSelectedCollection.dataDoc[Doc.LayoutFieldKey(this.currentSelectedCollection.dataDoc)]); - while (docs.length > 0) { - newarray = []; - docs.forEach((d) => { - if (d.data !== undefined) { - d._searchDocs = new List<Doc>(); - d._docFilters = new List(); - const newdocs = DocListCast(d.data); - newdocs.forEach((newdoc) => { - newarray.push(newdoc); - }); - } - }); - docs = newarray; - } - - this.currentSelectedCollection.props.Document._searchDocs = new List<Doc>([]); - this.currentSelectedCollection.props.Document._docFilters = new List(); - this.props.Document.selectedDoc = undefined; - } - this._results.forEach(result => { - Doc.UnBrushDoc(result[0]); - result[0].searchMatch = undefined; - }); - - if (this.currentSelectedCollection !== undefined) { - this.currentSelectedCollection.props.Document._searchDocs = new List<Doc>([]); - this.currentSelectedCollection = undefined; - this.props.Document.selectedDoc = undefined; + console.log("Reset start"); + this.docsforfilter = undefined; + this.setSearchFilter(this.currentSelectedCollection, undefined); + this.resetSearch(false); - } - runInAction(() => { this.open = false; }); - this._openNoResults = false; + this.open = false; this._results = []; this._resultsSet.clear(); this._visibleElements = []; @@ -187,62 +102,16 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc this._curRequest = undefined; this._maxSearchIndex = 0; } - } + }); - enter = (e: React.KeyboardEvent) => { - if (e.key === "Enter") { + enter = action((e: React.KeyboardEvent | undefined) => { + if (!e || e.key === "Enter") { this.layoutDoc._searchString = this.newsearchstring; - runInAction(() => this._pageStart = 0); - - if (StrCast(this.layoutDoc._searchString) !== "" || !this.searchFullDB) { - runInAction(() => this.open = true); - } - else { - runInAction(() => this.open = false); - - } + this._pageStart = 0; + this.open = StrCast(this.layoutDoc._searchString) !== "" || this._searchFullDB !== "DB"; this.submitSearch(); } - } - - @observable open: boolean = false; - - - public static async convertDataUri(imageUri: string, returnedFilename: string) { - try { - const posting = Utils.prepend("/uploadURI"); - const returnedUri = await rp.post(posting, { - body: { - uri: imageUri, - name: returnedFilename - }, - json: true, - }); - return returnedUri; - - } catch (e) { - console.log("SearchBox:" + e); - } - } - - public _allIcons: string[] = [DocumentType.INK, DocumentType.AUDIO, DocumentType.COL, DocumentType.IMG, DocumentType.LINK, DocumentType.PDF, DocumentType.RTF, DocumentType.VID, DocumentType.WEB]; - //if true, any keywords can be used. if false, all keywords are required. - //this also serves as an indicator if the word status filter is applied - @observable private _filterOpen: boolean = false; - //if icons = all icons, then no icon filter is applied - // get _icons() { return this.props.searchFileTypes; } - // set _icons(value) { - // this.props.setSearchFileTypes(value); - // } - @observable _icons: string[] = this._allIcons; - //if all of these are true, no key filter is applied - @observable private _titleFieldStatus: boolean = true; - @observable private _authorFieldStatus: boolean = true; - //this also serves as an indicator if the collection status filter is applied - @observable public _deletedDocsStatus: boolean = false; - @observable public _onlyAliases: boolean = true; - @observable private _collectionStatus = false; - + }); getFinalQuery(query: string): string { //alters the query so it looks in the correct fields @@ -250,8 +119,6 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc //TODO: data const initialfilters = Cast(this.props.Document._docFilters, listSpec("string"), []); - const type: string[] = []; - const filters: string[] = []; for (let i = 0; i < initialfilters.length; i = i + 3) { @@ -279,9 +146,7 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc const mod = "_t:"; const newWords: string[] = []; const oldWords = values[0].split(" "); - oldWords.forEach((word, i) => { - i === 0 ? newWords.push(key + mod + "\"" + word + "\"") : newWords.push("AND " + key + mod + "\"" + word + "\""); - }); + oldWords.forEach((word, i) => i === 0 ? newWords.push(key + mod + "\"" + word + "\"") : newWords.push("AND " + key + mod + "\"" + word + "\"")); query = `(${query}) AND (${newWords.join(" ")})`; } else { @@ -289,194 +154,74 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc const mod = "_t:"; const newWords: string[] = []; const oldWords = values[i].split(" "); - oldWords.forEach((word, i) => { - i === 0 ? newWords.push(key + mod + "\"" + word + "\"") : newWords.push("AND " + key + mod + "\"" + word + "\""); - }); + oldWords.forEach((word, i) => i === 0 ? newWords.push(key + mod + "\"" + word + "\"") : newWords.push("AND " + key + mod + "\"" + word + "\"")); const v = "(" + newWords.join(" ") + ")"; if (i === 0) { - query = `(${query}) AND (${v}`; - if (values.length === 1) { - query = query + ")"; - } - } - else if (i === values.length - 1) { - query = query + " OR " + v + ")"; - } - else { - query = query + " OR " + v; + query = `(${query}) AND (${v}` + (values.length === 1 ? ")" : ""); } + else query = query + " OR " + v + (i === values.length - 1 ? ")" : ""); } } - } - - // let limit = typepos.length - // typepos.forEach(i => { - // if (i === 0) { - // if (i + 1 === limit) { - // query = query + " && " + filters[i] + "_t:" + filters; - // } - // else if (filters[i] === filters[i + 3]) { - // query = query + " && (" + filters[i] + "_t:" + filters; - // } - // else { - // query = query + " && " + filters[i] + "_t:" + filters; - // } - - // } - // else if (i + 3 > filters.length) { - - // } - // else { - - // } - - // }); - - // query = this.applyBasicFieldFilters(query); - - - - query = query.replace(/-\s+/g, ''); - // query = query.replace(/-/g, ""); - return query; - } - - basicRequireWords(query: string): string { - return query.split(" ").join(" + ").replace(/ + /, ""); + return query.replace(/-\s+/g, ''); } @action filterDocsByType(docs: Doc[]) { const finalDocs: Doc[] = []; - const blockedTypes: string[] = [DocumentType.PRESELEMENT, DocumentType.DOCHOLDER, DocumentType.SEARCH, DocumentType.SEARCHITEM, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING]; docs.forEach(doc => { - const layoutresult = Cast(doc.type, "string"); - if (layoutresult && !blockedTypes.includes(layoutresult)) { - if (layoutresult && this._icons.includes(layoutresult)) { - finalDocs.push(doc); - } + const layoutresult = StrCast(doc.type, "string") as DocumentType; + if (layoutresult && !this._blockedTypes.includes(layoutresult) && this._icons.includes(layoutresult)) { + finalDocs.push(doc); } }); return finalDocs; } - addCollectionFilter(query: string): string { - const collections: Doc[] = this.getCurCollections(); - const oldWords = query.split(" "); - - const collectionString: string[] = []; - collections.forEach(doc => { - const proto = doc.proto; - const protoId = (proto || doc)[Id]; - const colString: string = "{!join from=data_l to=id}id:" + protoId + " "; - collectionString.push(colString); - }); - - let finalColString = collectionString.join(" "); - finalColString = finalColString.trim(); - return "+(" + finalColString + ")" + query; - } - - get filterTypes() { - return this._icons.length === this._allIcons.length ? undefined : this._icons; - } - - //TODO: basically all of this - //gets all of the collections of all the docviews that are selected - //if a collection is the only thing selected, search only in that collection (not its container) - getCurCollections(): Doc[] { - const selectedDocs: DocumentView[] = SelectionManager.SelectedDocuments(); - const collections: Doc[] = []; - selectedDocs.forEach(async element => { - const layout: string = StrCast(element.props.Document.layout); - //checks if selected view (element) is a collection. if it is, adds to list to search through - if (layout.indexOf("Collection") > -1) { - //makes sure collections aren't added more than once - if (!collections.includes(element.props.Document)) { - collections.push(element.props.Document); - } - } - //makes sure collections aren't added more than once - if (element.props.ContainingCollectionDoc && !collections.includes(element.props.ContainingCollectionDoc)) { - collections.push(element.props.ContainingCollectionDoc); - } - }); - - return collections; + static foreachRecursiveDoc(docs: Doc[], func: (doc: Doc) => void) { + let newarray: Doc[] = []; + while (docs.length > 0) { + newarray = []; + docs.filter(d => d).forEach(d => { + const fieldKey = Doc.LayoutFieldKey(d); + const annos = !Field.toString(Doc.LayoutField(d) as Field).includes("CollectionView"); + const data = d[annos ? fieldKey + "-annotations" : fieldKey]; + data && newarray.push(...DocListCast(data)); + func(d); + }); + docs = newarray; + } } - - currentSelectedCollection: DocumentView | undefined = undefined; - docsforfilter: Doc[] = []; - + @action searchCollection(query: string) { - const selectedCollection: DocumentView = SelectionManager.SelectedDocuments()[0]; + const selectedCollection = this.currentSelectedCollection;//SelectionManager.SelectedDocuments()[0]; query = query.toLowerCase(); if (selectedCollection !== undefined) { - this.currentSelectedCollection = selectedCollection; - if (this.filter === true) { - this.props.Document.selectedDoc = selectedCollection.props.Document; - } - let docs = DocListCast(selectedCollection.dataDoc[Doc.LayoutFieldKey(selectedCollection.dataDoc)]); + // this._currentSelectedCollection = selectedCollection; + const docs = DocListCast(selectedCollection.dataDoc[Doc.LayoutFieldKey(selectedCollection.dataDoc)]); const found: [Doc, string[], string[]][] = []; - const docsforFilter: Doc[] = []; - let newarray: Doc[] = []; + SearchBox.foreachRecursiveDoc(docs, (doc: Doc) => { + const hlights = new Set<string>(); + SearchBox.documentKeys(doc).forEach(key => Field.toString(doc[key] as Field).toLowerCase().includes(query) && hlights.add(key)); + Array.from(hlights.keys()).length > 0 && found.push([doc, Array.from(hlights.keys()), []]); + }); - while (docs.length > 0) { - newarray = []; - docs.forEach((d) => { - d ? console.log(Cast(d.context, Doc)) : null; - if (d.data !== undefined) { - newarray.push(...DocListCast(d.data)); - } - const hlights: string[] = []; - const protos = Doc.GetAllPrototypes(d); - protos.forEach(proto => { - Object.keys(proto).forEach(key => { - if (StrCast(d[key]).toLowerCase().includes(query) && !hlights.includes(key)) { - hlights.push(key); - } - }); - }); - if (hlights.length > 0) { - found.push([d, hlights, []]); - docsforFilter.push(d); - } - }); - docs = newarray; - } this._results = found; - this.docsforfilter = docsforFilter; - if (this.filter === true) { - selectedCollection.props.Document._searchDocs = new List<Doc>(docsforFilter); - docs = DocListCast(selectedCollection.dataDoc[Doc.LayoutFieldKey(selectedCollection.dataDoc)]); - while (docs.length > 0) { - newarray = []; - docs.forEach((d) => { - if (d.data !== undefined) { - d._searchDocs = new List<Doc>(docsforFilter); - const newdocs = DocListCast(d.data); - newdocs.forEach((newdoc) => { - newarray.push(newdoc); - }); - } - }); - docs = newarray; - } - } + this.docsforfilter = this._results.map(r => r[0]); + this.setSearchFilter(selectedCollection, this.filter && found.length ? this.docsforfilter : undefined); this._numTotalResults = found.length; + this.realTotalResults = found.length; } else { - this.noresults = "No collection selected :("; + this._noResults = "No collection selected :("; } } - - documentKeys(doc: Doc) { + static documentKeys(doc: Doc) { const keys: { [key: string]: boolean } = {}; // bcz: ugh. this is untracked since otherwise a large collection of documents will blast the server for all their fields. // then as each document's fields come back, we update the documents _proxies. Each time we do this, the whole schema will be @@ -484,139 +229,52 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc // then by the time the options button is clicked, all of the fields should be in place. If a new field is added while this menu // is displayed (unlikely) it won't show up until something else changes. //TODO Types - Doc.GetAllPrototypes(doc).map - (proto => Object.keys(proto).forEach(key => keys[key] = false)); + Doc.GetAllPrototypes(doc).map(proto => Object.keys(proto).forEach(key => keys[key] = false)); return Array.from(Object.keys(keys)); } - applyBasicFieldFilters(query: string) { - let finalQuery = ""; - - finalQuery = finalQuery + this.basicFieldFilters(query, Keys.TITLE); - finalQuery = finalQuery + this.basicFieldFilters(query, Keys.AUTHOR); - - if (this._deletedDocsStatus) { - finalQuery = finalQuery + this.basicFieldFilters(query, Keys.TEXT); - } - return finalQuery; - } - - basicFieldFilters(query: string, type: string): string { - let mod = ""; - switch (type) { - case Keys.AUTHOR: mod = " author_t:"; break; - case Keys.TITLE: mod = " title_t:"; break; - case Keys.TEXT: mod = " text_t:"; break; - } - - const newWords: string[] = []; - const oldWords = query.split(" "); - oldWords.forEach(word => newWords.push(mod + word)); - - query = newWords.join(" "); - - return query; - } - - get fieldFiltersApplied() { return !(this._authorFieldStatus && this._titleFieldStatus); } - @action - submitSearch = async (reset?: boolean) => { - if (this.currentSelectedCollection !== undefined) { - let newarray: Doc[] = []; - let docs: Doc[] = []; - docs = DocListCast(this.currentSelectedCollection.dataDoc[Doc.LayoutFieldKey(this.currentSelectedCollection.dataDoc)]); - while (docs.length > 0) { - newarray = []; - docs.forEach((d) => { - if (d.data !== undefined) { - d._searchDocs = new List<Doc>(); - //d._docFilters = new List(); - const newdocs = DocListCast(d.data); - newdocs.forEach((newdoc) => { - newarray.push(newdoc); - }); - } - }); - docs = newarray; - } + submitSearch = async () => { + this.resetSearch(false); - this.currentSelectedCollection.props.Document._searchDocs = new List<Doc>([]); - this.currentSelectedCollection.props.Document._docFilters = new List(); - this.props.Document.selectedDoc = undefined; - } - if (reset) { - this.layoutDoc._searchString = ""; - } //this.props.Document._docFilters = new List(); - this.noresults = ""; + this._noResults = ""; this.dataDoc[this.fieldKey] = new List<Doc>([]); - this.headercount = 0; this.children = 0; - this.buckets = []; - this.new_buckets = {}; let query = StrCast(this.layoutDoc._searchString); Doc.SetSearchQuery(query); - this.searchFullDB ? query = this.getFinalQuery(query) : console.log("local"); - this._results.forEach(result => { - Doc.UnBrushDoc(result[0]); - result[0].searchMatch = undefined; - }); + this._searchFullDB && (query = this.getFinalQuery(query)); this._results = []; this._resultsSet.clear(); - this._isSearch = []; - this._isSorted = []; this._visibleElements = []; this._visibleDocuments = []; - if (StrCast(this.props.Document.searchQuery)) { - if (this._timeout) { clearTimeout(this._timeout); this._timeout = undefined; } - this._timeout = setTimeout(() => { - console.log("Resubmitting search"); - }, 60000); - } - if (query !== "") { + if (query || this._searchFullDB === "My Stuff") { this._endIndex = 12; this._maxSearchIndex = 0; this._numTotalResults = -1; - this.searchFullDB ? await this.getResults(query) : this.searchCollection(query); + this._searchFullDB ? await this.searchDatabase(query) : this.searchCollection(query); runInAction(() => { - this._resultsOpen = true; - this._searchbarOpen = true; - this._openNoResults = true; + this.open = this._searchbarOpen = true; this.resultsScrolled(); - }); } } - @observable searchFullDB = true; - - @observable _timeout: any = undefined; - - @observable firststring: string = ""; - @observable secondstring: string = ""; - - @observable bucketcount: number[] = []; - @observable buckets: Doc[] | undefined; - getAllResults = async (query: string) => { return SearchUtil.Search(query, true, { fq: this.filterQuery, start: 0, rows: 10000000 }); } private get filterQuery() { - const types = ["preselement", "docholder", "search", "searchitem", "fonticonbox"]; // this.filterTypes; const baseExpr = "NOT system_b:true"; - const includeDeleted = this.getDataStatus() ? "" : " NOT deleted_b:true"; - const typeExpr = this._onlyAliases ? "NOT {!join from=id to=proto_i}type_t:*" : `(type_t:* OR {!join from=id to=proto_i}type_t:*) ${types.map(type => `NOT ({!join from=id to=proto_i}type_t:${type}) AND NOT type_t:${type}`).join(" AND ")}`; + const authorExpr = this._searchFullDB === "My Stuff" ? ` author_t:${Doc.CurrentUserEmail}` : undefined; + const includeDeleted = this._deletedDocsStatus ? "" : " NOT deleted_b:true"; + const typeExpr = this._onlyAliases ? "NOT {!join from=id to=proto_i}type_t:*" : `(type_t:* OR {!join from=id to=proto_i}type_t:*) ${this._blockedTypes.map(type => `NOT ({!join from=id to=proto_i}type_t:${type}) AND NOT type_t:${type}`).join(" AND ")}`; // fq: type_t:collection OR {!join from=id to=proto_i}type_t:collection q:text_t:hello - const query = [baseExpr, includeDeleted, typeExpr].join(" AND ").replace(/AND $/, ""); - return query; + return [baseExpr, authorExpr, includeDeleted, typeExpr].filter(q => q).join(" AND ").replace(/AND $/, ""); } - getDataStatus() { return this._deletedDocsStatus; } - @computed get primarySort() { const suffixMap = (type: ColumnType) => { switch (type) { @@ -625,19 +283,16 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc case ColumnType.Boolean: return "_b"; case ColumnType.Number: return "_n"; } - } + }; const headers = Cast(this.props.Document._schemaHeaders, listSpec(SchemaHeaderField), []); return headers.reduce((p: Opt<string>, header: SchemaHeaderField) => p || (header.desc !== undefined && suffixMap(header.type) ? (header.heading + suffixMap(header.type) + (header.desc ? " desc" : " asc")) : undefined), undefined); } - private NumResults = 50; - private lockPromise?: Promise<void>; - getResults = async (query: string) => { - if (this.lockPromise) { - await this.lockPromise; - } - this.lockPromise = new Promise(async res => { + + searchDatabase = async (query: string) => { + this._lockPromise && (await this._lockPromise); + this._lockPromise = new Promise(async res => { while (this._results.length <= this._endIndex && (this._numTotalResults === -1 || this._maxSearchIndex < this._numTotalResults)) { - this._curRequest = SearchUtil.Search(query, true, { onlyAliases: true, allowAliases: true, sort: this.primarySort, fq: this.filterQuery, start: 0, rows: this.NumResults, hl: true, "hl.fl": "*", }).then(action(async (res: SearchUtil.DocSearchResult) => { + this._curRequest = SearchUtil.Search(query, true, { onlyAliases: true, allowAliases: true, /*sort: this.primarySort,*/ fq: this.filterQuery, start: 0, rows: this._numResultsPerPage, hl: "on", "hl.fl": "*", }).then(action(async (res: SearchUtil.DocSearchResult) => { // happens at the beginning this.realTotalResults = res.numFound <= 0 ? 0 : res.numFound; if (res.numFound !== this._numTotalResults && this._numTotalResults === -1) { @@ -652,50 +307,44 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc docs.forEach((doc, index) => highlights[doc[Id]] = highlightList[index]); const filteredDocs = this.filterDocsByType(docs); - runInAction(() => { - filteredDocs.forEach((doc, i) => { - const index = this._resultsSet.get(doc); - const highlight = highlights[doc[Id]]; - const line = lines.get(doc[Id]) || []; - const hlights = highlight ? Object.keys(highlight).map(key => key.substring(0, key.length - 2)).filter(k => k) : []; - // if (this.findCommonElements(hlights)) { - // } - if (index === undefined) { - this._resultsSet.set(doc, this._results.length); - this._results.push([doc, hlights, line]); - } else { - this._results[index][1].push(...hlights); - this._results[index][2].push(...line); - } + runInAction(() => filteredDocs.forEach((doc, i) => { + const index = this._resultsSet.get(doc); + const highlight = highlights[doc[Id]]; + const line = lines.get(doc[Id]) || []; + const hlights = highlight ? Object.keys(highlight).map(key => key.substring(0, key.length - 2)).filter(k => k) : []; + // if (this.findCommonElements(hlights)) { + // } + if (index === undefined) { + this._resultsSet.set(doc, this._results.length); + this._results.push([doc, hlights, line]); + } else { + this._results[index][1].push(...hlights); + this._results[index][2].push(...line); + } - }); - }); + })); this._curRequest = undefined; })); - this._maxSearchIndex += this.NumResults; + this._maxSearchIndex += this._numResultsPerPage; await this._curRequest; } this.resultsScrolled(); + + const selectedCollection = this.currentSelectedCollection;//SelectionManager.SelectedDocuments()[0]; + this.docsforfilter = this._results.map(r => r[0]); + this.setSearchFilter(selectedCollection, this.filter ? this.docsforfilter : undefined); res(); }); - return this.lockPromise; + return this._lockPromise; } - @observable noresults = ""; - collectionRef = React.createRef<HTMLSpanElement>(); + startDragCollection = async () => { const res = await this.getAllResults(this.getFinalQuery(StrCast(this.layoutDoc._searchString))); const filtered = this.filterDocsByType(res.docs); - const docs = filtered.map(doc => { - const isProto = Doc.GetT(doc, "isPrototype", "boolean", true); - if (isProto) { - return Doc.MakeDelegate(doc); - } else { - return Doc.MakeAlias(doc); - } - }); + const docs = filtered.map(doc => Doc.GetT(doc, "isPrototype", "boolean", true) ? Doc.MakeDelegate(doc) : Doc.MakeAlias(doc)); let x = 0; let y = 0; for (const doc of docs.map(d => Doc.Layout(d))) { @@ -719,28 +368,26 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc y += 300; } } - return Docs.Create.SchemaDocument(Cast(this.props.Document._schemaHeaders, listSpec(SchemaHeaderField), []), DocListCast(this.dataDoc[this.fieldKey]), { _autoHeight: true, _viewType: CollectionViewType.Schema, title: StrCast(this.layoutDoc._searchString) }); + const headers = Cast(this.props.Document._schemaHeaders, listSpec(SchemaHeaderField), []).map(h => { const v = h[Copy](); v.color = "#f1efeb"; return v; }); + return Docs.Create.SchemaDocument(headers, DocListCast(this.dataDoc[this.fieldKey]), { _autoHeight: true, _viewType: CollectionViewType.Schema, title: StrCast(this.layoutDoc._searchString) }); } @action.bound openSearch(e: React.SyntheticEvent) { e.stopPropagation(); - this._openNoResults = false; - this._resultsOpen = true; - this._searchbarOpen = true; + this._results.forEach(result => Doc.BrushDoc(result[0])); } - realTotalResults: number = 0; - - @action.bound - closeSearch = () => { - //this.closeResults(); - this._searchbarOpen = false; - } + resetSearch = action((close: boolean) => { + this._results.forEach(result => { + Doc.UnBrushDoc(result[0]); + Doc.ClearSearchMatches(); + }); + close && (this.open = this._searchbarOpen = false); + }); @action.bound closeResults() { - this._resultsOpen = false; this._results = []; this._resultsSet.clear(); this._visibleElements = []; @@ -750,21 +397,10 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc this._curRequest = undefined; } - @observable _pageStart: number = 0; - @observable _pageCount: number = SearchBox.NUM_SEARCH_RESULTS_PER_PAGE; - - @observable children: number = 0; @action resultsScrolled = (e?: React.UIEvent<HTMLDivElement>) => { - if (!this._resultsRef.current) return; this._endIndex = 30; const headers = new Set<string>(["title", "author", "text", "type", "data", "*lastModified", "context"]); - // if ((this._numTotalResults === 0 || this._results.length === 0) && this._openNoResults) { - // if (this.noresults === "") { - // this.noresults = "No search results :("; - // } - // return; - // } if (this._numTotalResults <= this._maxSearchIndex) { this._numTotalResults = this._results.length; @@ -776,302 +412,170 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc // undefined until a searchitem is put in there this._visibleElements = Array<JSX.Element>(this._numTotalResults === -1 ? 0 : this._numTotalResults); this._visibleDocuments = Array<Doc>(this._numTotalResults === -1 ? 0 : this._numTotalResults); - // indicates if things are placeholders - this._isSearch = Array<undefined>(this._numTotalResults === -1 ? 0 : this._numTotalResults); - this._isSorted = Array<undefined>(this._numTotalResults === -1 ? 0 : this._numTotalResults); - } - let max = this.NumResults; + let max = this._numResultsPerPage; max > this._results.length ? max = this._results.length : console.log(""); for (let i = this._pageStart; i < max; i++) { //if the index is out of the window then put a placeholder in //should ones that have already been found get set to placeholders? - if (this._isSearch[i] !== "search") { - let result: [Doc, string[], string[]] | undefined = undefined; - - result = this._results[i]; - if (result) { - const highlights = Array.from([...Array.from(new Set(result[1]).values())]); - const lines = new List<string>(result[2]); - highlights.forEach((item) => headers.add(item)); - result[0].lines = lines; - result[0].highlighting = highlights.join(", "); - result[0].searchMatch = true; - if (i < this._visibleDocuments.length) { - this._visibleDocuments[i] = result[0]; - this._isSearch[i] = "search"; - Doc.BrushDoc(result[0]); - Doc.AddDocToList(this.dataDoc, this.props.fieldKey, result[0]); - this.children++; - } - + let result: [Doc, string[], string[]] | undefined = undefined; + + result = this._results[i]; + if (result) { + const highlights = Array.from([...Array.from(new Set(result[1]).values())]); + const lines = new List<string>(result[2]); + highlights.forEach((item) => headers.add(item)); + Doc.SetSearchMatch(result[0], { searchMatch: 1 }); + if (i < this._visibleDocuments.length) { + this._visibleDocuments[i] = result[0]; + Doc.BrushDoc(result[0]); + Doc.AddDocToList(this.dataDoc, this.props.fieldKey, result[0]); + this.children++; } - } } - this.headerscale = headers.size; - if (Cast(this.props.Document._docFilters, listSpec("string"), []).length === 0) { - const oldSchemaHeaders = Cast(this.props.Document._schemaHeaders, listSpec("string"), []); - if (oldSchemaHeaders?.length && typeof oldSchemaHeaders[0] !== "object") { - const newSchemaHeaders = oldSchemaHeaders.map(i => typeof i === "string" ? new SchemaHeaderField(i, "#f1efeb") : i); - headers.forEach(header => { - if (oldSchemaHeaders.includes(header) === false) { - newSchemaHeaders.push(new SchemaHeaderField(header, "#f1efeb")); - } - }); - this.headercount = newSchemaHeaders.length; - this.props.Document._schemaHeaders = new List<SchemaHeaderField>(newSchemaHeaders); - } else if (this.props.Document._schemaHeaders === undefined) { - this.props.Document._schemaHeaders = new List<SchemaHeaderField>([new SchemaHeaderField("title", "#f1efeb")]); - } + if (this.props.Document._schemaHeaders === undefined) { + this.props.Document._schemaHeaders = new List<SchemaHeaderField>([new SchemaHeaderField("title", "#f1efeb")]); } if (this._maxSearchIndex >= this._numTotalResults) { this._visibleElements.length = this._results.length; this._visibleDocuments.length = this._results.length; - this._isSearch.length = this._results.length; } } - @observable headercount: number = 0; - @observable headerscale: number = 0; - - findCommonElements(arr2: string[]) { - const arr1 = ["layout", "data"]; - return arr1.some(item => arr2.includes(item)); - } - - @computed - get resFull() { return this._numTotalResults <= 8; } - - @computed - get resultHeight() { return this._numTotalResults * 70; } - addButtonDoc = (doc: Doc) => Doc.AddDocToList(CurrentUserUtils.UserDocument.expandingButtons as Doc, "data", doc); - remButtonDoc = (doc: Doc) => Doc.RemoveDocFromList(CurrentUserUtils.UserDocument.expandingButtons as Doc, "data", doc); - moveButtonDoc = (doc: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean) => this.remButtonDoc(doc) && addDocument(doc); + getTransform = () => this.props.ScreenToLocalTransform().translate(-5, -65);// listBox padding-left and pres-box-cont minHeight + panelHeight = () => this.props.PanelHeight(); + selectElement = (doc: Doc) => { /* this.gotoDocument(this.childDocs.indexOf(doc), NumCasst(this.layoutDoc._itemIndex)); */ }; + returnHeight = () => NumCast(this.layoutDoc._height); + returnLength = () => Math.min(window.innerWidth, 51 + 205 * Cast(this.props.Document._schemaHeaders, listSpec(SchemaHeaderField), []).length); - @computed get searchItemTemplate() { return Cast(Doc.UserDoc().searchItemTemplate, Doc, null); } - - @computed get viewspec() { return Cast(this.props.Document._docFilters, listSpec("string"), []); } - - getTransform = () => { - return this.props.ScreenToLocalTransform().translate(-5, -65);// listBox padding-left and pres-box-cont minHeight - } - panelHeight = () => { - return this.props.PanelHeight(); - } - selectElement = (doc: Doc) => { - //this.gotoDocument(this.childDocs.indexOf(doc), NumCasst(this.layoutDoc._itemIndex)); - } - - addDocument = (doc: Doc) => { - return null; + @action + changeSearchScope = (scope: string) => { + this.docsforfilter = undefined; + this.setSearchFilter(this.currentSelectedCollection, undefined); + this._searchFullDB = scope; + this.dataDoc[this.fieldKey] = new List<Doc>([]); + this.submitSearch(); + } + + @computed get scopeButtons() { + return <div style={{ height: 25, paddingLeft: "4px", paddingRight: "4px", border: "1px solid gray", borderRadius: "0.3em", borderBottom: !this.open ? "1px solid" : "none", }}> + <form className="beta" style={{ justifyContent: "space-evenly", display: "flex" }}> + <div style={{ display: "contents" }}> + <div className="radio" style={{ margin: 0 }}> + <label style={{ fontSize: 12, marginTop: 6 }} > + <input type="radio" style={{ marginLeft: -16, marginTop: -1 }} checked={!this._searchFullDB} onChange={() => this.changeSearchScope("")} /> + Dashboard + </label> + </div> + <div className="radio" style={{ margin: 0 }}> + <label style={{ fontSize: 12, marginTop: 6 }} > + <input type="radio" style={{ marginLeft: -16, marginTop: -1 }} checked={this._searchFullDB?.length ? true : false} onChange={() => this.changeSearchScope("DB")} /> + DB + <span onClick={action(() => this._searchFullDB = this._searchFullDB === "My Stuff" ? "DB" : "My Stuff")}> + {this._searchFullDB === "My Stuff" ? "(me)" : "(full)"} + </span> + </label> + </div> + </div> + </form> + </div>; } - @observable filter = false; + setSearchFilter = action((collectionView: { props: { Document: Doc } }, docsForFilter: Doc[] | undefined) => { + if (collectionView) { + const docFilters = Cast(this.props.Document._docFilters, listSpec("string"), null); + collectionView.props.Document._searchFilterDocs = docsForFilter?.length ? new List<Doc>(docsForFilter) : undefined; + collectionView.props.Document._docFilters = docsForFilter?.length && docFilters?.length ? new List<string>(docFilters) : undefined; + } + }); - @action newpage() { - this._pageStart += SearchBox.NUM_SEARCH_RESULTS_PER_PAGE; - this.dataDoc[this.fieldKey] = new List<Doc>([]); - this.resultsScrolled(); - } render() { - this.props.Document._chromeStatus === "disabled"; - this.props.Document._searchDoc = true; - const cols = Cast(this.props.Document._schemaHeaders, listSpec(SchemaHeaderField), []).length; - let length = 0; - length = cols * 205 + 51; - let height = 0; - const rows = this.children; - height = 31 + 31 * 6; + const myDashboards = DocListCast(CurrentUserUtils.MyDashboards.data); return ( <div style={{ pointerEvents: "all" }} className="searchBox-container"> - <div style={{ position: "absolute", left: 15, height: 32, alignItems: "center", display: "flex" }}>{Doc.CurrentUserEmail}</div> <div className="searchBox-bar"> - <div style={{ position: "relative", display: "flex", width: 400 }}> - <input value={this.newsearchstring} autoComplete="off" onChange={this.onChange} type="text" placeholder="Search..." id="search-input" ref={this.inputRef} - className="searchBox-barChild searchBox-input" onPointerDown={this.openSearch} onKeyPress={this.enter} onFocus={this.openSearch} + <div className="searchBox-lozenges" > + <div className="searchBox-lozenge-user"> + {`${Doc.CurrentUserEmail}`} + <div className="searchBox-logoff" onClick={() => window.location.assign(Utils.prepend("/logout"))}> + Logoff + </div> + </div> + <div className="searchBox-lozenge"> + {`UI project`} + </div> + <div className="searchBox-lozenge-dashboard" > + <select className="searchBox-dashSelect" onChange={e => CurrentUserUtils.openDashboard(Doc.UserDoc(), myDashboards[Number(e.target.value)])} + value={myDashboards.indexOf(CurrentUserUtils.ActiveDashboard)}> + {myDashboards.map((dash, i) => <option key={dash[Id]} value={i}> {StrCast(dash.title)} </option>)} + </select> + <div className="searchBox-dashboards" onClick={undoBatch(() => CurrentUserUtils.createNewDashboard(Doc.UserDoc()))}> + New + </div> + <div className="searchBox-dashboards" onClick={undoBatch(() => CurrentUserUtils.snapshotDashboard(Doc.UserDoc()))}> + Snapshot + </div> + </div> + </div> + <div className="searchBox-query" > + <input defaultValue={""} autoComplete="off" onChange={this.onChange} type="text" placeholder="Search..." id="search-input" ref={this._inputRef} + className="searchBox-barChild searchBox-input" onKeyPress={this.enter} style={{ padding: 1, paddingLeft: 20, paddingRight: 60, color: "black", height: 20, width: 250 }} /> <div style={{ display: "flex", alignItems: "center" }}> <div style={{ position: "absolute", left: 10 }}> <Tooltip title={<div className="dash-tooltip" >drag search results as collection</div>}> - <div><FontAwesomeIcon onPointerDown={SetupDrag(this.collectionRef, () => StrCast(this.layoutDoc._searchString) ? this.startDragCollection() : undefined)} icon={"search"} size="lg" + <div ref={this.collectionRef}><FontAwesomeIcon onPointerDown={SetupDrag(this.collectionRef, () => StrCast(this.layoutDoc._searchString) ? this.startDragCollection() : undefined)} icon={"search"} size="lg" style={{ cursor: "hand", color: "black", padding: 1, position: "relative" }} /></div> </Tooltip> </div> - <div style={{ position: "absolute", left: 200, width: 30, zIndex: 9000, color: "grey", background: "white", }}> + <div style={{ position: "absolute", left: Doc.UserDoc().noviceMode ? 220 : 200, width: 30, zIndex: 9000, color: "grey", background: "white", }}> {`${this._results.length}` + " of " + `${this.realTotalResults}`} </div> - <div style={{ cursor: "default", left: 235, position: "absolute", }}> + {Doc.UserDoc().noviceMode ? (null) : <div style={{ cursor: "default", left: 235, position: "absolute", }}> <Tooltip title={<div className="dash-tooltip" >only display documents matching search</div>} > - <div><FontAwesomeIcon icon={"filter"} size="lg" - style={{ cursor: "hand", padding: 1, backgroundColor: this.filter ? "white" : "lightgray", color: this.filter ? "black" : "white" }} - onPointerDown={e => { e.stopPropagation(); SetupDrag(this.collectionRef, () => StrCast(this.layoutDoc._searchString) ? this.startDragCollection() : undefined); }} - onClick={action(() => { - ///DONT Change without emailing andy r first. - this.filter = !this.filter && !this.searchFullDB; - if (this.filter === true && this.currentSelectedCollection !== undefined) { - this.currentSelectedCollection.props.Document._searchDocs = new List<Doc>(this.docsforfilter); - let newarray: Doc[] = []; - let docs: Doc[] = []; - docs = DocListCast(this.currentSelectedCollection.dataDoc[Doc.LayoutFieldKey(this.currentSelectedCollection.dataDoc)]); - while (docs.length > 0) { - newarray = []; - docs.forEach((d) => { - if (d.data !== undefined) { - d._searchDocs = new List<Doc>(this.docsforfilter); - const newdocs = DocListCast(d.data); - newdocs.forEach((newdoc) => { - newarray.push(newdoc); - }); - } - }); - docs = newarray; - } - - this.currentSelectedCollection.props.Document._docFilters = new List<string>(this.viewspec); - this.props.Document.selectedDoc = this.currentSelectedCollection.props.Document; - } - else if (this.filter === false && this.currentSelectedCollection !== undefined) { - let newarray: Doc[] = []; - let docs: Doc[] = []; - docs = DocListCast(this.currentSelectedCollection.dataDoc[Doc.LayoutFieldKey(this.currentSelectedCollection.dataDoc)]); - while (docs.length > 0) { - newarray = []; - docs.forEach((d) => { - if (d.data !== undefined) { - d._searchDocs = new List<Doc>(); - d._docFilters = new List(); - const newdocs = DocListCast(d.data); - newdocs.forEach((newdoc) => { - newarray.push(newdoc); - }); - } - }); - docs = newarray; - } - - this.currentSelectedCollection.props.Document._searchDocs = new List<Doc>([]); - this.currentSelectedCollection.props.Document._docFilters = new List(); - this.props.Document.selectedDoc = undefined; - } - } - )} /></div> - </Tooltip> - </div> - <div style={{ - height: 25, - paddingLeft: "4px", - paddingRight: "4px", - border: "1px solid gray", - borderRadius: "0.3em", - borderBottom: this.open === false ? "1px solid" : "none", - }}> - <form className="beta" style={{ justifyContent: "space-evenly", display: "flex" }}> - <div style={{ display: "contents" }}> - <div className="radio" style={{ margin: 0 }}> - <label style={{ fontSize: 12, marginTop: 6 }} > - <input type="radio" style={{ marginLeft: -16, marginTop: -1 }} checked={!this.searchFullDB} onChange={() => { - runInAction(() => { - this.searchFullDB = !this.searchFullDB; - this.dataDoc[this.fieldKey] = new List<Doc>([]); - if (this.currentSelectedCollection !== undefined) { - let newarray: Doc[] = []; - let docs: Doc[] = []; - docs = DocListCast(this.currentSelectedCollection.dataDoc[Doc.LayoutFieldKey(this.currentSelectedCollection.dataDoc)]); - while (docs.length > 0) { - newarray = []; - docs.forEach((d) => { - if (d.data !== undefined) { - d._searchDocs = new List<Doc>(); - d._docFilters = new List(); - const newdocs = DocListCast(d.data); - newdocs.forEach((newdoc) => { - newarray.push(newdoc); - }); - } - }); - docs = newarray; - } - this.currentSelectedCollection.props.Document._docFilters = new List(); - this.currentSelectedCollection.props.Document._searchDocs = undefined; - this.currentSelectedCollection = undefined; - } - this.submitSearch(); - }); - }} /> - Collection - </label> - </div> - <div className="radio" style={{ margin: 0 }}> - <label style={{ fontSize: 12, marginTop: 6 }} > - <input style={{ marginLeft: -16, marginTop: -1 }} type="radio" checked={this.searchFullDB} onChange={() => { - runInAction(() => { - this.searchFullDB = !this.searchFullDB; - this.dataDoc[this.fieldKey] = new List<Doc>([]); - this.filter = false; - if (this.currentSelectedCollection !== undefined) { - let newarray: Doc[] = []; - let docs: Doc[] = []; - docs = DocListCast(this.currentSelectedCollection.dataDoc[Doc.LayoutFieldKey(this.currentSelectedCollection.dataDoc)]); - while (docs.length > 0) { - newarray = []; - docs.forEach((d) => { - if (d.data !== undefined) { - d._searchDocs = new List<Doc>(); - d._docFilters = new List(); - const newdocs = DocListCast(d.data); - newdocs.forEach((newdoc) => { - newarray.push(newdoc); - }); - } - }); - docs = newarray; - } - this.currentSelectedCollection.props.Document._docFilters = new List(); - this.currentSelectedCollection.props.Document._searchDocs = undefined; - this.currentSelectedCollection = undefined; - } - this.submitSearch(); - }); - }} /> - DB - </label> - </div> + <div> + <FontAwesomeIcon icon={"filter"} size="lg" + style={{ cursor: "hand", padding: 1, backgroundColor: this.filter ? "white" : "lightgray", color: this.filter ? "black" : "white" }} + onPointerDown={e => { e.stopPropagation(); SetupDrag(this.collectionRef, () => this.layoutDoc._searchString ? this.startDragCollection() : undefined); }} + onClick={action(() => this.setSearchFilter(this.currentSelectedCollection, this.filter ? undefined : this.docsforfilter))} /> </div> - </form> - </div> + </Tooltip> + </div>} + {this.scopeButtons} </div> - - </div> - </div> - <div style={{ zIndex: 20000, color: "black" }}> - {this._searchbarOpen === true ? + </div > + </div > + {!this._searchbarOpen ? (null) : + <div style={{ zIndex: 20000, color: "black" }} ref={(r) => r?.focus()}> <div style={{ display: "flex", justifyContent: "center", }}> - {this.noresults === "" ? <div style={{ display: this.open === true ? "flex" : "none", overflow: "auto", }}> - <CollectionView {...this.props} + <div style={{ display: this.open ? "flex" : "none", overflow: "auto", position: "absolute" }}> + <CollectionSchemaView {...this.props} + CollectionView={undefined} + annotationsKey={""} + addDocument={returnFalse} Document={this.props.Document} moveDocument={returnFalse} removeDocument={returnFalse} - PanelHeight={this.open === true ? () => height : () => 0} - PanelWidth={this.open === true ? () => length : () => 0} - overflow={length > window.innerWidth || rows > 6 ? true : false} + PanelHeight={this.open ? this.returnHeight : returnZero} + PanelWidth={this.open ? this.returnLength : returnZero} + overflow={length > window.innerWidth || this.children > 6 ? true : false} focus={this.selectElement} ScreenToLocalTransform={Transform.Identity} /> - </div> : - <div style={{ display: "flex", justifyContent: "center" }}><div style={{ height: 200, top: 54, minWidth: 400, position: "absolute", backgroundColor: "rgb(241, 239, 235)", display: "flex", justifyContent: "center", alignItems: "center", border: "black 1px solid", }}> - <div>{this.noresults}</div> - </div></div>} - </div> : undefined} - </div> - - <div className="searchBox-results" onScroll={this.resultsScrolled} style={{ - display: this._resultsOpen ? "flex" : "none", - height: this.resFull ? "auto" : this.resultHeight, - overflow: "visibile" // this.resFull ? "auto" : "visible" - }} ref={this._resultsRef}> - </div> + <div style={{ position: "absolute", right: 5, bottom: 7, width: 15, height: 15, }} + onPointerDown={e => setupMoveUpEvents(this, e, (e: PointerEvent, down: number[], delta: number[]) => { + this.props.Document._height = NumCast(this.props.Document._height) + delta[1]; + return false; + }, returnFalse, emptyFunction)} + > + <FontAwesomeIcon icon="grip-lines" size="lg" /> + </div> + </div> + </div> + </div> + } </div > ); } diff --git a/src/client/views/webcam/DashWebRTCVideo.tsx b/src/client/views/webcam/DashWebRTCVideo.tsx index 647e1ce6f..82c0e19c8 100644 --- a/src/client/views/webcam/DashWebRTCVideo.tsx +++ b/src/client/views/webcam/DashWebRTCVideo.tsx @@ -1,20 +1,16 @@ -import { observer } from "mobx-react"; -import React = require("react"); -import { CollectionFreeFormDocumentViewProps } from "../nodes/CollectionFreeFormDocumentView"; -import { FieldViewProps, FieldView } from "../nodes/FieldView"; -import { observable, action } from "mobx"; -import { DocumentDecorations } from "../DocumentDecorations"; -import "../../views/nodes/WebBox.scss"; -import "./DashWebRTCVideo.scss"; -import { initialize, hangup, refreshVideos } from "./WebCamLogic"; +import { faPhoneSlash, faSync } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; -import { faSync, faPhoneSlash } from "@fortawesome/free-solid-svg-icons"; +import { action, observable } from "mobx"; +import { observer } from "mobx-react"; import { Doc } from "../../../fields/Doc"; import { InkTool } from "../../../fields/InkField"; - -library.add(faSync); -library.add(faPhoneSlash); +import "../../views/nodes/WebBox.scss"; +import { DocumentDecorations } from "../DocumentDecorations"; +import { CollectionFreeFormDocumentViewProps } from "../nodes/CollectionFreeFormDocumentView"; +import { FieldView, FieldViewProps } from "../nodes/FieldView"; +import "./DashWebRTCVideo.scss"; +import { hangup, initialize, refreshVideos } from "./WebCamLogic"; +import React = require("react"); /** diff --git a/src/fields/DateField.ts b/src/fields/DateField.ts index bee62663e..48106d978 100644 --- a/src/fields/DateField.ts +++ b/src/fields/DateField.ts @@ -20,14 +20,14 @@ export class DateField extends ObjectField { } toString() { - return `${this.date.toISOString()}`; + return `${this.date.toLocaleString()}`; } [ToScriptString]() { return `new DateField(new Date(${this.date.toISOString()}))`; } [ToString]() { - return this.date.toISOString(); + return this.date.toLocaleString(); } getDate() { diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 0dcb8ab42..48fd831d6 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -23,6 +23,8 @@ import { deleteProperty, getField, getter, makeEditable, makeReadOnly, setter, u import { LinkManager } from "../client/util/LinkManager"; import JSZip = require("jszip"); import { saveAs } from "file-saver"; +import { CollectionDockingView } from "../client/views/collections/CollectionDockingView"; +import { SelectionManager } from "../client/util/SelectionManager"; export namespace Field { export function toKeyValueString(doc: Doc, key: string): string { @@ -91,6 +93,9 @@ export async function DocCastAsync(field: FieldResult): Promise<Opt<Doc>> { export function DocListCast(field: FieldResult): Doc[] { return Cast(field, listSpec(Doc), []).filter(d => d instanceof Doc) as Doc[]; } +export function DocListCastOrNull(field: FieldResult) { + return Cast(field, listSpec(Doc), null)?.filter(d => d instanceof Doc) as Doc[] | undefined; +} export const WidthSym = Symbol("Width"); export const HeightSym = Symbol("Height"); @@ -98,6 +103,7 @@ export const DataSym = Symbol("Data"); export const LayoutSym = Symbol("Layout"); export const FieldsSym = Symbol("Fields"); export const AclSym = Symbol("Acl"); +export const AclUnset = Symbol("AclUnset"); export const AclPrivate = Symbol("AclOwnerOnly"); export const AclReadonly = Symbol("AclReadOnly"); export const AclAddonly = Symbol("AclAddonly"); @@ -107,6 +113,7 @@ export const UpdatingFromServer = Symbol("UpdatingFromServer"); export const CachedUpdates = Symbol("Cached updates"); const AclMap = new Map<string, symbol>([ + ["None", AclUnset], [SharingPermissions.None, AclPrivate], [SharingPermissions.View, AclReadonly], [SharingPermissions.Add, AclAddonly], @@ -117,7 +124,7 @@ const AclMap = new Map<string, symbol>([ export function fetchProto(doc: Doc) { const permissions: { [key: string]: symbol } = {}; - Object.keys(doc).filter(key => key.startsWith("ACL")).forEach(key => permissions[key] = AclMap.get(StrCast(doc[key]))!); + Object.keys(doc).filter(key => key.startsWith("acl") && (permissions[key] = AclMap.get(StrCast(doc[key]))!)); if (Object.keys(permissions).length) doc[AclSym] = permissions; @@ -179,7 +186,7 @@ export class Doc extends RefField { this.___fields = value; for (const key in value) { const field = value[key]; - field && (this.__fieldKeys[key] = true); + (field !== undefined) && (this.__fieldKeys[key] = true); if (!(field instanceof ObjectField)) continue; field[Parent] = this[Self]; field[OnUpdate] = updateFunction(this[Self], key, field, this[SelfProxy]); @@ -207,10 +214,10 @@ export class Doc extends RefField { private [Self] = this; private [SelfProxy]: any; public [FieldsSym](clear?: boolean) { return clear ? this.___fields = this.___fieldKeys = {} : this.___fields; } - public [WidthSym]() { return NumCast(this[SelfProxy]._width); } - public [HeightSym]() { return NumCast(this[SelfProxy]._height); } - public [ToScriptString]() { return `DOC-"${this[Self][Id]}"-`; } - public [ToString]() { return `Doc(${GetEffectiveAcl(this) === AclPrivate ? "-inaccessible-" : this.title})`; } + public [WidthSym] = () => NumCast(this[SelfProxy]._width); + public [HeightSym] = () => NumCast(this[SelfProxy]._height); + public [ToScriptString] = () => `DOC-"${this[Self][Id]}"-`; + public [ToString] = () => `Doc(${GetEffectiveAcl(this[SelfProxy]) === AclPrivate ? "-inaccessible-" : this[SelfProxy].title})`; public get [LayoutSym]() { return this[SelfProxy].__LAYOUT__; } public get [DataSym]() { const self = this[SelfProxy]; @@ -249,7 +256,7 @@ export class Doc extends RefField { const prev = GetEffectiveAcl(this); this[UpdatingFromServer] = true; this[fKey] = value; - if (fKey.startsWith("ACL")) { + if (fKey.startsWith("acl")) { fetchProto(this); } this[UpdatingFromServer] = false; @@ -257,7 +264,7 @@ export class Doc extends RefField { DocServer.GetRefField(this[Id], true); } }; - if (sameAuthor || fKey.startsWith("ACL") || DocServer.getFieldWriteMode(fKey) !== DocServer.WriteMode.Playground) { + if (sameAuthor || fKey.startsWith("acl") || DocServer.getFieldWriteMode(fKey) !== DocServer.WriteMode.Playground) { delete this[CachedUpdates][fKey]; await fn(); } else { @@ -327,7 +334,7 @@ export namespace Doc { export function Get(doc: Doc, key: string, ignoreProto: boolean = false): FieldResult { try { return getField(doc[Self], key, ignoreProto); - } catch { + } catch { return doc; } } @@ -340,6 +347,9 @@ export namespace Doc { export function IsBaseProto(doc: Doc) { return GetT(doc, "baseProto", "boolean", true); } + export function IsSystem(doc: Doc) { + return GetT(doc, "system", "boolean", true); + } export async function SetInPlace(doc: Doc, key: string, value: Field | undefined, defaultProto: boolean) { const hasProto = doc.proto instanceof Doc; const onDeleg = Object.getOwnPropertyNames(doc).indexOf(key) !== -1; @@ -494,13 +504,15 @@ export namespace Doc { } export function MakeAlias(doc: Doc, id?: string) { - const alias = !GetT(doc, "isPrototype", "boolean", true) ? Doc.MakeCopy(doc, undefined, id) : Doc.MakeDelegate(doc, id); + const alias = !GetT(doc, "isPrototype", "boolean", true) && doc.proto ? Doc.MakeCopy(doc, undefined, id) : Doc.MakeDelegate(doc, id); const layout = Doc.LayoutField(alias); if (layout instanceof Doc && layout !== alias && layout === Doc.Layout(alias)) { Doc.SetLayout(alias, Doc.MakeAlias(layout)); } alias.aliasOf = doc; - alias.title = ComputedField.MakeFunction(`renameAlias(this, ${Doc.GetProto(doc).aliasNumber = NumCast(Doc.GetProto(doc).aliasNumber) + 1})`); + if (doc !== Doc.GetProto(doc)) { + alias.title = ComputedField.MakeFunction(`renameAlias(this, ${Doc.GetProto(doc).aliasNumber = NumCast(Doc.GetProto(doc).aliasNumber) + 1})`); + } alias.author = Doc.CurrentUserEmail; alias[AclSym] = doc[AclSym]; @@ -679,24 +691,26 @@ export namespace Doc { if (templateLayoutDoc.resolvedDataDoc === (targetDoc.rootDocument || Doc.GetProto(targetDoc)) && templateLayoutDoc.PARAMS === StrCast(targetDoc.PARAMS)) { expandedTemplateLayout = templateLayoutDoc; // reuse an existing template layout if its for the same document with the same params } else { - _pendingMap.set(targetDoc[Id] + expandedLayoutFieldKey + args, true); templateLayoutDoc.resolvedDataDoc && (templateLayoutDoc = Cast(templateLayoutDoc.proto, Doc, null) || templateLayoutDoc); // if the template has already been applied (ie, a nested template), then use the template's prototype - setTimeout(action(() => { - if (!targetDoc[expandedLayoutFieldKey]) { - const newLayoutDoc = Doc.MakeDelegate(templateLayoutDoc, undefined, "[" + templateLayoutDoc.title + "]"); - // the template's arguments are stored in params which is derefenced to find - // the actual field key where the parameterized template data is stored. - newLayoutDoc[params] = args !== "..." ? args : ""; // ... signifies the layout has sub template(s) -- so we have to expand the layout for them so that they can get the correct 'rootDocument' field, but we don't need to reassign their params. it would be better if the 'rootDocument' field could be passed dynamically to avoid have to create instances - newLayoutDoc.rootDocument = targetDoc; - targetDoc[expandedLayoutFieldKey] = newLayoutDoc; - const dataDoc = Doc.GetProto(targetDoc); - newLayoutDoc.resolvedDataDoc = dataDoc; - if (dataDoc[templateField] === undefined && templateLayoutDoc[templateField] instanceof List) { + if (!targetDoc[expandedLayoutFieldKey]) { + const newLayoutDoc = Doc.MakeDelegate(templateLayoutDoc, undefined, "[" + templateLayoutDoc.title + "]"); + // the template's arguments are stored in params which is derefenced to find + // the actual field key where the parameterized template data is stored. + newLayoutDoc[params] = args !== "..." ? args : ""; // ... signifies the layout has sub template(s) -- so we have to expand the layout for them so that they can get the correct 'rootDocument' field, but we don't need to reassign their params. it would be better if the 'rootDocument' field could be passed dynamically to avoid have to create instances + newLayoutDoc.rootDocument = targetDoc; + const dataDoc = Doc.GetProto(targetDoc); + newLayoutDoc.resolvedDataDoc = dataDoc; + + _pendingMap.set(targetDoc[Id] + expandedLayoutFieldKey + args, true); + setTimeout(() => { + if (dataDoc[templateField] === undefined && templateLayoutDoc[templateField] instanceof List && (templateLayoutDoc[templateField] as any).length) { dataDoc[templateField] = ComputedField.MakeFunction(`ObjectField.MakeCopy(templateLayoutDoc["${templateField}"] as List)`, { templateLayoutDoc: Doc.name }, { templateLayoutDoc }); } + targetDoc[expandedLayoutFieldKey] = newLayoutDoc; + _pendingMap.delete(targetDoc[Id] + expandedLayoutFieldKey + args); - } - }), 0); + }); + } } } return expandedTemplateLayout instanceof Doc ? expandedTemplateLayout : undefined; // layout is undefined if the expandedTemplateLayout is pending. @@ -764,7 +778,7 @@ export namespace Doc { } }); copy.author = Doc.CurrentUserEmail; - Doc.UserDoc().defaultAclPrivate && (copy["ACL-Public"] = "Not Shared"); + Doc.UserDoc().defaultAclPrivate && (copy["acl-Public"] = "Not Shared"); return copy; } @@ -792,7 +806,7 @@ export namespace Doc { const applied = ApplyTemplateTo(templateDoc, target, targetKey, templateDoc.title + "(..." + _applyCount++ + ")"); target.layoutKey = targetKey; applied && (Doc.GetProto(applied).type = templateDoc.type); - Doc.UserDoc().defaultAclPrivate && (applied["ACL-Public"] = "Not Shared"); + Doc.UserDoc().defaultAclPrivate && (applied["acl-Public"] = "Not Shared"); return applied; } return undefined; @@ -833,14 +847,6 @@ export namespace Doc { Cast(templateFieldValue, listSpec(Doc), [])?.map(d => d instanceof Doc && MakeMetadataFieldTemplate(d, templateDoc)); (Doc.GetProto(templateField)[metadataFieldKey] = ObjectField.MakeCopy(templateFieldValue)); } - // copy the textTemplates from 'this' (not 'self') because the layout contains the template info, not the original doc - if (templateCaptionValue instanceof RichTextField && !templateCaptionValue.Empty()) { - templateField["caption-textTemplate"] = ComputedField.MakeFunction(`copyField(this.caption)`); - } - if (templateFieldValue instanceof RichTextField && !templateFieldValue.Empty()) { - templateField[metadataFieldKey + "-textTemplate"] = ComputedField.MakeFunction(`copyField(this.${metadataFieldKey})`); - } - // get the layout string that the template uses to specify its layout const templateFieldLayoutString = StrCast(Doc.LayoutField(Doc.Layout(templateField))); @@ -868,16 +874,12 @@ export namespace Doc { } export function isBrushedHighlightedDegree(doc: Doc) { - if (Doc.IsHighlighted(doc)) { - return 6; - } - else { - return Doc.IsBrushedDegree(doc); - } + return Doc.IsHighlighted(doc) ? 6 : Doc.IsBrushedDegree(doc); } export class DocBrush { BrushedDoc: ObservableMap<Doc, boolean> = new ObservableMap(); + SearchMatchDoc: ObservableMap<Doc, { searchMatch: number }> = new ObservableMap(); } const brushManager = new DocBrush(); @@ -903,6 +905,34 @@ export namespace Doc { export function SetSelectedTool(tool: InkTool) { Doc.UserDoc().activeInkTool = tool; } export function GetSelectedTool(): InkTool { return StrCast(Doc.UserDoc().activeInkTool, InkTool.None) as InkTool; } export function SetUserDoc(doc: Doc) { manager._user_doc = doc; } + + export function IsSearchMatch(doc: Doc) { + return computedFn(function IsSearchMatch(doc: Doc) { + return brushManager.SearchMatchDoc.has(doc) ? brushManager.SearchMatchDoc.get(doc) : + brushManager.SearchMatchDoc.has(Doc.GetProto(doc)) ? brushManager.SearchMatchDoc.get(Doc.GetProto(doc)) : undefined; + })(doc); + } + export function IsSearchMatchUnmemoized(doc: Doc) { + return brushManager.SearchMatchDoc.has(doc) ? brushManager.SearchMatchDoc.get(doc) : + brushManager.SearchMatchDoc.has(Doc.GetProto(doc)) ? brushManager.SearchMatchDoc.get(Doc.GetProto(doc)) : undefined; + } + export function SetSearchMatch(doc: Doc, results: { searchMatch: number }) { + if (doc && GetEffectiveAcl(doc) !== AclPrivate && GetEffectiveAcl(Doc.GetProto(doc)) !== AclPrivate) { + brushManager.SearchMatchDoc.set(doc, results); + } + return doc; + } + export function SearchMatchNext(doc: Doc, backward: boolean) { + if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return doc; + const result = brushManager.SearchMatchDoc.get(doc); + const num = Math.abs(result?.searchMatch || 0) + 1; + runInAction(() => result && brushManager.SearchMatchDoc.set(doc, { searchMatch: backward ? -num : num })); + return doc; + } + export function ClearSearchMatches() { + brushManager.SearchMatchDoc.clear(); + } + export function IsBrushed(doc: Doc) { return computedFn(function IsBrushed(doc: Doc) { return brushManager.BrushedDoc.has(doc) || brushManager.BrushedDoc.has(Doc.GetProto(doc)); @@ -996,10 +1026,12 @@ export namespace Doc { const fieldVal = doc[key]; if (Cast(fieldVal, listSpec("string"), []).length) { const vals = Cast(fieldVal, listSpec("string"), []); - return vals.some(v => v === value); + const docs = vals.some(v => (v as any) instanceof Doc); + if (docs) return value === Field.toString(fieldVal as Field); + return vals.some(v => v.includes(value)); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring } const fieldStr = Field.toString(fieldVal as Field); - return fieldStr === value; + return fieldStr.includes(value); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring } export function deiconifyView(doc: any) { @@ -1013,7 +1045,8 @@ export namespace Doc { doc.layoutKey = deiconify || "layout"; } export function setDocFilterRange(target: Doc, key: string, range?: number[]) { - const docRangeFilters = Cast(target._docRangeFilters, listSpec("string"), []); + const container = target ?? CollectionDockingView.Instance.props.Document; + const docRangeFilters = Cast(container._docRangeFilters, listSpec("string"), []); for (let i = 0; i < docRangeFilters.length; i += 3) { if (docRangeFilters[i] === key) { docRangeFilters.splice(i, 3); @@ -1024,22 +1057,19 @@ export namespace Doc { docRangeFilters.push(key); docRangeFilters.push(range[0].toString()); docRangeFilters.push(range[1].toString()); - target._docRangeFilters = new List<string>(docRangeFilters); + container._docRangeFilters = new List<string>(docRangeFilters); } } - export function aliasDocs(field: any) { - return new List<Doc>(field.map((d: any) => Doc.MakeAlias(d))); - } - // 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 - export function setDocFilter(container: Doc, key: string, value: any, modifiers?: "match" | "check" | "x" | undefined) { + export function setDocFilter(target: Opt<Doc>, key: string, value: any, modifiers?: "remove" | "match" | "check" | "x" | undefined) { + const container = target ?? CollectionDockingView.Instance.props.Document; const docFilters = Cast(container._docFilters, listSpec("string"), []); runInAction(() => { for (let i = 0; i < docFilters.length; i += 3) { - if (docFilters[i] === key && (docFilters[i + 1] === value || modifiers === "match")) { + if (docFilters[i] === key && (docFilters[i + 1] === value || modifiers === "match" || modifiers === "remove")) { if (docFilters[i + 2] === modifiers && modifiers && docFilters[i + 1] === value) return; docFilters.splice(i, 3); container._docFilters = new List<string>(docFilters); @@ -1049,7 +1079,7 @@ export namespace Doc { if (typeof modifiers === "string") { if (!docFilters.length && modifiers === "match" && value === undefined) { container._docFilters = undefined; - } else { + } else if (modifiers !== "remove") { docFilters.push(key); docFilters.push(value); docFilters.push(modifiers); @@ -1097,6 +1127,24 @@ export namespace Doc { return false; } + export function copyDragFactory(dragFactory: Doc) { + const ndoc = dragFactory.isTemplateDoc ? Doc.ApplyTemplate(dragFactory) : Doc.MakeCopy(dragFactory, true); + if (ndoc && dragFactory["dragFactory-count"] !== undefined) { + dragFactory["dragFactory-count"] = NumCast(dragFactory["dragFactory-count"]) + 1; + Doc.SetInPlace(ndoc, "title", ndoc.title + " " + NumCast(dragFactory["dragFactory-count"]).toString(), true); + } + return ndoc; + } + export function delegateDragFactory(dragFactory: Doc) { + const ndoc = Doc.MakeDelegate(dragFactory); + ndoc.isPrototype = true; + if (ndoc && dragFactory["dragFactory-count"] !== undefined) { + dragFactory["dragFactory-count"] = NumCast(dragFactory["dragFactory-count"]) + 1; + Doc.GetProto(ndoc).title = ndoc.title + " " + NumCast(dragFactory["dragFactory-count"]).toString(); + } + return ndoc; + } + export namespace Get { @@ -1245,14 +1293,15 @@ Scripting.addGlobal(function getProto(doc: any) { return Doc.GetProto(doc); }); Scripting.addGlobal(function getDocTemplate(doc?: any) { return Doc.getDocTemplate(doc); }); Scripting.addGlobal(function getAlias(doc: any) { return Doc.MakeAlias(doc); }); Scripting.addGlobal(function getCopy(doc: any, copyProto: any) { return doc.isTemplateDoc ? Doc.ApplyTemplate(doc) : Doc.MakeCopy(doc, copyProto); }); +Scripting.addGlobal(function copyDragFactory(dragFactory: Doc) { return Doc.copyDragFactory(dragFactory); }); +Scripting.addGlobal(function delegateDragFactory(dragFactory: Doc) { return Doc.delegateDragFactory(dragFactory); }); Scripting.addGlobal(function copyField(field: any) { return field instanceof ObjectField ? ObjectField.MakeCopy(field) : field; }); -Scripting.addGlobal(function aliasDocs(field: any) { return Doc.aliasDocs(field); }); Scripting.addGlobal(function docList(field: any) { return DocListCast(field); }); Scripting.addGlobal(function setInPlace(doc: any, field: any, value: any) { return Doc.SetInPlace(doc, field, value, false); }); Scripting.addGlobal(function sameDocs(doc1: any, doc2: any) { return Doc.AreProtosEqual(doc1, doc2); }); Scripting.addGlobal(function deiconifyView(doc: any) { Doc.deiconifyView(doc); }); -Scripting.addGlobal(function undo() { return UndoManager.Undo(); }); -Scripting.addGlobal(function redo() { return UndoManager.Redo(); }); +Scripting.addGlobal(function undo() { SelectionManager.DeselectAll(); return UndoManager.Undo(); }); +Scripting.addGlobal(function redo() { SelectionManager.DeselectAll(); return UndoManager.Redo(); }); Scripting.addGlobal(function DOC(id: string) { console.log("Can't parse a document id in a script"); return "invalid"; }); Scripting.addGlobal(function assignDoc(doc: Doc, field: string, id: string) { return Doc.assignDocToField(doc, field, id); }); Scripting.addGlobal(function docCast(doc: FieldResult): any { return DocCastAsync(doc); }); @@ -1261,7 +1310,7 @@ Scripting.addGlobal(function activePresentationItem() { return curPres && DocListCast(curPres[Doc.LayoutFieldKey(curPres)])[NumCast(curPres._itemIndex)]; }); Scripting.addGlobal(function selectedDocs(container: Doc, excludeCollections: boolean, prevValue: any) { - const docs = DocListCast(Doc.UserDoc().activeSelection). + const docs = SelectionManager.SelectedDocuments().map(dv => dv.props.Document). filter(d => !Doc.AreProtosEqual(d, container) && !d.annotationOn && d.type !== DocumentType.DOCHOLDER && d.type !== DocumentType.KVP && (!excludeCollections || d.type !== DocumentType.COL || !Cast(d.data, listSpec(Doc), null))); return docs.length ? new List(docs) : prevValue; diff --git a/src/fields/List.ts b/src/fields/List.ts index a9da75abb..c9e4bd3c1 100644 --- a/src/fields/List.ts +++ b/src/fields/List.ts @@ -1,14 +1,16 @@ -import { Deserializable, autoObject, afterDocDeserialize } from "../client/util/SerializationHelper"; +import { action, observable, runInAction } from "mobx"; +import { alias, list, serializable } from "serializr"; +import { DocServer } from "../client/DocServer"; +import { Scripting } from "../client/util/Scripting"; +import { afterDocDeserialize, autoObject, Deserializable } from "../client/util/SerializationHelper"; import { Field } from "./Doc"; -import { setter, getter, deleteProperty, updateFunction } from "./util"; -import { serializable, alias, list } from "serializr"; -import { observable, action, runInAction } from "mobx"; +import { Copy, OnUpdate, Parent, Self, SelfProxy, ToScriptString, ToString, Update } from "./FieldSymbols"; import { ObjectField } from "./ObjectField"; -import { RefField } from "./RefField"; import { ProxyField } from "./Proxy"; -import { Self, Update, Parent, OnUpdate, SelfProxy, ToScriptString, ToString, Copy, Id } from "./FieldSymbols"; -import { Scripting } from "../client/util/Scripting"; -import { DocServer } from "../client/DocServer"; +import { RefField } from "./RefField"; +import { listSpec } from "./Schema"; +import { Cast } from "./Types"; +import { deleteProperty, getter, setter, updateFunction } from "./util"; const listHandlers: any = { /// Mutator methods @@ -322,10 +324,15 @@ class ListImpl<T extends Field> extends ObjectField { return `new List([${(this as any).map((field: any) => Field.toScriptString(field))}])`; } [ToString]() { - return "List"; + return `List(${(this as any).length})`; } } export type List<T extends Field> = ListImpl<T> & (T | (T extends RefField ? Promise<T> : never))[]; export const List: { new <T extends Field>(fields?: T[]): List<T> } = ListImpl as any; -Scripting.addGlobal("List", List);
\ No newline at end of file +Scripting.addGlobal("List", List); +Scripting.addGlobal(function compareLists(l1: any, l2: any) { + const L1 = Cast(l1, listSpec("string"), []); + const L2 = Cast(l2, listSpec("string"), []); + return !L1 && !L2 ? true : L1 && L2 && L1.length === L2.length && L2.reduce((p, v) => p && L1.includes(v), true); +}, "compare two lists");
\ No newline at end of file diff --git a/src/fields/RichTextField.ts b/src/fields/RichTextField.ts index 9c6233af5..ae5f301d0 100644 --- a/src/fields/RichTextField.ts +++ b/src/fields/RichTextField.ts @@ -28,7 +28,7 @@ export class RichTextField extends ObjectField { } [ToScriptString]() { - return `new RichTextField("${this.Data.replace(/"/g, "'")}", "${this.Text}")`; + return `new RichTextField("${this.Data.replace(/"/g, "\\\"")}", "${this.Text}")`; } [ToString]() { return this.Text; diff --git a/src/fields/RichTextUtils.ts b/src/fields/RichTextUtils.ts index a590c88c4..0b5f14d74 100644 --- a/src/fields/RichTextUtils.ts +++ b/src/fields/RichTextUtils.ts @@ -278,7 +278,7 @@ export namespace RichTextUtils { } else { docid = backingDocId; } - return schema.node("image", { src, agnostic, width, docid, float: null, location: "onRight" }); + return schema.node("image", { src, agnostic, width, docid, float: null, location: "add:right" }); }; const textNode = (schema: any, run: docs_v1.Schema$TextRun) => { diff --git a/src/fields/Schema.ts b/src/fields/Schema.ts index 23ac50f74..4607e0fd5 100644 --- a/src/fields/Schema.ts +++ b/src/fields/Schema.ts @@ -52,7 +52,7 @@ export function makeInterface<T extends Interface[]>(...schemas: T): InterfaceFu return field; }, set(target: any, prop, value, receiver) { - receiver.doc && (receiver.doc[prop] = value); // receiver.doc may be undefined as the result of a change in ACLs + receiver.doc && (receiver.doc[prop] = value); // receiver.doc may be undefined as the result of a change in acls return true; } }); diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts index 1fb71fefb..47efccc99 100644 --- a/src/fields/ScriptField.ts +++ b/src/fields/ScriptField.ts @@ -38,14 +38,14 @@ const scriptSchema = createSimpleSchema({ }); async function deserializeScript(script: ScriptField) { - if (script.script.originalScript === 'getCopy(this.dragFactory, true)') { - return (script as any).script = (ScriptField.GetCopyOfDragFactory ?? (ScriptField.GetCopyOfDragFactory = ScriptField.MakeFunction('getCopy(this.dragFactory, true)')))?.script; + if (script.script.originalScript === 'copyDragFactory(this.dragFactory)') { + return (script as any).script = (ScriptField.GetCopyOfDragFactory ?? (ScriptField.GetCopyOfDragFactory = ScriptField.MakeFunction('copyDragFactory(this.dragFactory)')))?.script; } if (script.script.originalScript === 'links(self)') { return (script as any).script = (ScriptField.LinksSelf ?? (ScriptField.LinksSelf = ComputedField.MakeFunction('links(self)')))?.script; } - if (script.script.originalScript === 'openOnRight(getCopy(this.dragFactory, true))') { - return (script as any).script = (ScriptField.OpenOnRight ?? (ScriptField.OpenOnRight = ComputedField.MakeFunction('openOnRight(getCopy(this.dragFactory, true))')))?.script; + if (script.script.originalScript === 'openOnRight(copyDragFactory(this.dragFactory))') { + return (script as any).script = (ScriptField.OpenOnRight ?? (ScriptField.OpenOnRight = ComputedField.MakeFunction('openOnRight(copyDragFactory(this.dragFactory))')))?.script; } if (script.script.originalScript === 'deiconifyView(self)') { return (script as any).script = (ScriptField.DeiconifyView ?? (ScriptField.DeiconifyView = ComputedField.MakeFunction('deiconifyView(self)')))?.script; @@ -73,6 +73,13 @@ async function deserializeScript(script: ScriptField) { throw new Error("Couldn't compile loaded script"); } (script as any).script = comp; + if (script.setterscript) { + const compset = CompileScript(script.setterscript?.originalScript, script.setterscript.options); + if (!compset.compiled) { + throw new Error("Couldn't compile setter script"); + } + (script as any).setterscript = compset; + } } @scriptingGlobal diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts index 848a648e1..71294c59c 100644 --- a/src/fields/documentSchemas.ts +++ b/src/fields/documentSchemas.ts @@ -13,10 +13,11 @@ export const documentSchema = createSchema({ links: listSpec(Doc), // computed (readonly) list of links associated with this document // "Location" properties in a very general sense - currentFrame: "number", // current frame of a frame based collection (e.g., a progressive slide) + _curPage: "number", // current page of a page based document + _currentFrame: "number", // current frame of a frame based collection (e.g., a progressive slide) lastFrame: "number", // last frame of a frame based collection (e.g., a progressive slide) activeFrame: "number", // the active frame of a frame based animated document - currentTimecode: "number", // current play back time of a temporal document (video / audio) + _currentTimecode: "number", // current play back time of a temporal document (video / audio) displayTimecode: "number", // the time that a document should be displayed (e.g., time an annotation should be displayed on a video) inOverlay: "boolean", // whether the document is rendered in an OverlayView which handles selection/dragging differently isLabel: "boolean", // whether the document is a label or not (video / audio) @@ -83,7 +84,10 @@ export const documentSchema = createSchema({ textTransform: "string", treeViewOpen: "boolean", // flag denoting whether the documents sub-tree (contents) is visible or hidden treeViewExpandedView: "string", // name of field whose contents are being displayed as the document's subtree + treeViewLockExpandedView: "boolean", // whether the expanded view can be changed + treeViewDefaultExpandedView: "string", // name of field whose contents are displayed by default treeViewPreventOpen: "boolean", // ignores the treeViewOpen flag (for allowing a view to not be slaved to other views of the document) + treeViewOutlineMode: "boolean", // whether tree view is an outline and clicks edit document titles immediately since double-click opening is turned off // interaction and linking properties ignoreClick: "boolean", // whether documents ignores input clicks (but does not ignore manipulation and other events) @@ -91,13 +95,13 @@ export const documentSchema = createSchema({ onPointerDown: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop) onPointerUp: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop) onDragStart: ScriptField, // script to run when document is dragged (without being selected). the script should return the Doc to be dropped. - followLinkLocation: "string",// flag for where to place content when following a click interaction (e.g., onRight, inPlace, inTab, ) + followLinkLocation: "string",// flag for where to place content when following a click interaction (e.g., add:right, inPlace, default, ) hideLinkButton: "boolean", // whether the blue link counter button should be hidden hideAllLinks: "boolean", // whether all individual blue anchor dots should be hidden linkDisplay: "boolean", // whether a link connection should be shown between link anchor endpoints. isInPlaceContainer: "boolean",// whether the marked object will display addDocTab() calls that target "inPlace" destinations isLinkButton: "boolean", // whether document functions as a link follow button to follow the first link on the document when clicked - isBackground: "boolean", // whether document is a background element and ignores input events (can only select with marquee) + _isBackground: "boolean", // whether document is a background element and ignores input events (can only select with marquee) lockedPosition: "boolean", // whether the document can be moved (dragged) _lockedTransform: "boolean",// whether a freeformview can pan/zoom diff --git a/src/fields/util.ts b/src/fields/util.ts index d8523dfa2..f0ff2dad4 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -1,5 +1,5 @@ import { UndoManager } from "../client/util/UndoManager"; -import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAddonly, AclSym, CachedUpdates, DataSym, DocListCast, AclAdmin, FieldsSym, HeightSym, WidthSym, fetchProto } from "./Doc"; +import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAddonly, AclSym, CachedUpdates, DataSym, DocListCast, AclAdmin, FieldsSym, HeightSym, WidthSym, fetchProto, AclUnset } from "./Doc"; import { SerializationHelper } from "../client/util/SerializationHelper"; import { ProxyField, PrefetchProxy } from "./Proxy"; import { RefField } from "./RefField"; @@ -10,6 +10,7 @@ import { DocServer } from "../client/DocServer"; import { ComputedField } from "./ScriptField"; import { ScriptCast, StrCast } from "./Types"; import { returnZero } from "../Utils"; +import CursorField from "./CursorField"; function _readOnlySetter(): never { @@ -111,7 +112,7 @@ export function makeEditable() { _setter = _setterImpl; } var _overrideAcl = false; -export function OVERRIDE_ACL(val: boolean) { +export function OVERRIDE_acl(val: boolean) { _overrideAcl = val; } @@ -156,16 +157,19 @@ export enum SharingPermissions { */ export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number, user?: string): symbol { if (!target) return AclPrivate; + + // all changes received fromt the server must be processed as Admin if (in_prop === UpdatingFromServer || target[UpdatingFromServer]) return AclAdmin; - if (target[AclSym] && Object.keys(target[AclSym]).length) { + // if the current user is the author of the document / the current user is a member of the admin group + const userChecked = user || Doc.CurrentUserEmail; + if (userChecked === (target.__fields?.author || target.author)) return AclAdmin; + if (currentUserGroups.includes("Admin")) return AclAdmin; - const userChecked = user || Doc.CurrentUserEmail; - // if the current user is the author of the document / the current user is a member of the admin group - if (userChecked === (target.__fields?.author || target.author) || currentUserGroups.includes("admin")) return AclAdmin; + if (target[AclSym] && Object.keys(target[AclSym]).length) { - // if the ACL is being overriden or the property being modified is one of the playground fields (which can be freely modified) + // if the acl is being overriden or the property being modified is one of the playground fields (which can be freely modified) if (_overrideAcl || (in_prop && DocServer.PlaygroundFields?.includes(in_prop.toString()))) return AclEdit; let effectiveAcl = AclPrivate; @@ -180,13 +184,19 @@ export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number, for (const [key, value] of Object.entries(target[AclSym])) { // there are issues with storing fields with . in the name, so they are replaced with _ during creation // as a result we need to restore them again during this comparison. - if (currentUserGroups.includes(key.substring(4).replace("_", ".")) || userChecked === key.substring(4).replace("_", ".")) { + const entity = key.substring(4).replace('_', '.'); // an individual or a group + if (currentUserGroups.includes(entity) || userChecked === entity) { if (HierarchyMapping.get(value as symbol)! > HierarchyMapping.get(effectiveAcl)!) { effectiveAcl = value as symbol; - if (effectiveAcl === AclAdmin) break; + if (effectiveAcl === AclAdmin) return effectiveAcl; } } } + + // if there's an overriding acl set through the properties panel or sharing menu, that's what's returned if the user isn't an admin of the document + const override = target[AclSym]["acl-Override"]; + if (override !== AclUnset && override !== undefined) effectiveAcl = target[AclSym]["acl-Override"]; + // if we're in playground mode, return AclEdit (or AclAdmin if that's the user's effectiveAcl) return DocServer?.Control?.isReadOnly?.() && HierarchyMapping.get(effectiveAcl)! < 3 ? AclEdit : effectiveAcl; } @@ -194,13 +204,16 @@ export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number, } /** * Recursively distributes the access right for a user across the children of a document and its annotations. - * @param key the key storing the access right (e.g. ACL-groupname) + * @param key the key storing the access right (e.g. acl-groupname) * @param acl the access right being stored (e.g. "Can Edit") * @param target the document on which this access right is being set - * @param inheritingFromCollection whether the target is being assigned rights after being dragged into a collection (and so is inheriting the ACLs from the collection) - * inheritingFromCollection is not currently being used but could be used if ACL assignment defaults change + * @param inheritingFromCollection whether the target is being assigned rights after being dragged into a collection (and so is inheriting the acls from the collection) + * inheritingFromCollection is not currently being used but could be used if acl assignment defaults change */ -export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, inheritingFromCollection?: boolean) { +export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, inheritingFromCollection?: boolean, visited?: Doc[]) { + if (!visited) visited = [] as Doc[]; + if (visited.includes(target)) return; + visited.push(target); const HierarchyMapping = new Map<string, number>([ ["Not Shared", 0], @@ -215,42 +228,41 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc const dataDoc = target[DataSym]; // if it is inheriting from a collection, it only inherits if A) the key doesn't already exist or B) the right being inherited is more restrictive - if (!inheritingFromCollection || !target[key] || HierarchyMapping.get(StrCast(target[key]))! > HierarchyMapping.get(acl)!) { + if (GetEffectiveAcl(target) === AclAdmin && (!inheritingFromCollection || !target[key] || HierarchyMapping.get(StrCast(target[key]))! > HierarchyMapping.get(acl)!)) { target[key] = acl; layoutDocChanged = true; } if (dataDoc && (!inheritingFromCollection || !dataDoc[key] || HierarchyMapping.get(StrCast(dataDoc[key]))! > HierarchyMapping.get(acl)!)) { - dataDoc[key] = acl; - dataDocChanged = true; - // maps over the aliases of the document - const aliases = DocListCast(dataDoc.aliases); - if (aliases.length) { - aliases.map(alias => { - alias !== target && distributeAcls(key, acl, alias, inheritingFromCollection); - }); + if (GetEffectiveAcl(dataDoc) === AclAdmin) { + dataDoc[key] = acl; + dataDocChanged = true; } + // maps over the aliases of the document + const links = DocListCast(dataDoc.links); + links.forEach(link => distributeAcls(key, acl, link, inheritingFromCollection, visited)); + // maps over the children of the document DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc)]).map(d => { - if (GetEffectiveAcl(d) === AclAdmin && (!inheritingFromCollection || !d[key] || HierarchyMapping.get(StrCast(d[key]))! > HierarchyMapping.get(acl)!)) { - distributeAcls(key, acl, d, inheritingFromCollection); - } + // if (GetEffectiveAcl(d) === AclAdmin && (!inheritingFromCollection || !d[key] || HierarchyMapping.get(StrCast(d[key]))! > HierarchyMapping.get(acl)!)) { + distributeAcls(key, acl, d, inheritingFromCollection, visited); + // } const data = d[DataSym]; - if (data && GetEffectiveAcl(data) === AclAdmin && (!inheritingFromCollection || !data[key] || HierarchyMapping.get(StrCast(data[key]))! > HierarchyMapping.get(acl)!)) { - distributeAcls(key, acl, data, inheritingFromCollection); + if (data) {// && GetEffectiveAcl(data) === AclAdmin && (!inheritingFromCollection || !data[key] || HierarchyMapping.get(StrCast(data[key]))! > HierarchyMapping.get(acl)!)) { + distributeAcls(key, acl, data, inheritingFromCollection, visited); } }); // maps over the annotations of the document DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + "-annotations"]).map(d => { - if (GetEffectiveAcl(d) === AclAdmin && (!inheritingFromCollection || !d[key] || HierarchyMapping.get(StrCast(d[key]))! > HierarchyMapping.get(acl)!)) { - distributeAcls(key, acl, d, inheritingFromCollection); - } + // if (GetEffectiveAcl(d) === AclAdmin && (!inheritingFromCollection || !d[key] || HierarchyMapping.get(StrCast(d[key]))! > HierarchyMapping.get(acl)!)) { + distributeAcls(key, acl, d, inheritingFromCollection, visited); + // } const data = d[DataSym]; - if (data && GetEffectiveAcl(data) === AclAdmin && (!inheritingFromCollection || !data[key] || HierarchyMapping.get(StrCast(data[key]))! > HierarchyMapping.get(acl)!)) { - distributeAcls(key, acl, data, inheritingFromCollection); + if (data) {// && GetEffectiveAcl(data) === AclAdmin && (!inheritingFromCollection || !data[key] || HierarchyMapping.get(StrCast(data[key]))! > HierarchyMapping.get(acl)!)) { + distributeAcls(key, acl, data, inheritingFromCollection, visited); } }); } @@ -267,8 +279,8 @@ export function setter(target: any, in_prop: string | symbol | number, value: an if (effectiveAcl !== AclEdit && effectiveAcl !== AclAdmin) return true; // if you're trying to change an acl but don't have Admin access / you're trying to change it to something that isn't an acceptable acl, you can't - if (typeof prop === "string" && prop.startsWith("ACL") && (effectiveAcl !== AclAdmin || ![...Object.values(SharingPermissions), undefined].includes(value))) return true; - // if (typeof prop === "string" && prop.startsWith("ACL") && !["Can Edit", "Can Add", "Can View", "Not Shared", undefined].includes(value)) return true; + if (typeof prop === "string" && prop.startsWith("acl") && (effectiveAcl !== AclAdmin || ![...Object.values(SharingPermissions), undefined, "None"].includes(value))) return true; + // if (typeof prop === "string" && prop.startsWith("acl") && !["Can Edit", "Can Add", "Can View", "Not Shared", undefined].includes(value)) return true; if (typeof prop === "string" && prop !== "__id" && prop !== "__fields" && (prop.startsWith("_") || layoutProps.includes(prop))) { if (!prop.startsWith("_")) { @@ -289,7 +301,7 @@ export function setter(target: any, in_prop: string | symbol | number, value: an export function getter(target: any, in_prop: string | symbol | number, receiver: any): any { let prop = in_prop; - if (in_prop === ToString || in_prop === ToScriptString || in_prop === FieldsSym || in_prop === Id || in_prop === HandleUpdate || in_prop === CachedUpdates) return target.__fields[prop] || target[prop]; + if (in_prop === "toString" || in_prop === ToString || in_prop === ToScriptString || in_prop === FieldsSym || in_prop === Id || in_prop === HandleUpdate || in_prop === CachedUpdates) return target.__fields[prop] || target[prop]; if (in_prop === AclSym) return _overrideAcl ? undefined : target[AclSym]; if (GetEffectiveAcl(target) === AclPrivate && !_overrideAcl) return prop === HeightSym || prop === WidthSym ? returnZero : undefined; if (prop === LayoutSym) { @@ -357,10 +369,12 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any const oldValue = current; const newValue = ObjectField.MakeCopy(value); current = newValue; - UndoManager.AddEvent({ - redo() { receiver[prop] = newValue; }, - undo() { receiver[prop] = oldValue; } - }); + if (!(value instanceof CursorField) && !(value?.some?.((v: any) => v instanceof CursorField))) { + UndoManager.AddEvent({ + redo() { receiver[prop] = newValue; }, + undo() { receiver[prop] = oldValue; } + }); + } } target[Update](diff); }; diff --git a/src/mobile/AudioUpload.tsx b/src/mobile/AudioUpload.tsx index 8ae504f1b..36663c85f 100644 --- a/src/mobile/AudioUpload.tsx +++ b/src/mobile/AudioUpload.tsx @@ -3,18 +3,18 @@ import "./ImageUpload.scss"; import React = require('react'); import { observer } from 'mobx-react'; import { observable, action, computed } from 'mobx'; -import { Utils, emptyPath, returnFalse, emptyFunction, returnOne, returnZero, returnTrue, returnEmptyFilter } from '../Utils'; +import { Utils, emptyPath, returnFalse, emptyFunction, returnOne, returnZero, returnTrue, returnEmptyFilter, returnEmptyDoclist } from '../Utils'; import { Doc, Opt } from '../fields/Doc'; import { Cast, FieldValue } from '../fields/Types'; import { listSpec } from '../fields/Schema'; -import MainViewModal from '../client/views/MainViewModal'; +import { MainViewModal } from '../client/views/MainViewModal'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { nullAudio } from '../fields/URLField'; import { Transform } from '../client/util/Transform'; import { DocumentView } from '../client/views/nodes/DocumentView'; import { MobileInterface } from './MobileInterface'; import { DictationOverlay } from '../client/views/DictationOverlay'; -import RichTextMenu from '../client/views/nodes/formattedText/RichTextMenu'; +import { RichTextMenu } from '../client/views/nodes/formattedText/RichTextMenu'; import { ContextMenu } from '../client/views/ContextMenu'; @observer @@ -53,7 +53,7 @@ export class AudioUpload extends React.Component { * Pushing the audio doc onto Dash Web through the right side bar */ uploadAudio = () => { - const audioRightSidebar = Cast(Doc.UserDoc()["sidebar-sharing"], Doc) as Doc; + const audioRightSidebar = Cast(Doc.UserDoc().mySharedDocs, Doc) as Doc; const audioDoc = this._audioCol; const data = Cast(audioRightSidebar.data, listSpec(Doc)); for (let i = 1; i < 8; i++) { @@ -88,13 +88,13 @@ export class AudioUpload extends React.Component { rootSelected={returnTrue} removeDocument={undefined} docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} onClick={undefined} ScreenToLocalTransform={Transform.Identity} ContentScaling={returnOne} PanelWidth={() => 600} PanelHeight={() => 400} - NativeHeight={returnZero} - NativeWidth={returnZero} renderDepth={0} focus={emptyFunction} backgroundColor={() => "rgba(0,0,0,0)"} diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx index d21d326f6..1ee4c7815 100644 --- a/src/mobile/ImageUpload.tsx +++ b/src/mobile/ImageUpload.tsx @@ -1,25 +1,27 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, observable } from 'mobx'; +import { observer } from 'mobx-react'; import * as rp from 'request-promise'; -import { Docs } from '../client/documents/Documents'; -import "./ImageUpload.scss"; -import React = require('react'); import { DocServer } from '../client/DocServer'; -import { observer } from 'mobx-react'; -import { observable, action } from 'mobx'; -import { Utils } from '../Utils'; +import { Docs } from '../client/documents/Documents'; import { Networking } from '../client/Network'; +import { DFLT_IMAGE_NATIVE_DIM } from '../client/views/globalCssVariables.scss'; +import { MainViewModal } from '../client/views/MainViewModal'; import { Doc, Opt } from '../fields/Doc'; -import { Cast } from '../fields/Types'; -import { listSpec } from '../fields/Schema'; import { List } from '../fields/List'; -import MainViewModal from '../client/views/MainViewModal'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { listSpec } from '../fields/Schema'; +import { Cast } from '../fields/Types'; +import { Utils } from '../Utils'; +import "./ImageUpload.scss"; import { MobileInterface } from './MobileInterface'; +import React = require('react'); export interface ImageUploadProps { Document: Doc; // Target document for upload (upload location) } const inputRef = React.createRef<HTMLInputElement>(); +const defaultNativeImageDim = Number(DFLT_IMAGE_NATIVE_DIM.replace("px", "")); @observer export class Uploader extends React.Component<ImageUploadProps> { @@ -52,13 +54,13 @@ export class Uploader extends React.Component<ImageUploadProps> { let doc = null; // Case 1: File is a video if (file.type === "video/mp4") { - doc = Docs.Create.VideoDocument(path, { _nativeWidth: 400, _width: 400, title: name }); + doc = Docs.Create.VideoDocument(path, { _nativeWidth: defaultNativeImageDim, _width: 400, title: name }); // Case 2: File is a PDF document } else if (file.type === "application/pdf") { - doc = Docs.Create.PdfDocument(path, { _nativeWidth: 400, _width: 400, _fitWidth: true, title: name }); + doc = Docs.Create.PdfDocument(path, { _nativeWidth: defaultNativeImageDim, _width: 400, _fitWidth: true, title: name }); // Case 3: File is another document type (most likely Image) } else { - doc = Docs.Create.ImageDocument(path, { _nativeWidth: 400, _width: 400, title: name }); + doc = Docs.Create.ImageDocument(path, { _nativeWidth: defaultNativeImageDim, _width: 400, title: name }); } this.setOpacity(4, "1"); // Slab 4 const res = await rp.get(Utils.prepend("/getUserDocumentId")); diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx index c5e395d2f..72914e62b 100644 --- a/src/mobile/MobileInterface.tsx +++ b/src/mobile/MobileInterface.tsx @@ -1,4 +1,4 @@ -import * as React from "react"; + import { library } from '@fortawesome/fontawesome-svg-core'; import { faTasks, faReply, faQuoteLeft, faHandPointLeft, faFolderOpen, faAngleDoubleLeft, faExternalLinkSquareAlt, faMobile, faThLarge, faWindowClose, faEdit, faTrashAlt, faPalette, faAngleRight, faBell, faTrash, faCamera, faExpand, faCaretDown, faCaretLeft, faCaretRight, faCaretSquareDown, faCaretSquareRight, faArrowsAltH, faPlus, faMinus, @@ -10,34 +10,35 @@ import { faAlignRight, faAlignLeft } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable, reaction, trace, runInAction } from 'mobx'; +import { action, computed, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; -import { Doc, DocListCast } from '../fields/Doc'; -import { CurrentUserUtils } from '../client/util/CurrentUserUtils'; -import { emptyFunction, emptyPath, returnFalse, returnOne, returnTrue, returnZero, returnEmptyFilter } from '../Utils'; +import * as React from "react"; import { Docs, DocumentOptions } from '../client/documents/Documents'; +import { DocumentType } from "../client/documents/DocumentTypes"; +import { CurrentUserUtils } from '../client/util/CurrentUserUtils'; import { Scripting } from '../client/util/Scripting'; -import { DocumentView } from '../client/views/nodes/DocumentView'; +import { SettingsManager } from '../client/util/SettingsManager'; import { Transform } from '../client/util/Transform'; -import "./MobileInterface.scss"; -import "./ImageUpload.scss"; -import "./AudioUpload.scss"; -import SettingsManager from '../client/util/SettingsManager'; -import { Uploader } from "./ImageUpload"; -import { DockedFrameRenderer } from '../client/views/collections/CollectionDockingView'; -import { InkTool } from '../fields/InkField'; -import GestureOverlay from "../client/views/GestureOverlay"; -import { ScriptField } from "../fields/ScriptField"; -import { RadialMenu } from "../client/views/nodes/RadialMenu"; import { UndoManager } from "../client/util/UndoManager"; +import { TabDocView } from '../client/views/collections/TabDocView'; +import { CollectionViewType } from "../client/views/collections/CollectionView"; +import { GestureOverlay } from "../client/views/GestureOverlay"; +import { AudioBox } from "../client/views/nodes/AudioBox"; +import { DocumentView } from '../client/views/nodes/DocumentView'; +import { RichTextMenu } from "../client/views/nodes/formattedText/RichTextMenu"; +import { RadialMenu } from "../client/views/nodes/RadialMenu"; +import { Doc, DocListCast } from '../fields/Doc'; +import { InkTool } from '../fields/InkField'; import { List } from "../fields/List"; -import { AudioUpload } from "./AudioUpload"; +import { ScriptField } from "../fields/ScriptField"; import { Cast, FieldValue } from '../fields/Types'; -import RichTextMenu from "../client/views/nodes/formattedText/RichTextMenu"; -import { AudioBox } from "../client/views/nodes/AudioBox"; -import { CollectionViewType } from "../client/views/collections/CollectionView"; -import { DocumentType } from "../client/documents/DocumentTypes"; -import { CollectionFreeFormViewChrome } from "../client/views/collections/CollectionMenu"; +import { emptyFunction, emptyPath, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, returnTrue, returnZero } from '../Utils'; +import { AudioUpload } from "./AudioUpload"; +import { Uploader } from "./ImageUpload"; +import "./AudioUpload.scss"; +import "./ImageUpload.scss"; +import "./MobileInterface.scss"; +import { InkStrokeProperties } from '../client/views/InkStrokeProperties'; library.add(faTasks, faReply, faQuoteLeft, faHandPointLeft, faFolderOpen, faAngleDoubleLeft, faExternalLinkSquareAlt, faMobile, faThLarge, faWindowClose, faEdit, faTrashAlt, faPalette, faAngleRight, faBell, faTrash, faCamera, faExpand, faCaretDown, faCaretLeft, faCaretRight, faCaretSquareDown, faCaretSquareRight, faArrowsAltH, faPlus, faMinus, faTerminal, faToggleOn, fileSolid, faExternalLinkAlt, faLocationArrow, faSearch, faFileDownload, faStop, faCalculator, faWindowMaximize, faAddressCard, @@ -47,6 +48,7 @@ library.add(faTasks, faReply, faQuoteLeft, faHandPointLeft, faFolderOpen, faAngl faThumbtack, faTree, faTv, faUndoAlt, faBook, faVideo, faAsterisk, faBrain, faImage, faPaintBrush, faTimes, faEye, faHome, faLongArrowAltLeft, faBars, faTh, faChevronLeft, faAlignLeft, faAlignRight); + @observer export class MobileInterface extends React.Component { static Instance: MobileInterface; @@ -58,7 +60,7 @@ export class MobileInterface extends React.Component { @observable private _menuListView: boolean = false; //to switch between menu view (list / icon) @observable private _ink: boolean = false; //toggle whether ink is being dispalyed @observable private _homeMenu: boolean = true; // to determine whether currently at home menu - @observable private _child: Doc | null = null; // currently selected document + @observable private dashboards: Doc | null = null; // currently selected document @observable private _activeDoc: Doc = this._mainDoc; // doc updated as the active mobile page is updated (initially home menu) @observable private _homeDoc: Doc = this._mainDoc; // home menu as a document @observable private _parents: Array<Doc> = []; // array of parent docs (for pathbar) @@ -136,19 +138,19 @@ export class MobileInterface extends React.Component { back = () => { const header = document.getElementById("header") as HTMLElement; const doc = Cast(this._parents.pop(), Doc) as Doc; // Parent document - // Case 1: Parent document is 'workspaces' + // Case 1: Parent document is 'dashboards' if (doc === Cast(this._library, Doc) as Doc) { - this._child = null; + this.dashboards = null; this._library.then(library => this.switchCurrentView(library)); // Case 2: Parent document is the 'home' menu (root node) } else if (doc === Cast(this._homeDoc, Doc) as Doc) { this._homeMenu = true; this._parents = []; - this._child = null; + this.dashboards = null; this.switchCurrentView(this._homeDoc); // Case 3: Parent document is any document } else if (doc) { - this._child = doc; + this.dashboards = doc; this.switchCurrentView(doc); this._homeMenu = false; header.textContent = String(doc.title); @@ -164,7 +166,7 @@ export class MobileInterface extends React.Component { if (!this._homeMenu || this._sidebarActive) { this._homeMenu = true; this._parents = []; - this._child = null; + this.dashboards = null; this.switchCurrentView(this._homeDoc); } if (this._sidebarActive) { @@ -173,14 +175,14 @@ export class MobileInterface extends React.Component { } /** - * Return to primary Workspace in library (Workspaces Doc) + * Return to primary Dashboard in library (Dashboards Doc) */ @action returnMain = () => { this._parents = [this._homeDoc]; this._library.then(library => this.switchCurrentView(library)); this._homeMenu = false; - this._child = null; + this.dashboards = null; } /** @@ -194,7 +196,7 @@ export class MobileInterface extends React.Component { /** * DocumentView for graphic display of all documents */ - @computed get displayWorkspaces() { + @computed get displayDashboards() { return !this.mainContainer ? (null) : <div style={{ position: "relative", top: '198px', height: `calc(100% - 350px)`, width: "100%", left: "0%" }}> <DocumentView @@ -211,8 +213,6 @@ export class MobileInterface extends React.Component { ContentScaling={returnOne} PanelWidth={this.returnWidth} PanelHeight={this.returnHeight} - NativeHeight={returnZero} - NativeWidth={returnZero} renderDepth={0} focus={emptyFunction} backgroundColor={this.whitebackground} @@ -220,6 +220,8 @@ export class MobileInterface extends React.Component { whenActiveChanged={emptyFunction} bringToFront={emptyFunction} docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} /> @@ -243,7 +245,7 @@ export class MobileInterface extends React.Component { this._parents.push(this._activeDoc); this.switchCurrentView(doc); this._homeMenu = false; - this._child = doc; + this.dashboards = doc; } }); } @@ -258,7 +260,7 @@ export class MobileInterface extends React.Component { this._parents.push(this._activeDoc); this.switchCurrentView(doc); this._homeMenu = false; - this._child = doc; + this.dashboards = doc; this.toggleSidebar(); } @@ -290,13 +292,13 @@ export class MobileInterface extends React.Component { handlePathClick = async (doc: Doc, index: number) => { const library = await this._library; if (doc === library) { - this._child = null; + this.dashboards = null; this.switchCurrentView(doc); this._parents.length = index; } else if (doc === this._homeDoc) { this.returnHome(); } else { - this._child = doc; + this.dashboards = doc; this.switchCurrentView(doc); this._parents.length = index; } @@ -321,13 +323,13 @@ export class MobileInterface extends React.Component { </div> ); } - // stores workspace documents as 'workspaces' variable - let workspaces = Cast(Doc.UserDoc().myWorkspaces, Doc) as Doc; - if (this._child) { - workspaces = this._child; + // stores dashboards documents as 'dashboards' variable + let dashboards = CurrentUserUtils.MyDashboards; + if (this.dashboards) { + dashboards = this.dashboards; } // returns a list of navbar buttons as 'buttons' - const buttons = DocListCast(workspaces.data).map((doc: Doc, index: any) => { + const buttons = DocListCast(dashboards.data).map((doc: Doc, index: any) => { if (doc.type !== "ink") { return ( <div @@ -357,7 +359,7 @@ export class MobileInterface extends React.Component { {this.renderPathbar()} <div className={`sidebar ${this._sidebarActive ? "active" : ""}`}> <div className="sidebarButtons"> - {this._child ? + {this.dashboards ? <> {buttons} <div @@ -365,7 +367,7 @@ export class MobileInterface extends React.Component { onClick={this.returnMain} style={{ opacity: 0.7 }}> <FontAwesomeIcon className="right" icon="angle-double-left" size="lg" /> - <div className="item-type">Return to workspaces</div> + <div className="item-type">Return to dashboards</div> </div> </> : <> @@ -373,9 +375,9 @@ export class MobileInterface extends React.Component { <div className="item" style={{ opacity: 0.7 }} - onClick={() => this.createNewWorkspace()}> + onClick={() => this.createNewDashboard()}> <FontAwesomeIcon className="right" icon="plus" size="lg" /> - <div className="item-type">Create New Workspace</div> + <div className="item-type">Create New Dashboard</div> </div> </> } @@ -388,27 +390,27 @@ export class MobileInterface extends React.Component { } /** - * Handles the 'Create New Workspace' button in the menu (taken from MainView.tsx) + * Handles the 'Create New Dashboard' button in the menu (taken from MainView.tsx) */ @action - createNewWorkspace = async (id?: string) => { - const workspaces = Cast(Doc.UserDoc().myWorkspaces, Doc) as Doc; - const workspaceCount = DocListCast(workspaces.data).length + 1; + createNewDashboard = async (id?: string) => { + const scens = CurrentUserUtils.MyDashboards; + const dashboardCount = DocListCast(scens.data).length + 1; const freeformOptions: DocumentOptions = { x: 0, y: 400, - title: "Collection " + workspaceCount, + title: "Collection " + dashboardCount, }; const freeformDoc = CurrentUserUtils.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions); - const workspaceDoc = Docs.Create.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600, path: [Doc.UserDoc().myCatalog as Doc] }], { title: `Workspace ${workspaceCount}` }, id, "row"); + const dashboardDoc = Docs.Create.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600 }], { title: `Dashboard ${dashboardCount}` }, id, "row"); const toggleTheme = ScriptField.MakeScript(`self.darkScheme = !self.darkScheme`); const toggleComic = ScriptField.MakeScript(`toggleComicMode()`); - const cloneWorkspace = ScriptField.MakeScript(`cloneWorkspace()`); - workspaceDoc.contextMenuScripts = new List<ScriptField>([toggleTheme!, toggleComic!, cloneWorkspace!]); - workspaceDoc.contextMenuLabels = new List<string>(["Toggle Theme Colors", "Toggle Comic Mode", "New Workspace Layout"]); + const cloneDashboard = ScriptField.MakeScript(`cloneDashboard()`); + dashboardDoc.contextMenuScripts = new List<ScriptField>([toggleTheme!, toggleComic!, cloneDashboard!]); + dashboardDoc.contextMenuLabels = new List<string>(["Toggle Theme Colors", "Toggle Comic Mode", "New Dashboard Layout"]); - Doc.AddDocToList(workspaces, "data", workspaceDoc); + Doc.AddDocToList(scens, "data", dashboardDoc); } // Button for switching between pen and ink mode @@ -438,7 +440,7 @@ export class MobileInterface extends React.Component { // DocButton that uses UndoManager and handles the opacity change if CanUndo is true @computed get undo() { if (this.mainContainer && this._activeDoc.type === "collection" && this._activeDoc !== this._homeDoc && - this._activeDoc !== Doc.UserDoc()["sidebar-sharing"] && this._activeDoc.title !== "WORKSPACES") { + this._activeDoc !== Doc.UserDoc().mySharedDocs && this._activeDoc.title !== "WORKSPACES") { return ( <div className="docButton" style={{ backgroundColor: "black", color: "white", fontSize: "60", opacity: UndoManager.CanUndo() ? "1" : "0.4", }} @@ -456,7 +458,7 @@ export class MobileInterface extends React.Component { // DocButton that uses UndoManager and handles the opacity change if CanRedo is true @computed get redo() { if (this.mainContainer && this._activeDoc.type === "collection" && this._activeDoc !== this._homeDoc && - this._activeDoc !== Doc.UserDoc()["sidebar-sharing"] && this._activeDoc.title !== "WORKSPACES") { + this._activeDoc !== Doc.UserDoc().mySharedDocs && this._activeDoc.title !== "WORKSPACES") { return ( <div className="docButton" style={{ backgroundColor: "black", color: "white", fontSize: "60", opacity: UndoManager.CanRedo() ? "1" : "0.4", }} @@ -514,7 +516,7 @@ export class MobileInterface extends React.Component { return <div className="docButton" title={Doc.isDocPinned(this._activeDoc) ? "Unpin from presentation" : "Pin to presentation"} style={{ backgroundColor: isPinned ? "black" : "white", color: isPinned ? "white" : "black" }} - onClick={e => DockedFrameRenderer.PinDoc(this._activeDoc, isPinned)}> + onClick={e => TabDocView.PinDoc(this._activeDoc, isPinned)}> <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="map-pin" /> </div>; } else return (null); @@ -607,12 +609,12 @@ export class MobileInterface extends React.Component { // Returns the image upload pop up @computed get uploadImage() { - const doc = !this._homeMenu ? this._activeDoc : Cast(Doc.UserDoc()["sidebar-sharing"], Doc) as Doc; + const doc = !this._homeMenu ? this._activeDoc : Cast(Doc.UserDoc().mySharedDocs, Doc) as Doc; return <Uploader Document={doc} />; } // Radial menu can only be used if it is a colleciton and it is not a homeDoc - // (and cannot be used on Workspace to avoid pin to presentation opening on right) + // (and cannot be used on Dashboard to avoid pin to presentation opening on right) @computed get displayRadialMenu() { return this._activeDoc.type === "collection" && this._activeDoc !== this._homeDoc && this._activeDoc._viewType !== CollectionViewType.Docking ? <RadialMenu /> : (null); @@ -630,7 +632,7 @@ export class MobileInterface extends React.Component { */ @action switchToMobileUploads = () => { - const mobileUpload = Cast(Doc.UserDoc()["sidebar-sharing"], Doc) as Doc; + const mobileUpload = Cast(Doc.UserDoc().mySharedDocs, Doc) as Doc; this.switchCurrentView(mobileUpload); this._homeMenu = false; } @@ -657,7 +659,7 @@ export class MobileInterface extends React.Component { {this.drawInk} {this.uploadImageButton} </div> - {this.displayWorkspaces} + {this.displayDashboards} {this.renderDefaultContent} </GestureOverlay> {this.displayRadialMenu} @@ -669,7 +671,7 @@ export class MobileInterface extends React.Component { //Global functions for mobile menu Scripting.addGlobal(function switchToMobileLibrary() { return MobileInterface.Instance.switchToLibrary(); }, - "opens the library to navigate through workspaces on Dash Mobile"); + "opens the library to navigate through dashboards on Dash Mobile"); Scripting.addGlobal(function openMobileUploads() { return MobileInterface.Instance.toggleUpload(); }, "opens the upload files menu for Dash Mobile"); Scripting.addGlobal(function switchToMobileUploadCollection() { return MobileInterface.Instance.switchToMobileUploads(); }, diff --git a/src/server/ApiManagers/SearchManager.ts b/src/server/ApiManagers/SearchManager.ts index a5aaab63e..775e90520 100644 --- a/src/server/ApiManagers/SearchManager.ts +++ b/src/server/ApiManagers/SearchManager.ts @@ -47,13 +47,20 @@ export class SearchManager extends ApiManager { const resObj: { ids: string[], numFound: number, lines: string[] } = { ids: [], numFound: 0, lines: [] }; let results: any; const dir = pathToDirectory(Directory.text); - results = await findInFiles.find({ 'term': q, 'flags': 'ig' }, dir, ".txt$"); - for (const result in results) { - resObj.ids.push(path.basename(result, ".txt").replace(/upload_/, "")); - resObj.lines.push(results[result].line); - resObj.numFound++; + try { + const regex = new RegExp(q.toString()); + results = await findInFiles.find({ 'term': q, 'flags': 'ig' }, dir, ".txt$"); + for (const result in results) { + resObj.ids.push(path.basename(result, ".txt").replace(/upload_/, "")); + resObj.lines.push(results[result].line); + resObj.numFound++; + } + res.send(resObj); + } catch (e) { + console.log(red("textsearch:bad RegExp" + q.toString())); + res.send([]); + return; } - res.send(resObj); } }); @@ -62,7 +69,7 @@ export class SearchManager extends ApiManager { subscription: "/dashsearch", secureHandler: async ({ req, res }) => { const solrQuery: any = {}; - ["q", "fq", "start", "rows", "sort", "hl", "hl.fl"].forEach(key => solrQuery[key] = req.query[key]); + ["q", "fq", "start", "rows", "sort", "hl.maxAnalyzedChars", "hl", "hl.fl"].forEach(key => solrQuery[key] = req.query[key]); if (solrQuery.q === undefined) { res.send([]); return; diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts index e088cd2c4..e98498489 100644 --- a/src/server/ApiManagers/UploadManager.ts +++ b/src/server/ApiManagers/UploadManager.ts @@ -9,7 +9,7 @@ import { publicDirectory, filesDirectory } from ".."; import { Database } from "../database"; import { DashUploadUtils, InjectSize, SizeSuffix } from "../DashUploadUtils"; import * as sharp from 'sharp'; -import { AcceptibleMedia, Upload } from "../SharedMediaTypes"; +import { AcceptableMedia, Upload } from "../SharedMediaTypes"; import { normalize } from "path"; import RouteSubscriber from "../RouteSubscriber"; const imageDataUri = require('image-data-uri'); @@ -55,7 +55,7 @@ export default class UploadManager extends ApiManager { const results: Upload.FileResponse[] = []; for (const key in files) { const result = await DashUploadUtils.upload(files[key]); - result && results.push(result); + result && !(result.result instanceof Error) && results.push(result); } _success(res, results); resolve(); @@ -237,7 +237,7 @@ export default class UploadManager extends ApiManager { } return imageDataUri.outputFile(uri, serverPathToFile(Directory.images, InjectSize(filename, SizeSuffix.Original))).then((savedName: string) => { const ext = extname(savedName).toLowerCase(); - const { pngs, jpgs } = AcceptibleMedia; + const { pngs, jpgs } = AcceptableMedia; const resizers = [ { resizer: sharp().resize(100, undefined, { withoutEnlargement: true }), suffix: "_s" }, { resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: "_m" }, diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts index 890fb6f6d..e4d0d1f5f 100644 --- a/src/server/DashUploadUtils.ts +++ b/src/server/DashUploadUtils.ts @@ -13,7 +13,7 @@ import { Utils } from '../Utils'; import { createIfNotExists } from './ActionUtilities'; import { clientPathToFile, Directory, pathToDirectory, serverPathToFile } from './ApiManagers/UploadManager'; import { resolvedServerUrl } from "./server_Initialization"; -import { AcceptibleMedia, Upload } from './SharedMediaTypes'; +import { AcceptableMedia, Upload } from './SharedMediaTypes'; import request = require('request-promise'); const parse = require('pdf-parse'); const requestImageSize = require("../client/util/request-image-size"); @@ -48,13 +48,13 @@ export namespace DashUploadUtils { }; export function validateExtension(url: string) { - return AcceptibleMedia.imageFormats.includes(path.extname(url).toLowerCase()); + return AcceptableMedia.imageFormats.includes(path.extname(url).toLowerCase()); } const size = "content-length"; const type = "content-type"; - const { imageFormats, videoFormats, applicationFormats, audioFormats } = AcceptibleMedia; + const { imageFormats, videoFormats, applicationFormats, audioFormats } = AcceptableMedia; export async function upload(file: File): Promise<Upload.FileResponse> { const { type, path, name } = file; @@ -313,7 +313,7 @@ export namespace DashUploadUtils { return { data, error }; }; - const { pngs, jpgs, webps, tiffs } = AcceptibleMedia; + const { pngs, jpgs, webps, tiffs } = AcceptableMedia; const pngOptions = { compressionLevel: 9, adaptiveFiltering: true, diff --git a/src/server/Search.ts b/src/server/Search.ts index 3869867cd..68f61deb2 100644 --- a/src/server/Search.ts +++ b/src/server/Search.ts @@ -29,9 +29,8 @@ export namespace Search { export async function search(query: any) { try { - const searchResults = JSON.parse(await rp.get(pathTo("select"), { - qs: query - })); + const output = await rp.get(pathTo("select"), { qs: query }); + const searchResults = JSON.parse(output); const { docs, numFound } = searchResults.response; const ids = docs.map((field: any) => field.id); return { ids, numFound, highlighting: searchResults.highlighting }; diff --git a/src/server/SharedMediaTypes.ts b/src/server/SharedMediaTypes.ts index 0f788f6c5..a341fd1c2 100644 --- a/src/server/SharedMediaTypes.ts +++ b/src/server/SharedMediaTypes.ts @@ -1,7 +1,7 @@ import { ExifData } from 'exif'; import { File } from 'formidable'; -export namespace AcceptibleMedia { +export namespace AcceptableMedia { export const gifs = [".gif"]; export const pngs = [".png"]; export const jpgs = [".jpg", ".jpeg"]; @@ -10,7 +10,7 @@ export namespace AcceptibleMedia { export const imageFormats = [...pngs, ...jpgs, ...gifs, ...webps, ...tiffs]; export const videoFormats = [".mov", ".mp4"]; export const applicationFormats = [".pdf"]; - export const audioFormats = [".wav", ".mp3", ".flac", ".au", ".aiff", ".m4a", ".webm"]; + export const audioFormats = [".wav", ".mp3", ".mpeg", ".flac", ".au", ".aiff", ".m4a", ".webm"]; } export namespace Upload { |