diff options
author | brynnchernosky <56202540+brynnchernosky@users.noreply.github.com> | 2023-01-19 14:33:22 -0500 |
---|---|---|
committer | brynnchernosky <56202540+brynnchernosky@users.noreply.github.com> | 2023-01-19 14:33:22 -0500 |
commit | 0ef7050b0792ce183c7d5cda637cb79b7a92b704 (patch) | |
tree | d1dca8f09ddc2954c2ce88439172aeded672c0b6 /src | |
parent | ceb338752aacc383c97a0e3a9b608365a1cf39b6 (diff) | |
parent | d5f796b433d7e72130d4109a3775347ccb10c454 (diff) |
Merge branch 'master' of github.com:brown-dash/Dash-Web into master
Diffstat (limited to 'src')
143 files changed, 4945 insertions, 5216 deletions
diff --git a/src/Utils.ts b/src/Utils.ts index 5e0514bc6..9d3b9eb2b 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -558,9 +558,13 @@ export namespace JSONUtils { } } -const easeInOutQuad = (currentTime: number, start: number, change: number, duration: number) => { - let newCurrentTime = currentTime / (duration / 2); +const easeFunc = (transition: 'ease' | 'linear' | undefined, currentTime: number, start: number, change: number, duration: number) => { + if (transition === 'linear') { + let newCurrentTime = currentTime / duration; // currentTime / (duration / 2); + return start + newCurrentTime * change; + } + let newCurrentTime = currentTime / (duration / 2); if (newCurrentTime < 1) { return (change / 2) * newCurrentTime * newCurrentTime + start; } @@ -569,23 +573,28 @@ const easeInOutQuad = (currentTime: number, start: number, change: number, durat return (-change / 2) * (newCurrentTime * (newCurrentTime - 2) - 1) + start; }; -export function smoothScroll(duration: number, element: HTMLElement | HTMLElement[], to: number) { +export function smoothScroll(duration: number, element: HTMLElement | HTMLElement[], to: number, transition: 'ease' | 'linear' | undefined, stopper?: () => void) { + stopper?.(); const elements = element instanceof HTMLElement ? [element] : element; const starts = elements.map(element => element.scrollTop); const startDate = new Date().getTime(); - + let _stop = false; + const stop = () => (_stop = true); const animateScroll = () => { const currentDate = new Date().getTime(); const currentTime = currentDate - startDate; - elements.map((element, i) => (element.scrollTop = easeInOutQuad(currentTime, starts[i], to - starts[i], duration))); + elements.map((element, i) => (element.scrollTop = easeFunc(transition, currentTime, starts[i], to - starts[i], duration))); - if (currentTime < duration) { - requestAnimationFrame(animateScroll); - } else { - elements.forEach(element => (element.scrollTop = to)); + if (!_stop) { + if (currentTime < duration) { + requestAnimationFrame(animateScroll); + } else { + elements.forEach(element => (element.scrollTop = to)); + } } }; animateScroll(); + return stop; } export function smoothScrollHorizontal(duration: number, element: HTMLElement | HTMLElement[], to: number) { @@ -596,7 +605,7 @@ export function smoothScrollHorizontal(duration: number, element: HTMLElement | const animateScroll = () => { const currentDate = new Date().getTime(); const currentTime = currentDate - startDate; - elements.map((element, i) => (element.scrollLeft = easeInOutQuad(currentTime, starts[i], to - starts[i], duration))); + elements.map((element, i) => (element.scrollLeft = easeFunc('ease', currentTime, starts[i], to - starts[i], duration))); if (currentTime < duration) { requestAnimationFrame(animateScroll); diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index 5a34fcf11..cab90138f 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -2,6 +2,7 @@ import { runInAction } from 'mobx'; import * as rp from 'request-promise'; import * as io from 'socket.io-client'; import { Doc, Opt, UpdatingFromServer } from '../fields/Doc'; +import { FieldLoader } from '../fields/FieldLoader'; import { HandleUpdate, Id, Parent } from '../fields/FieldSymbols'; import { ObjectField } from '../fields/ObjectField'; import { RefField } from '../fields/RefField'; @@ -29,15 +30,23 @@ export namespace DocServer { let _cache: { [id: string]: RefField | Promise<Opt<RefField>> } = {}; export function UPDATE_SERVER_CACHE(print: boolean = false) { - const strings: string[] = []; - Array.from(Object.keys(_cache)).forEach(key => { - const doc = _cache[key]; - if (doc instanceof Doc) strings.push(StrCast(doc.author) + ' ' + StrCast(doc.title) + ' ' + StrCast(Doc.GetT(doc, 'title', 'string', true))); + if (print) { + const strings: string[] = []; + Array.from(Object.keys(_cache)).forEach(key => { + const doc = _cache[key]; + if (doc instanceof Doc) strings.push(StrCast(doc.author) + ' ' + StrCast(doc.title) + ' ' + StrCast(Doc.GetT(doc, 'title', 'string', true))); + }); + strings.sort().forEach((str, i) => console.log(i.toString() + ' ' + str)); + } + const filtered = Array.from(Object.keys(_cache)).filter(key => { + const doc = _cache[key] as Doc; + if (!(StrCast(doc.author).includes('.edu') || StrCast(doc.author).includes('.com')) || doc.author == Doc.CurrentUserEmail) return true; + return false; }); - print && strings.sort().forEach((str, i) => console.log(i.toString() + ' ' + str)); + rp.post(Utils.prepend('/setCacheDocumentIds'), { body: { - cacheDocumentIds: Array.from(Object.keys(_cache)).join(';'), + cacheDocumentIds: filtered.join(';'), }, json: true, }); @@ -349,21 +358,31 @@ export namespace DocServer { // 2) synchronously, we emit a single callback to the server requesting the serialized (i.e. represented by a string) // fields for the given ids. This returns a promise, which, when resolved, indicates that all the JSON serialized versions of // the fields have been returned from the server + console.log('Requesting ' + requestedIds.length + ' fields'); + FieldLoader.active && runInAction(() => (FieldLoader.ServerLoadStatus.requested = requestedIds.length)); const getSerializedFields: Promise<any> = Utils.EmitCallback(_socket, MessageStore.GetRefFields, requestedIds); // 3) when the serialized RefFields have been received, go head and begin deserializing them into objects. // Here, once deserialized, we also invoke .proto to 'load' the documents' prototypes, which ensures that all // future .proto calls on the Doc won't have to go farther than the cache to get their actual value. - const deserializeFields = getSerializedFields.then(async fields => { - const fieldMap: { [id: string]: RefField } = {}; + + let retrieved = 0; + const fields: { [id: string]: RefField } = {}; + await getSerializedFields.then(async fieldvals => { + console.log('deserializing ' + fieldvals.length + ' fields'); const proms: Promise<void>[] = []; - runInAction(() => { - for (const field of fields) { + await runInAction(async () => { + for (const field of fieldvals) { const cached = _cache[field.id]; if (!cached) { + retrieved++; + if (FieldLoader.active && retrieved % 150 === 0) { + runInAction(() => (FieldLoader.ServerLoadStatus.retrieved = retrieved)); + await new Promise(res => setTimeout(res)); + } // deserialize - const prom = SerializationHelper.Deserialize(field).then(deserialized => { - fieldMap[field.id] = deserialized; + const prom = SerializationHelper.Deserialize(field).then(async deserialized => { + fields[field.id] = deserialized; //overwrite or delete any promises (that we inserted as flags // to indicate that the field was in the process of being fetched). Now everything @@ -385,29 +404,27 @@ export namespace DocServer { // adds to a list of promises that will be awaited asynchronously proms.push(prom); } else if (cached instanceof Promise) { + console.log('.'); proms.push(cached as any); - cached.then((f: any) => (fieldMap[field.id] = f)); + cached.then((f: any) => (fields[field.id] = f)); } else if (field) { + console.log('-'); proms.push(cached as any); - fieldMap[field.id] = DocServer.GetCachedRefField(field.id) || field; + fields[field.id] = DocServer.GetCachedRefField(field.id) || field; } } }); - await Promise.all(proms); - return fieldMap; + return Promise.all(proms); }); // 5) at this point, all fields have a) been returned from the server and b) been deserialized into actual Field objects whose // prototype documents, if any, have also been fetched and cached. - const fields = await deserializeFields; + console.log('Deserialized ' + Object.keys(fields).length + ' fields'); // 6) with this confidence, we can now go through and update the cache at the ids of the fields that // we explicitly had to fetch. To finish it off, we add whatever value we've come up with for a given // id to the soon-to-be-returned field mapping. - requestedIds.forEach(id => { - const field = fields[id]; - map[id] = field; - }); + requestedIds.forEach(id => (map[id] = fields[id])); } // 7) those promises we encountered in the else if of 1), which represent diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index bbb896d6e..b51fcb454 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -7,7 +7,6 @@ import { Id } from '../../fields/FieldSymbols'; import { HtmlField } from '../../fields/HtmlField'; import { InkField, PointData } from '../../fields/InkField'; import { List } from '../../fields/List'; -import { ProxyField } from '../../fields/Proxy'; import { RichTextField } from '../../fields/RichTextField'; import { SchemaHeaderField } from '../../fields/SchemaHeaderField'; import { ComputedField, ScriptField } from '../../fields/ScriptField'; @@ -37,7 +36,7 @@ import { FontIconBox } from '../views/nodes/button/FontIconBox'; import { ColorBox } from '../views/nodes/ColorBox'; import { ComparisonBox } from '../views/nodes/ComparisonBox'; import { DataVizBox } from '../views/nodes/DataVizBox/DataVizBox'; -import { DocFocusOptions } from '../views/nodes/DocumentView'; +import { DocFocusOptions, OpenWhere, OpenWhereMod } from '../views/nodes/DocumentView'; import { EquationBox } from '../views/nodes/EquationBox'; import { FieldViewProps } from '../views/nodes/FieldView'; import { FilterBox } from '../views/nodes/FilterBox'; @@ -157,7 +156,7 @@ export class DocumentOptions { _contentBounds?: List<number>; // the (forced) bounds of the document to display. format is: [left, top, right, bottom] _lockedPosition?: boolean; // lock the x,y coordinates of the document so that it can't be dragged _lockedTransform?: boolean; // lock the panx,pany and scale parameters of the document so that it be panned/zoomed - _isPushpin?: boolean; // whether document, when clicked, toggles display of its link target + _followLinkToggle?: boolean; // whether document, when clicked, toggles display of its link target _showTitle?: string; // field name to display in header (:hover is an optional suffix) _showCaption?: string; // which field to display in the caption area. leave empty to have no caption _scrollTop?: number; // scroll location for pdfs @@ -237,6 +236,7 @@ export class DocumentOptions { childContextMenuScripts?: List<ScriptField>; childContextMenuLabels?: List<string>; childContextMenuIcons?: List<string>; + followLinkZoom?: boolean; // whether to zoom to the target of a link hideLinkButton?: boolean; // whether the blue link counter button should be hidden hideDecorationTitle?: boolean; hideOpenButton?: boolean; @@ -257,11 +257,11 @@ export class DocumentOptions { 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 + viewTransitionTime?: number; // transition duration for view parameters presTransition?: number; //the time taken for the transition TO a document presDuration?: number; //the duration of the slide in presentation view - presProgressivize?: boolean; borderRounding?: string; - boxShadow?: string; + boxShadow?: string; // box-shadow css string OR "standard" to use dash standard box shadow data?: any; baseProto?: boolean; // is this a base prototoype dontRegisterView?: boolean; @@ -271,7 +271,7 @@ export class DocumentOptions { clipWidth?: number; // percent transition from before to after in comparisonBox dockingConfig?: string; annotationOn?: Doc; - isPushpin?: boolean; + followLinkToggle?: boolean; isGroup?: boolean; // whether a collection should use a grouping UI behavior _removeDropProperties?: List<string>; // list of properties that should be removed from a document when it is dropped. e.g., a creator button may be forceActive to allow it be dragged, but the forceActive property can be removed from the dropped document noteType?: string; @@ -298,7 +298,6 @@ export class DocumentOptions { linearViewExpandable?: boolean; // can linear view be expanded linearViewToggleButton?: string; // button to open close linear view group linearViewSubMenu?: boolean; - linearViewFloating?: boolean; flexGap?: number; // Linear view flex gap flexDirection?: 'unset' | 'row' | 'column' | 'row-reverse' | 'column-reverse'; @@ -673,8 +672,6 @@ export namespace Docs { * haven't been initialized, the newly initialized prototype document. */ export async function initialize(): Promise<void> { - ProxyField.initPlugin(); - ComputedField.initPlugin(); // non-guid string ids for each document prototype const prototypeIds = Object.values(DocumentType) .filter(type => type !== DocumentType.NONE) @@ -799,7 +796,7 @@ export namespace Docs { const viewKeys = ['x', 'y', 'system']; // keys that should be addded to the view document even though they don't begin with an "_" const { omit: dataProps, extract: viewProps } = OmitKeys(options, viewKeys, '^_'); - dataProps['acl-Override'] = 'None'; + // dataProps['acl-Override'] = SharingPermissions.Unset; dataProps['acl-Public'] = options['acl-Public'] ? options['acl-Public'] : Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment; dataProps.system = viewProps.system; @@ -825,7 +822,7 @@ export namespace Docs { const viewFirstProps: { [id: string]: any } = {}; viewFirstProps['acl-Public'] = options['_acl-Public'] ? options['_acl-Public'] : Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment; - viewFirstProps['acl-Override'] = 'None'; + // viewFirstProps['acl-Override'] = SharingPermissions.Unset; viewFirstProps.author = Doc.CurrentUserEmail; let viewDoc: Doc; // determines whether viewDoc should be created using placeholder Doc or default @@ -837,13 +834,11 @@ export namespace Docs { viewDoc = Doc.assign(Doc.MakeDelegate(dataDoc, delegId), viewFirstProps, true, true); } Doc.assign(viewDoc, viewProps, true, true); - ![DocumentType.LINK, DocumentType.MARKER, DocumentType.LABEL].includes(viewDoc.type as any) && DocUtils.MakeLinkToActiveAudio(() => viewDoc); + if (![DocumentType.LINK, DocumentType.MARKER, DocumentType.LABEL].includes(viewDoc.type as any)) { + DocUtils.MakeLinkToActiveAudio(() => viewDoc); + } - !Doc.IsSystem(dataDoc) && - ![DocumentType.MARKER, DocumentType.KVP, DocumentType.LINK, DocumentType.LINKANCHOR].includes(proto.type as any) && - !dataDoc.isFolder && - !dataProps.annotationOn && - Doc.AddDocToList(Doc.MyFileOrphans, undefined, dataDoc); + Doc.AddFileOrphan(dataDoc); updateCachedAcls(dataDoc); updateCachedAcls(viewDoc); @@ -995,7 +990,7 @@ export namespace Docs { I.data = new InkField(points); I.creationDate = new DateField(); I['acl-Public'] = Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment; - I['acl-Override'] = 'None'; + //I['acl-Override'] = SharingPermissions.Unset; I.links = ComputedField.MakeFunction('links(self)'); I[Initializing] = false; return I; @@ -1049,6 +1044,10 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.MARKER), options?.data, options, id); } + export function ImageanchorDocument(options: DocumentOptions = {}, id?: string) { + return InstanceFromProto(Prototypes.get(DocumentType.MARKER), options?.data, options, id); + } + export function HTMLAnchorDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) { return InstanceFromProto(Prototypes.get(DocumentType.MARKER), new List(documents), options, id); } @@ -1318,7 +1317,7 @@ export namespace DocUtils { return DocUtils.ActiveRecordings.map(audio => { const sourceDoc = getSourceDoc(); const link = sourceDoc && DocUtils.MakeLink({ doc: sourceDoc }, { doc: audio.getAnchor() || audio.props.Document }, 'recording annotation:linked recording', 'recording timeline'); - link && (link.followLinkLocation = 'add:right'); + link && (link.followLinkLocation = OpenWhere.addRight); return link; }); } @@ -1371,7 +1370,6 @@ export namespace DocUtils { 'acl-Public': SharingPermissions.Augment, '_acl-Public': SharingPermissions.Augment, linkDisplay: true, - _hidden: true, _linkAutoMove: true, linkRelationship, _showCaption: 'description', @@ -1384,11 +1382,12 @@ export namespace DocUtils { ); } - export function AssignScripts(doc: Doc, scripts?: { [key: string]: string }, funcs?: { [key: string]: string }) { + export function AssignScripts(doc: Doc, scripts?: { [key: string]: string | undefined }, funcs?: { [key: string]: string }) { scripts && Object.keys(scripts).map(key => { - if (ScriptCast(doc[key])?.script.originalScript !== scripts[key] && scripts[key]) { - doc[key] = ScriptField.MakeScript(scripts[key], { + const script = scripts[key]; + if (ScriptCast(doc[key])?.script.originalScript !== scripts[key] && script) { + doc[key] = ScriptField.MakeScript(script, { dragData: DragManager.DocumentDragData.name, value: 'any', _readOnly_: 'boolean', @@ -1607,7 +1606,7 @@ export namespace DocUtils { const iconViews = DocListCast(Cast(Doc.UserDoc()['template-icons'], Doc, null)?.data); const templBtns = DocListCast(Cast(Doc.UserDoc()['template-buttons'], Doc, null)?.data); const noteTypes = DocListCast(Cast(Doc.UserDoc()['template-notes'], Doc, null)?.data); - const clickFuncs = DocListCast(Cast(Doc.UserDoc().clickFuncs, Doc, null)?.data); + const clickFuncs = DocListCast(Cast(Doc.UserDoc()['template-clickFuncs'], Doc, null)?.data); const allTemplates = iconViews .concat(templBtns) .concat(noteTypes) @@ -1699,7 +1698,7 @@ export namespace DocUtils { } export function LeavePushpin(doc: Doc, annotationField: string) { - if (doc.isPushpin) return undefined; + if (doc.followLinkToggle) return undefined; const context = Cast(doc.context, Doc, null) ?? Cast(doc.annotationOn, Doc, null); const hasContextAnchor = DocListCast(doc.links).some(l => (l.anchor2 === doc && Cast(l.anchor1, Doc, null)?.annotationOn === context) || (l.anchor1 === doc && Cast(l.anchor2, Doc, null)?.annotationOn === context)); if (context && !hasContextAnchor && (context.type === DocumentType.VID || context.type === DocumentType.WEB || context.type === DocumentType.PDF || context.type === DocumentType.IMG)) { @@ -1707,7 +1706,7 @@ export namespace DocUtils { title: 'pushpin', label: '', annotationOn: Cast(doc.annotationOn, Doc, null), - isPushpin: true, + followLinkToggle: true, icon: 'map-pin', x: Cast(doc.x, 'number', null), y: Cast(doc.y, 'number', null), @@ -1753,14 +1752,14 @@ export namespace DocUtils { return dd; } - async function processFileupload(generatedDocuments: Doc[], name: string, type: string, result: Error | Upload.FileInformation, options: DocumentOptions, rootDoc?: Doc) { + async function processFileupload(generatedDocuments: Doc[], name: string, type: string, result: Error | Upload.FileInformation, options: DocumentOptions, overwriteDoc?: Doc) { if (result instanceof Error) { alert(`Upload failed: ${result.message}`); return; } const full = { ...options, _width: 400, title: name }; const pathname = Utils.prepend(result.accessPaths.agnostic.client); - const doc = await DocUtils.DocumentFromType(type, pathname, full, rootDoc); + const doc = await DocUtils.DocumentFromType(type, pathname, full, overwriteDoc); if (doc) { const proto = Doc.GetProto(doc); proto.text = result.rawText; @@ -1792,8 +1791,10 @@ export namespace DocUtils { if (Upload.isVideoInformation(result)) { proto['data-duration'] = result.duration; } - if (rootDoc) { - Doc.removeCurrentlyLoading(rootDoc); + if (overwriteDoc) { + Doc.removeCurrentlyLoading(overwriteDoc); + // loading doc icons are just labels. so any icon views of loading docs need to be replaced with the proper icon view. + DocumentManager.Instance.getAllDocumentViews(overwriteDoc).forEach(dv => StrCast(dv.rootDoc.layoutKey) === 'layout_icon' && dv.iconify(() => dv.iconify())); } generatedDocuments.push(doc); } @@ -1872,7 +1873,7 @@ export namespace DocUtils { export function copyDragFactory(dragFactory: Doc) { if (!dragFactory) return undefined; const ndoc = dragFactory.isTemplateDoc ? Doc.ApplyTemplate(dragFactory) : Doc.MakeCopy(dragFactory, true); - ndoc && Doc.AddDocToList(Doc.MyFileOrphans, 'data', Doc.GetProto(ndoc)); + ndoc && Doc.AddFileOrphan(Doc.GetProto(ndoc)); 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); @@ -1910,5 +1911,5 @@ ScriptingGlobals.add(function generateLinkTitle(self: Doc) { return `${anchor1title} (${relation}) ${anchor2title}`; }); ScriptingGlobals.add(function openTabAlias(tab: Doc) { - CollectionDockingView.AddSplit(Doc.MakeAlias(tab), 'right'); + CollectionDockingView.AddSplit(Doc.MakeAlias(tab), OpenWhereMod.right); }); diff --git a/src/client/goldenLayout.js b/src/client/goldenLayout.js index dd11e6466..9cb20d834 100644 --- a/src/client/goldenLayout.js +++ b/src/client/goldenLayout.js @@ -459,7 +459,7 @@ this._bDragging = false; this.emit('dragStop', oEvent, this._nOriginalX + this._nX); } else { // make title receive pointer events to allow setting insertion position or selecting texst range - const classname = typeof oEvent.target?.className === "string" ? oEvent.target.className : ""; + const classname = oEvent.target ? (typeof oEvent.target.className === "string" ? oEvent.target.className : ""): ""; if (classname.includes("lm_title_wrap")) { oEvent.target.children[0].style.pointerEvents = "all"; oEvent.target.children[0].focus(); @@ -5391,10 +5391,11 @@ * @returns {void} */ _render: function () { - this._reactComponent = ReactDOM.render(this._getReactComponent(), this._container.getElement()[0]); - this._originalComponentWillUpdate = this._reactComponent.componentWillUpdate || function () { + this._reactComponent = ReactDOM.createRoot(this._container.getElement()[0]) + this._reactComponent.render(this._getReactComponent()); + this._originalComponentWillUpdate = this._reactComponent.componentDidUpdate || function () { }; - this._reactComponent.componentWillUpdate = this._onUpdate.bind(this); + this._reactComponent.componentDidUpdate = this._onUpdate.bind(this); if (this._container.getState()) { this._reactComponent.setState(this._container.getState()); } @@ -5407,7 +5408,8 @@ * @returns {void} */ _destroy: function () { - ReactDOM.unmountComponentAtNode(this._container.getElement()[0]); + this._reactComponent.unmount(); + // ReactDOM.unmountComponentAtNode(this._container.getElement()[0]); this._container.off('open', this._render, this); this._container.off('destroy', this._destroy, this); }, diff --git a/src/client/util/CaptureManager.tsx b/src/client/util/CaptureManager.tsx index 0b5957fac..735b06f6d 100644 --- a/src/client/util/CaptureManager.tsx +++ b/src/client/util/CaptureManager.tsx @@ -1,19 +1,14 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable, runInAction } from 'mobx'; +import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { convertToObject } from 'typescript'; import { Doc, DocListCast } from '../../fields/Doc'; -import { BoolCast, StrCast, Cast } from '../../fields/Types'; -import { addStyleSheet, addStyleSheetRule, Utils } from '../../Utils'; +import { StrCast } from '../../fields/Types'; +import { addStyleSheet } from '../../Utils'; import { LightboxView } from '../views/LightboxView'; import { MainViewModal } from '../views/MainViewModal'; import './CaptureManager.scss'; import { SelectionManager } from './SelectionManager'; -import { undoBatch } from './UndoManager'; -const higflyout = require('@hig/flyout'); -export const { anchorPoints } = higflyout; -export const Flyout = higflyout.default; @observer export class CaptureManager extends React.Component<{}> { @@ -53,15 +48,8 @@ export class CaptureManager extends React.Component<{}> { const doc = this._document; const order: JSX.Element[] = []; if (doc) { - console.log('title', doc.title); - console.log('links', doc.links); - const linkDocs = DocListCast(doc.links); - 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 - linkDocs.forEach((l, i) => { + DocListCast(doc.links).forEach((l, i) => { if (l) { - console.log(i, (l.anchor1 as Doc).title); - console.log(i, (l.anchor2 as Doc).title); order.push( <div className="list-item"> <div className="number">{i}</div> diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index a2974177e..5f183cf91 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -1,15 +1,14 @@ -import { forOwn } from "lodash"; import { reaction } from "mobx"; import * as rp from 'request-promise'; import { Doc, DocListCast, DocListCastAsync, Opt } from "../../fields/Doc"; -import { Id } from "../../fields/FieldSymbols"; +import { FieldLoader } from "../../fields/FieldLoader"; import { InkTool } from "../../fields/InkField"; import { List } from "../../fields/List"; import { PrefetchProxy } from "../../fields/Proxy"; import { RichTextField } from "../../fields/RichTextField"; import { listSpec } from "../../fields/Schema"; -import { ComputedField, ScriptField } from "../../fields/ScriptField"; -import { Cast, DateCast, DocCast, PromiseValue, StrCast } from "../../fields/Types"; +import { ScriptField } from "../../fields/ScriptField"; +import { Cast, DateCast, DocCast, StrCast } from "../../fields/Types"; import { nullAudio } from "../../fields/URLField"; import { SetCachedGroups, SharingPermissions } from "../../fields/util"; import { GestureUtils } from "../../pen-gestures/GestureUtils"; @@ -23,7 +22,7 @@ import { Colors } from "../views/global/globalEnums"; import { MainView } from "../views/MainView"; import { ButtonType, NumButtonType } from "../views/nodes/button/FontIconBox"; import { OverlayView } from "../views/OverlayView"; -import { DragManager } from "./DragManager"; +import { DragManager, dropActionType } from "./DragManager"; import { MakeTemplate } from "./DropConverter"; import { LinkManager } from "./LinkManager"; import { ScriptingGlobals } from "./ScriptingGlobals"; @@ -214,7 +213,7 @@ export class CurrentUserUtils { title: string, toolTip: string, icon: string, ignoreClick?: boolean, dragFactory?: Doc, backgroundColor?: string, clickFactory?: Doc, scripts?: { onClick?: string, onDragStart?: string}, funcs?: {onDragStart?:string, hidden?: string}, }[] { - const standardOps = (key:string) => ({ title : "Untitled "+ key, _fitWidth: true, system: true, "dragFactory-count": 0, cloneFieldFilter: new List<string>(["system"]) }); + const standardOps = (key:string) => ({ title : "Untitled "+ key, _fitWidth: false, system: true, "dragFactory-count": 0, cloneFieldFilter: new List<string>(["system"]) }); const json = { doc: { type: "doc", @@ -260,19 +259,19 @@ export class CurrentUserUtils { {key: "Note", creator: opts => Docs.Create.TextDocument("", opts), opts: { _width: 200, _autoHeight: true }}, {key: "Noteboard", creator: opts => Docs.Create.NoteTakingDocument([], opts), opts: { _width: 250, _height: 200 }}, {key: "Collection", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 150, _height: 100 }}, - {key: "Equation", creator: opts => Docs.Create.EquationDocument(opts), opts: { _width: 300, _height: 35, _fitWidth:false, _backgroundGridShow: true, }}, + {key: "Equation", creator: opts => Docs.Create.EquationDocument(opts), opts: { _width: 300, _height: 35, _backgroundGridShow: true, }}, {key: "Webpage", creator: opts => Docs.Create.WebDocument("",opts), opts: { _width: 400, _height: 512, _nativeWidth: 850, useCors: true, }}, {key: "Comparison", creator: Docs.Create.ComparisonDocument, opts: { _width: 300, _height: 300 }}, {key: "Audio", creator: opts => Docs.Create.AudioDocument(nullAudio, opts),opts: { _width: 200, _height: 100, }}, {key: "Map", creator: opts => Docs.Create.MapDocument([], opts), opts: { _width: 800, _height: 600, _showSidebar: true, }}, {key: "Screengrab", creator: Docs.Create.ScreenshotDocument, opts: { _width: 400, _height: 200 }}, {key: "WebCam", creator: opts => Docs.Create.WebCamDocument("", opts), opts: { _width: 400, _height: 200, recording:true, system: true, cloneFieldFilter: new List<string>(["system"]) }}, - {key: "Button", creator: Docs.Create.ButtonDocument, opts: { _width: 150, _height: 50, _xPadding: 10, _yPadding: 10, }}, + {key: "Button", creator: Docs.Create.ButtonDocument, opts: { _width: 150, _height: 50, _xPadding: 10, _yPadding: 10, _isLinkButton: true }}, {key: "Script", creator: opts => Docs.Create.ScriptingDocument(null, opts), opts: { _width: 200, _height: 250, }}, // {key: "DataViz", creator: opts => Docs.Create.DataVizDocument(opts), opts: { _width: 300, _height: 300 }}, {key: "Header", creator: headerTemplate, opts: { _width: 300, _height: 70, _headerPointerEvents: "all", _headerHeight: 12, _headerFontSize: 9, _autoHeight: true,}}, - {key: "Trail", creator: Docs.Create.PresDocument, opts: { _width: 400, _height: 30, _viewType: CollectionViewType.Stacking, targetDropAction: "alias" as any, treeViewHideTitle: true, _chromeHidden: true, boxShadow: "0 0" }}, - {key: "Tab", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 500, _height: 800, _backgroundGridShow: true, }}, + {key: "Trail", creator: Docs.Create.PresDocument, opts: { _width: 400, _height: 30, _viewType: CollectionViewType.Stacking, targetDropAction: "alias" as any, treeViewHideTitle: true, _fitWidth:true, _chromeHidden: true, boxShadow: "0 0" }}, + {key: "Tab", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 500, _height: 800, _fitWidth: true, _backgroundGridShow: true, }}, {key: "Slide", creator: opts => Docs.Create.TreeDocument([], opts), opts: { _width: 300, _height: 200, _viewType: CollectionViewType.Tree, treeViewHasOverlay: true, _fontSize: "20px", _autoHeight: true, allowOverlayDrop: true, treeViewType: TreeViewType.outline, @@ -307,7 +306,7 @@ export class CurrentUserUtils { const creatorBtns = CurrentUserUtils.creatorBtnDescriptors(doc).map((reqdOpts) => { const btn = dragCreatorDoc ? DocListCast(dragCreatorDoc.data).find(doc => doc.title === reqdOpts.title): undefined; const opts:DocumentOptions = {...OmitKeys(reqdOpts, ["funcs", "scripts", "backgroundColor"]).omit, - _nativeWidth: 50, _nativeHeight: 50, _width: 35, _height: 35, _hideContextMenu: true, _stayInCollection: true, _dropAction: "alias", + _nativeWidth: 50, _nativeHeight: 50, _width: 35, _height: 35, _hideContextMenu: true, _stayInCollection: true, btnType: ButtonType.ToolButton, backgroundColor: reqdOpts.backgroundColor ?? Colors.DARK_GRAY, color: Colors.WHITE, system: true, _removeDropProperties: new List<string>(["_stayInCollection"]), }; @@ -317,7 +316,7 @@ export class CurrentUserUtils { const reqdOpts:DocumentOptions = { title: "Basic Item Creators", _showTitle: "title", _xMargin: 0, _stayInCollection: true, _hideContextMenu: true, _chromeHidden: true, system: true, _autoHeight: true, _width: 500, _height: 300, _fitWidth: true, _columnWidth: 40, ignoreClick: true, _lockedPosition: true, _forceActive: true, - childDocumentsActive: true + childDocumentsActive: true, childDropAction: 'alias' }; const reqdScripts = { dropConverter: "convertToButtons(dragData)" }; return DocUtils.AssignScripts(DocUtils.AssignOpts(dragCreatorDoc, reqdOpts, creatorBtns) ?? Docs.Create.MasonryDocument(creatorBtns, reqdOpts), reqdScripts); @@ -353,15 +352,15 @@ export class CurrentUserUtils { const btnDoc = myLeftSidebarMenu ? DocListCast(myLeftSidebarMenu.data).find(doc => doc.title === title) : undefined; const reqdBtnOpts:DocumentOptions = { title, icon, target, btnType: ButtonType.MenuButton, system: true, dontUndo: true, dontRegisterView: true, - _width: 60, _height: 60, _stayInCollection: true, _hideContextMenu: true, _chromeHidden: true, _dropAction: "alias", - _removeDropProperties: new List<string>(["dropAction", "_stayInCollection"]), + _width: 60, _height: 60, _stayInCollection: true, _hideContextMenu: true, _chromeHidden: true, + _removeDropProperties: new List<string>(["_stayInCollection"]), }; return DocUtils.AssignScripts(DocUtils.AssignOpts(btnDoc, reqdBtnOpts) ?? Docs.Create.FontIconDocument(reqdBtnOpts), scripts, funcs); }); const reqdStackOpts:DocumentOptions ={ - title: "menuItemPanel", childDropAction: "alias", backgroundColor: Colors.DARK_GRAY, boxShadow: "rgba(0,0,0,0)", dontRegisterView: true, ignoreClick: true, - _chromeHidden: true, _gridGap: 0, _yMargin: 0, _yPadding: 0, _xMargin: 0, _autoHeight: false, _width: 60, _columnWidth: 60, _lockedPosition: true, system: true + title: "menuItemPanel", childDropAction: "same", backgroundColor: Colors.DARK_GRAY, boxShadow: "rgba(0,0,0,0)", dontRegisterView: true, ignoreClick: true, + _chromeHidden: true, _gridGap: 0, _yMargin: 0, _yPadding: 0, _xMargin: 0, _autoHeight: false, _width: 60, _columnWidth: 60, _lockedPosition: true, system: true, }; return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(items??[], opts), reqdStackOpts, menuBtns, { dropConverter: "convertToButtons(dragData)" }); } @@ -398,14 +397,14 @@ export class CurrentUserUtils { // sets up the main document for the mobile button static mobileButton = (opts: DocumentOptions, docs: Doc[]) => Docs.Create.MulticolumnDocument(docs, { ...opts, - _removeDropProperties: new List<string>(["dropAction"]), _nativeWidth: 900, _nativeHeight: 250, _width: 900, _height: 250, _yMargin: 15, + _nativeWidth: 900, _nativeHeight: 250, _width: 900, _height: 250, _yMargin: 15, borderRounding: "5px", boxShadow: "0 0", system: true }) as any as Doc // sets up the text container for the information contained within the mobile button static mobileTextContainer = (opts: DocumentOptions, docs: Doc[]) => Docs.Create.MultirowDocument(docs, { ...opts, - _removeDropProperties: new List<string>(["dropAction"]), _nativeWidth: 450, _nativeHeight: 250, _width: 450, _height: 250, _yMargin: 25, + _nativeWidth: 450, _nativeHeight: 250, _width: 450, _height: 250, _yMargin: 25, backgroundColor: "rgba(0,0,0,0)", borderRounding: "0", boxShadow: "0 0", ignoreClick: true, system: true }) as any as Doc @@ -574,32 +573,36 @@ export class CurrentUserUtils { }) static createToolButton = (opts: DocumentOptions) => Docs.Create.FontIconDocument({ - btnType: ButtonType.ToolButton, _forceActive: true, _dropAction: "alias", _hideContextMenu: true, - _removeDropProperties: new List<string>(["_dropAction", "_hideContextMenu", "stayInCollection"]), + btnType: ButtonType.ToolButton, _forceActive: true, _hideContextMenu: true, + _removeDropProperties: new List<string>([ "_hideContextMenu", "stayInCollection"]), _nativeWidth: 40, _nativeHeight: 40, _width: 40, _height: 40, system: true, ...opts, }) /// initializes the required buttons in the expanding button menu at the bottom of the Dash window static setupDockedButtons(doc: Doc, field="myDockedBtns") { const dockedBtns = DocCast(doc[field]); - const dockBtn = (opts: DocumentOptions, scripts: {[key:string]:string}, funcs?: {[key:string]:string}) => + const dockBtn = (opts: DocumentOptions, scripts: {[key:string]:string|undefined}, funcs?: {[key:string]:string}) => DocUtils.AssignScripts(DocUtils.AssignOpts(DocListCast(dockedBtns?.data)?.find(doc => doc.title === opts.title), opts) ?? CurrentUserUtils.createToolButton(opts), scripts, funcs); const btnDescs = [// setup reactions to change the highlights on the undo/redo buttons -- would be better to encode this in the undo/redo buttons, but the undo/redo stacks are not wired up that way yet { scripts: { onClick: "undo()"}, opts: { title: "undo", icon: "undo-alt", toolTip: "Click to undo" }}, { scripts: { onClick: "redo()"}, opts: { title: "redo", icon: "redo-alt", toolTip: "Click to redo" }}, + { scripts: { }, opts: { title: "linker", icon: "linkui", toolTip: "link started"}}, + { scripts: { }, opts: { title: "currently playing", icon: "currentlyplayingui", toolTip: "currently playing audio"}}, // { scripts: { onClick: 'prevKeyFrame(_readOnly_)'}, opts: { title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton, width: 20}}, // { scripts: { onClick:""}, opts: { title: "Num", icon: "", toolTip: "Frame Number", btnType: ButtonType.TextButton, width: 20}, funcs: { buttonText: 'selectedDocs()?.lastElement()?.currentFrame.toString()'}}, // { scripts: { onClick: 'nextKeyFrame(_readOnly_)'}, opts:{title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton, width: 20,} }, ]; const btns = btnDescs.map(desc => dockBtn({_width: 30, _height: 30, dontUndo: true, _stayInCollection: true, ...desc.opts}, desc.scripts)); - const dockBtnsReqdOpts = { - title: "docked buttons", _height: 40, flexGap: 0, linearViewFloating: true, + const dockBtnsReqdOpts:DocumentOptions = { + title: "docked buttons", _height: 40, flexGap: 0, boxShadow: "standard", childDropAction: 'alias', childDontRegisterViews: true, linearViewIsExpanded: true, linearViewExpandable: true, ignoreClick: true }; reaction(() => UndoManager.redoStack.slice(), () => Doc.GetProto(btns.find(btn => btn.title === "redo")!).opacity = UndoManager.CanRedo() ? 1 : 0.4, { fireImmediately: true }); - reaction(() => UndoManager.undoStack.slice(), () => Doc.GetProto(btns.find(btn => btn.title === "undo")!).opacity = UndoManager.CanUndo() ? 1 : 0.4, { fireImmediately: true }); + reaction(() => UndoManager.undoStack.slice(), () => { + Doc.GetProto(btns.find(btn => btn.title === "undo")!).opacity = UndoManager.CanUndo() ? 1 : 0.4; + }, { fireImmediately: true }); return DocUtils.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), dockBtnsReqdOpts, btns); } @@ -665,7 +668,7 @@ export class CurrentUserUtils { title: "Perspective", toolTip: "View", btnType: ButtonType.DropdownList, ignoreClick: true, width: 100, scripts: { script: 'setView(value, _readOnly_)'}}, { title: "Pin", icon: "map-pin", toolTip: "Pin View to Trail", btnType: ButtonType.ClickButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "tab")'}, width: 20, scripts: { onClick: 'pinWithView(_readOnly_, altKey)'}}, { title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}}, - { title: "Num", icon: "", toolTip: "Frame Number", btnType: ButtonType.TextButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20, scripts: {}}, + { title: "Num",icon: "",toolTip: "Frame Number (click to toggle edit mode)",btnType: ButtonType.TextButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20, scripts: { script: '{ return curKeyFrame(_readOnly_);}'}}, { title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20, scripts: { onClick: 'nextKeyFrame(_readOnly_)'}}, { title: "Fill", icon: "fill-drip", toolTip: "Background Fill Color",btnType: ButtonType.ColorButton, funcs: {hidden: '!SelectionManager_selectedDocType()'}, ignoreClick: true, width: 20, scripts: { script: 'return setBackgroundColor(value, _readOnly_)'}}, // Only when a document is selected { title: "Header", icon: "heading", toolTip: "Header Color", btnType: ButtonType.ColorButton, funcs: {hidden: '!SelectionManager_selectedDocType()'}, ignoreClick: true, scripts: { script: 'return setHeaderColor(value, _readOnly_)'}}, @@ -686,7 +689,7 @@ export class CurrentUserUtils { _nativeWidth: params.width ?? 30, _width: params.width ?? 30, _height: 30, _nativeHeight: 30, _stayInCollection: true, _hideContextMenu: true, _lockedPosition: true, - _dropAction: "alias", _removeDropProperties: new List<string>(["dropAction", "_stayInCollection"]), + _removeDropProperties: new List<string>([ "_stayInCollection"]), }; const reqdFuncs:{[key:string]:any} = { ...params.funcs, @@ -697,7 +700,7 @@ export class CurrentUserUtils { /// Initializes all the default buttons for the top bar context menu static setupContextMenuButtons(doc: Doc, field="myContextMenuBtns") { - const reqdCtxtOpts = { title: "context menu buttons", flexGap: 0, childDontRegisterViews: true, linearViewIsExpanded: true, ignoreClick: true, linearViewExpandable: false, _height: 35 }; + const reqdCtxtOpts:DocumentOptions = { title: "context menu buttons", flexGap: 0, childDropAction: 'alias', childDontRegisterViews: true, linearViewIsExpanded: true, ignoreClick: true, linearViewExpandable: false, _height: 35 }; const ctxtMenuBtnsDoc = DocUtils.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), reqdCtxtOpts, undefined); const ctxtMenuBtns = CurrentUserUtils.contextMenuTools().map(params => { const menuBtnDoc = DocListCast(ctxtMenuBtnsDoc?.data).find(doc => doc.title === params.title); @@ -751,12 +754,13 @@ export class CurrentUserUtils { const sharedDocOpts:DocumentOptions = { title: "My Shared Docs", userColor: "rgb(202, 202, 202)", + isFolder:true, childContextMenuFilters: new List<ScriptField>([dashboardFilter!,]), childContextMenuScripts: new List<ScriptField>([addToDashboards!,]), childContextMenuLabels: new List<string>(["Add to Dashboards",]), childContextMenuIcons: new List<string>(["user-plus",]), "acl-Public": SharingPermissions.Augment, "_acl-Public": SharingPermissions.Augment, - childDropAction: "alias", system: true, contentPointerEvents: "all", childLimitHeight: 0, _yMargin: 0, _gridGap: 15, + childDropAction: "alias", system: true, contentPointerEvents: "all", childLimitHeight: 0, _yMargin: 0, _gridGap: 15, childDontRegisterViews:true, // NOTE: treeViewHideTitle & _showTitle is for a TreeView's editable title, _showTitle is for DocumentViews title bar _showTitle: "title", treeViewHideTitle: true, ignoreClick: true, _lockedPosition: true, boxShadow: "0 0", _chromeHidden: true, dontRegisterView: true, explainer: "This is where documents or dashboards that other users have shared with you will appear. To share a document or dashboard right click and select 'Share'" @@ -828,6 +832,8 @@ export class CurrentUserUtils { DocUtils.AssignDocField(doc, "globalScriptDatabase", (opts) => Docs.Prototypes.MainScriptDocument(), {}); DocUtils.AssignDocField(doc, "myHeaderBar", (opts) => Docs.Create.MulticolumnDocument([], opts), { title: "header bar", system: true }); // drop down panel at top of dashboard for stashing documents + Doc.AddDocToList(Doc.MyFilesystem, undefined, Doc.MySharedDocs) + if (doc.activeDashboard instanceof Doc) { // undefined means ColorScheme.Light until all CSS is updated with values for each color scheme (e.g., see MainView.scss, DocumentDecorations.scss) doc.activeDashboard.colorScheme = doc.activeDashboard.colorScheme === ColorScheme.Light ? undefined : doc.activeDashboard.colorScheme; @@ -866,10 +872,12 @@ export class CurrentUserUtils { if (result.cacheDocumentIds) { const ids = result.cacheDocumentIds.split(";"); - const batch = 10000; + const batch = 30000; + FieldLoader.active = true; for (let i = 0; i < ids.length; i = Math.min(ids.length, i+batch)) { await DocServer.GetRefFields(ids.slice(i, i+batch)); } + FieldLoader.active = false; } return result; } else { diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts index 0a61f3478..203d4ad62 100644 --- a/src/client/util/DictationManager.ts +++ b/src/client/util/DictationManager.ts @@ -11,7 +11,7 @@ import { Utils } from '../../Utils'; import { Docs } from '../documents/Documents'; import { DocumentType } from '../documents/DocumentTypes'; import { DictationOverlay } from '../views/DictationOverlay'; -import { DocumentView } from '../views/nodes/DocumentView'; +import { DocumentView, OpenWhere, OpenWhereMod } from '../views/nodes/DocumentView'; import { SelectionManager } from './SelectionManager'; import { UndoManager } from './UndoManager'; @@ -328,7 +328,7 @@ export namespace DictationManager { { action: (target: DocumentView) => { const kvp = Docs.Create.KVPDocument(target.props.Document, { _width: 300, _height: 300 }); - target.props.addDocTab(kvp, 'add:right'); + target.props.addDocTab(kvp, OpenWhere.addRight); }, }, ], @@ -345,7 +345,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, 'add:right'); + target.props.addDocTab(newBox, OpenWhere.addRight); }, }, ], diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index b046d950f..70fe7f2c0 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -1,19 +1,19 @@ -import { action, observable, runInAction } from 'mobx'; -import { Doc, DocListCast, DocListCastAsync, Opt } from '../../fields/Doc'; +import { action, observable, ObservableSet, runInAction } from 'mobx'; +import { AnimationSym, Doc, Opt } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; -import { Cast } from '../../fields/Types'; +import { listSpec } from '../../fields/Schema'; +import { Cast, DocCast, StrCast } from '../../fields/Types'; +import { AudioField } from '../../fields/URLField'; import { returnFalse } from '../../Utils'; import { DocumentType } from '../documents/DocumentTypes'; -import { LightboxView } from '../views/LightboxView'; -import { DocumentView, ViewAdjustment } from '../views/nodes/DocumentView'; -import { LinkAnchorBox } from '../views/nodes/LinkAnchorBox'; import { CollectionDockingView } from '../views/collections/CollectionDockingView'; import { CollectionFreeFormView } from '../views/collections/collectionFreeForm'; import { CollectionView } from '../views/collections/CollectionView'; +import { LightboxView } from '../views/LightboxView'; +import { DocFocusOptions, DocumentView, OpenWhereMod, ViewAdjustment } from '../views/nodes/DocumentView'; +import { LinkAnchorBox } from '../views/nodes/LinkAnchorBox'; import { ScriptingGlobals } from './ScriptingGlobals'; import { SelectionManager } from './SelectionManager'; -import { listSpec } from '../../fields/Schema'; -import { AudioField } from '../../fields/URLField'; const { Howl } = require('howler'); export class DocumentManager { @@ -31,26 +31,50 @@ export class DocumentManager { //private constructor so no other class can create a nodemanager private constructor() {} + private _viewRenderedCbs: { doc: Doc; func: (dv: DocumentView) => any }[] = []; + public AddViewRenderedCb = (doc: Doc, func: (dv: DocumentView) => any) => { + const dv = this.getDocumentViewById(doc[Id]); + this._viewRenderedCbs.push({ doc, func }); + if (dv) { + this.callAddViewFuncs(dv); + } + }; + callAddViewFuncs = (view: DocumentView) => { + const callFuncs = this._viewRenderedCbs.filter(vc => vc.doc === view.rootDoc); + if (callFuncs.length) { + this._viewRenderedCbs = this._viewRenderedCbs.filter(vc => !callFuncs.includes(vc)); + const intTimer = setInterval( + () => { + if (!view.ComponentView?.incrementalRendering?.()) { + callFuncs.forEach(cf => cf.func(view)); + clearInterval(intTimer); + } + }, + view.ComponentView?.incrementalRendering?.() ? 0 : 100 + ); + } + }; + @action public AddView = (view: DocumentView) => { //console.log("MOUNT " + view.props.Document.title + "/" + view.props.LayoutTemplateString); if (view.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) { const viewAnchorIndex = view.props.LayoutTemplateString.includes('anchor2') ? 'anchor2' : 'anchor1'; - DocListCast(view.rootDoc.links).forEach(link => { - this.LinkAnchorBoxViews?.filter(dv => Doc.AreProtosEqual(dv.rootDoc, link) && !dv.props.LayoutTemplateString?.includes(viewAnchorIndex)).forEach(otherView => - this.LinkedDocumentViews.push({ - a: viewAnchorIndex === 'anchor2' ? otherView : view, - b: viewAnchorIndex === 'anchor2' ? view : otherView, - l: link, - }) - ); - }); + const link = view.rootDoc; + this.LinkAnchorBoxViews?.filter(dv => Doc.AreProtosEqual(dv.rootDoc, link) && !dv.props.LayoutTemplateString?.includes(viewAnchorIndex)).forEach(otherView => + this.LinkedDocumentViews.push({ + a: viewAnchorIndex === 'anchor2' ? otherView : view, + b: viewAnchorIndex === 'anchor2' ? view : otherView, + l: link, + }) + ); this.LinkAnchorBoxViews.push(view); // this.LinkedDocumentViews.forEach(view => console.log(" LV = " + view.a.props.Document.title + "/" + view.a.props.LayoutTemplateString + " --> " + // view.b.props.Document.title + "/" + view.b.props.LayoutTemplateString)); } else { this.DocumentViews.add(view); } + this.callAddViewFuncs(view); }; public RemoveView = action((view: DocumentView) => { this.LinkedDocumentViews.slice().forEach( @@ -122,7 +146,14 @@ export class DocumentManager { } public getDocumentView(toFind: Doc, preferredCollection?: CollectionView): DocumentView | undefined { - return this.getDocumentViewById(toFind[Id], preferredCollection); + const found = + // Array.from(DocumentManager.Instance.DocumentViews).find( + // dv => + // ((dv.rootDoc.data as any)?.url?.href && (dv.rootDoc.data as any)?.url?.href === (toFind.data as any)?.url?.href) || + // ((DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href && (DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href === (DocCast(toFind.annotationOn)?.data as any)?.url?.href) + // )?.rootDoc ?? + toFind; + return this.getDocumentViewById(found[Id], preferredCollection); } public getLightboxDocumentView = (toFind: Doc, originatingDoc: Opt<Doc> = undefined): DocumentView | undefined => { @@ -131,10 +162,19 @@ export class DocumentManager { return views?.find(view => view.ContentDiv?.getBoundingClientRect().width && view.props.focus !== returnFalse) || views?.find(view => view.props.focus !== returnFalse) || (views.length ? views[0] : undefined); }; public getFirstDocumentView = (toFind: Doc, originatingDoc: Opt<Doc> = undefined): DocumentView | undefined => { + if (LightboxView.LightboxDoc) return DocumentManager.Instance.getLightboxDocumentView(toFind, originatingDoc); const views = this.getDocumentViews(toFind).filter(view => view.rootDoc !== originatingDoc); return views?.find(view => view.ContentDiv?.getBoundingClientRect().width && view.props.focus !== returnFalse) || views?.find(view => view.props.focus !== returnFalse) || (views.length ? views[0] : undefined); }; - public getDocumentViews(toFind: Doc): DocumentView[] { + public getDocumentViews(toFindIn: Doc): DocumentView[] { + const toFind = + // Array.from(DocumentManager.Instance.DocumentViews).find( + // dv => + // ((dv.rootDoc.data as any)?.url?.href && (dv.rootDoc.data as any)?.url?.href === (toFindIn.data as any)?.url?.href) || + // ((DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href && (DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href === (DocCast(toFindIn.annotationOn)?.data as any)?.url?.href) + // )?.rootDoc ?? + toFindIn; + const toReturn: DocumentView[] = []; const docViews = Array.from(DocumentManager.Instance.DocumentViews).filter(view => !LightboxView.IsLightboxDocView(view.docViewPath)); const lightViews = Array.from(DocumentManager.Instance.DocumentViews).filter(view => LightboxView.IsLightboxDocView(view.docViewPath)); @@ -150,26 +190,39 @@ export class DocumentManager { return toReturn; } + static playAudioAnno(doc: Doc) { + const anno = Cast(doc[Doc.LayoutFieldKey(doc) + '-audioAnnotations'], listSpec(AudioField), null)?.lastElement(); + if (anno) { + if (anno instanceof AudioField) { + new Howl({ + src: [anno.url.href], + format: ['mp3'], + autoplay: true, + loop: false, + volume: 0.5, + }); + } + } + } + + public static removeOverlayViews() { + DocumentManager._overlayViews?.forEach(action(view => (view.textHtmlOverlay = undefined))); + DocumentManager._overlayViews?.clear(); + } + static _overlayViews = new ObservableSet<DocumentView>(); static addView = (doc: Doc, finished?: () => void) => { - CollectionDockingView.AddSplit(doc, 'right'); + CollectionDockingView.AddSplit(doc, OpenWhereMod.right); finished?.(); }; public jumpToDocument = ( targetDoc: Doc, // document to display - willZoom: boolean, // whether to zoom doc to take up most of screen + options: DocFocusOptions, // options for how to navigate to target createViewFunc = DocumentManager.addView, // how to create a view of the doc if it doesn't exist - docContext: Doc[], // context to load that should contain the target - linkDoc?: Doc, // link that's being followed - closeContextIfNotFound: boolean = false, // after opening a context where the document should be, this determines whether the context should be closed if the Doc isn't actually there - originatingDoc: Opt<Doc> = undefined, // doc that initiated the display of the target odoc - finished?: () => void, - originalTarget?: Doc, - noSelect?: boolean, - presZoomScale?: number + docContextPath: Doc[], // context to load that should contain the target + finished?: () => void ): void => { - originalTarget = originalTarget ?? targetDoc; - const getFirstDocView = LightboxView.LightboxDoc ? DocumentManager.Instance.getLightboxDocumentView : DocumentManager.Instance.getFirstDocumentView; - const docView = getFirstDocView(targetDoc, originatingDoc); + const originalTarget = options.originalTarget ?? targetDoc; + const docView = this.getFirstDocumentView(targetDoc, options.originatingDoc); const annotatedDoc = Cast(targetDoc.annotationOn, Doc, null); const resolvedTarget = targetDoc.type === DocumentType.MARKER ? annotatedDoc ?? docView?.rootDoc ?? targetDoc : docView?.rootDoc ?? targetDoc; // if target is a marker, then focus toggling should apply to the document it's on since the marker itself doesn't have a hidden field var wasHidden = resolvedTarget.hidden; @@ -179,65 +232,63 @@ export class DocumentManager { docView?.props.bringToFront(resolvedTarget); }); } - const focusAndFinish = (didFocus: boolean) => { + const focusAndFinish = action((didFocus: boolean) => { const finalTargetDoc = resolvedTarget; - if (originatingDoc?.isPushpin) { + if (options.toggleTarget) { if (!didFocus && !wasHidden) { // don't toggle the hidden state if the doc was already un-hidden as part of this document traversal finalTargetDoc.hidden = !finalTargetDoc.hidden; } } else { finalTargetDoc.hidden && (finalTargetDoc.hidden = undefined); - !noSelect && docView?.select(false); - if (originatingDoc?.followLinkAudio) { - const anno = Cast(finalTargetDoc[Doc.LayoutFieldKey(finalTargetDoc) + '-audioAnnotations'], listSpec(AudioField), null).lastElement(); - if (anno) { - if (anno instanceof AudioField) { - new Howl({ - src: [anno.url.href], - format: ['mp3'], - autoplay: true, - loop: false, - volume: 0.5, - }); - } - } + !options.noSelect && docView?.select(false); + } + if (targetDoc.textHtml && options.zoomTextSelections) { + const containerView = DocumentManager.Instance.getFirstDocumentView(finalTargetDoc); + if (containerView) { + containerView.htmlOverlayEffect = StrCast(options?.effect?.presEffect, StrCast(options?.effect?.followLinkAnimEffect)); + containerView.textHtmlOverlay = StrCast(targetDoc.textHtml); + DocumentManager._overlayViews.add(containerView); + if (Doc.UnhighlightTimer) { + Doc.AddUnHighlightWatcher(() => { + DocumentManager.removeOverlayViews(); + containerView.htmlOverlayEffect = ''; + }); + } else setTimeout(() => (containerView.htmlOverlayEffect = '')); } } finished?.(); - }; - const annoContainerView = (!wasHidden || resolvedTarget !== annotatedDoc) && annotatedDoc && getFirstDocView(annotatedDoc); - const contextDocs = docContext.length ? DocListCast(docContext[0].data) : undefined; - const contextDoc = contextDocs?.find(doc => Doc.AreProtosEqual(doc, targetDoc) || Doc.AreProtosEqual(doc, annotatedDoc)) ? docContext.lastElement() : undefined; - const targetDocContext = contextDoc || annotatedDoc; - const targetDocContextView = (targetDocContext && getFirstDocView(targetDocContext)) || (wasHidden && annoContainerView); // if we have an annotation container and the target was hidden, then try again because we just un-hid the document above - const focusView = !docView && targetDoc.type === DocumentType.MARKER && annoContainerView ? annoContainerView : docView; + }); + const annoContainerView = (!wasHidden || resolvedTarget !== annotatedDoc) && annotatedDoc && this.getFirstDocumentView(annotatedDoc); if (annoContainerView) { if (annoContainerView.props.Document.layoutKey === 'layout_icon') { - annoContainerView.iconify(() => - annoContainerView.focus(targetDoc, { - originalTarget, - willZoom, - scale: presZoomScale, - afterFocus: (didFocus: boolean) => - new Promise<ViewAdjustment>(res => { - focusAndFinish(true); - res(ViewAdjustment.doNothing); - }), - }) - ); - return; - } else if (!docView && targetDoc.type !== DocumentType.MARKER) { + return annoContainerView.iconify(() => DocumentManager.Instance.AddViewRenderedCb(targetDoc, () => this.jumpToDocument(targetDoc, { ...options, originalTarget, toggleTarget: false }, createViewFunc, docContextPath, finished)), 30); + } + if (!docView && targetDoc.type !== DocumentType.MARKER) { annoContainerView.focus(targetDoc, {}); // this allows something like a PDF view to remove its doc filters to expose the target so that it can be found in the retry code below } } + + const contextDoc = docContextPath.length ? docContextPath[0] : undefined; + const remainingDocContext = docContextPath.length ? docContextPath.slice(1) : []; + const targetDocContext = contextDoc || annotatedDoc; + const targetDocContextView = (targetDocContext && this.getFirstDocumentView(targetDocContext)) || (wasHidden && annoContainerView); // if we have an annotation container and the target was hidden, then try again because we just un-hid the document above + const focusView = !docView && targetDoc.type === DocumentType.MARKER && annoContainerView ? annoContainerView : docView; if (focusView) { - !noSelect && Doc.linkFollowHighlight(focusView.rootDoc); //TODO:glr make this a setting in PresBox + if (focusView.rootDoc === originalTarget) { + if (!options.noSelect) Doc.linkFollowHighlight(focusView.rootDoc, undefined, options.effect); //TODO:glr make this a setting in PresBox + else { + focusView.rootDoc[AnimationSym] = options.effect; + if (Doc.UnhighlightTimer) { + Doc.AddUnHighlightWatcher(action(() => (focusView.rootDoc[AnimationSym] = undefined))); + } + } + } + if (options.playAudio) DocumentManager.playAudioAnno(focusView.rootDoc); const doFocus = (forceDidFocus: boolean) => - focusView.focus(originalTarget ?? targetDoc, { + focusView.focus(originalTarget, { + ...options, originalTarget, - willZoom, - scale: presZoomScale, afterFocus: (didFocus: boolean) => new Promise<ViewAdjustment>(res => { focusAndFinish(forceDidFocus || didFocus); @@ -252,75 +303,60 @@ export class DocumentManager { } else { if (!targetDocContext) { // we don't have a view and there's no context specified ... create a new view of the target using the dockFunc or default - createViewFunc(Doc.BrushDoc(targetDoc), finished); // bcz: should we use this?: Doc.MakeAlias(targetDoc))); + createViewFunc(Doc.BrushDoc(targetDoc), () => focusAndFinish(true)); // bcz: should we use this?: Doc.MakeAlias(targetDoc))); } else { // otherwise try to get a view of the context of the target if (targetDocContextView) { // we found a context view and aren't forced to create a new one ... focus on the context first.. wasHidden = wasHidden || targetDocContextView.rootDoc.hidden; targetDocContextView.rootDoc.hidden = false; // make sure context isn't hidden - targetDocContext._viewTransition = 'transform 500ms'; + + if (targetDocContext.layoutKey === 'layout_icon') { + return targetDocContextView.iconify( + () => DocumentManager.Instance.AddViewRenderedCb(targetDoc, () => this.jumpToDocument(resolvedTarget ?? targetDoc, { ...options /* originalTarget - needed? */ }, createViewFunc, docContextPath, finished)), + 30 + ); + } + + const contextFocusTime = options.zoomTime ? options.zoomTime / 2 : 500; + const remainingFocustime = options.zoomTime ? options.zoomTime - contextFocusTime : undefined; + targetDocContextView.setViewTransition('transform', contextFocusTime); + // this makes focusing on contexts run in parallel -- jutmp to document below makes them run sequentially + this.AddViewRenderedCb(targetDoc, () => this.jumpToDocument(targetDoc, { ...options, zoomTime: remainingFocustime }, createViewFunc, remainingDocContext, finished)); targetDocContextView.props.focus(targetDocContextView.rootDoc, { - willZoom, + ...options, + zoomTime: contextFocusTime, + // originalTarget, // needed? afterFocus: async () => { - targetDocContext._viewTransition = undefined; - if (targetDocContext.layoutKey === 'layout_icon') { - targetDocContextView.iconify(() => - this.jumpToDocument(resolvedTarget ?? targetDoc, willZoom, createViewFunc, docContext, linkDoc, closeContextIfNotFound, originatingDoc, finished, originalTarget, noSelect, presZoomScale) - ); - } - return ViewAdjustment.doNothing; - }, - }); - - // now find the target document within the context - if (targetDoc._timecodeToShow) { - // if the target has a timecode, it should show up once the (presumed) video context scrubs to the display timecode; - targetDocContext._currentTimecode = targetDoc.anchorTimecodeToShow; - finished?.(); - } else { - // no timecode means we need to find the context view and focus on our target - const findView = (delay: number) => { - const retryDocView = getFirstDocView(resolvedTarget); // 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. - Doc.linkFollowHighlight(retryDocView.rootDoc); - retryDocView.focus(targetDoc, { - willZoom, - afterFocus: (didFocus: boolean) => - new Promise<ViewAdjustment>(res => { - !noSelect && focusAndFinish(true); - res(ViewAdjustment.doNothing); - }), - }); // focus on the target in the context - } else if (delay > 1000) { - // 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.rootDoc); - if (targetDoc.layout) { + // now find the target document within the context + if (targetDoc._timecodeToShow) { + // if the target has a timecode, it should show up once the (presumed) video context scrubs to the display timecode; + targetDocContext._currentTimecode = targetDoc.anchorTimecodeToShow; + finished?.(); + } else { + // otherwise, just look for the target document in this context view now that we've focused the context view + if (this.getFirstDocumentView(resolvedTarget)) { + // test again for the target view snce we presumably created the context above by focusing on it + this.jumpToDocument(targetDoc, { ...options, zoomTime: remainingFocustime }, createViewFunc, remainingDocContext, finished); + } else if (targetDoc.layout) { // there will no layout for a TEXTANCHOR type document createViewFunc(Doc.BrushDoc(targetDoc), finished); // create a new view of the target } - } else { - setTimeout(() => findView(delay + 200), 200); } - }; - setTimeout(() => findView(0), 0); - } + return ViewAdjustment.doNothing; + }, + }); } else { - if (docContext.length && docContext[0]?.layoutKey === 'layout_icon') { - const docContextView = this.getFirstDocumentView(docContext[0]); - if (docContextView) { - return docContextView.iconify(() => - this.jumpToDocument(targetDoc, willZoom, createViewFunc, docContext.slice(1, docContext.length), linkDoc, closeContextIfNotFound, originatingDoc, finished, originalTarget, noSelect, presZoomScale) - ); - } + if (docContextPath.length && docContextPath[0]?.layoutKey === 'layout_icon') { + Doc.deiconifyView(docContextPath[0]); + this.jumpToDocument(targetDoc, options, createViewFunc, docContextPath, finished); + } 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, { ...options }, (doc: Doc, finished?: () => void) => doc !== targetDocContext && createViewFunc(doc, finished), remainingDocContext, finished) + ); } - // there's no context view so we need to create one first and try again when that finishes - const finishFunc = () => this.jumpToDocument(targetDoc, true, createViewFunc, docContext, linkDoc, true /* if we don't find the target, we want to get rid of the context just created */, undefined, finished, originalTarget); - createViewFunc( - targetDocContext, // after creating the context, this calls the finish function that will retry looking for the target - finishFunc - ); } } } @@ -330,12 +366,12 @@ export function DocFocusOrOpen(doc: Doc, collectionDoc?: Doc) { const cv = collectionDoc && DocumentManager.Instance.getDocumentView(collectionDoc); const dv = DocumentManager.Instance.getDocumentView(doc, (cv?.ComponentView as CollectionFreeFormView)?.props.CollectionView); if (dv && Doc.AreProtosEqual(dv.props.Document, doc)) { - dv.props.focus(dv.props.Document, { willZoom: true }); + dv.props.focus(dv.props.Document, { willPanZoom: true }); Doc.linkFollowHighlight(dv?.props.Document, false); } else { const context = doc.context !== Doc.MyFilesystem && Cast(doc.context, Doc, null); const showDoc = context || doc; - CollectionDockingView.AddSplit(Doc.BestAlias(showDoc), 'right') && context && setTimeout(() => DocumentManager.Instance.getDocumentView(Doc.GetProto(doc))?.focus(doc, {})); + CollectionDockingView.AddSplit(Doc.BestAlias(showDoc), OpenWhereMod.right) && context && setTimeout(() => DocumentManager.Instance.getDocumentView(Doc.GetProto(doc))?.focus(doc, {})); } } ScriptingGlobals.add(DocFocusOrOpen); diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 36e5a65fb..d0690fa10 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -450,7 +450,7 @@ export namespace DragManager { const hideDragShowOriginalElements = (hide: boolean) => { dragLabel.style.display = hide ? '' : 'none'; !hide && dragElements.map(dragElement => dragElement.parentNode === dragDiv && dragDiv.removeChild(dragElement)); - eles.forEach(ele => (ele.hidden = hide)); + setTimeout(() => eles.forEach(ele => (ele.hidden = hide))); }; options?.hideSource && hideDragShowOriginalElements(true); diff --git a/src/client/util/HypothesisUtils.ts b/src/client/util/HypothesisUtils.ts index e910a9118..297520d5d 100644 --- a/src/client/util/HypothesisUtils.ts +++ b/src/client/util/HypothesisUtils.ts @@ -1,30 +1,28 @@ -import { StrCast, Cast } from "../../fields/Types"; -import { SearchUtil } from "./SearchUtil"; -import { action, runInAction } from "mobx"; -import { Doc, Opt } from "../../fields/Doc"; -import { DocumentType } from "../documents/DocumentTypes"; -import { Docs } from "../documents/Documents"; -import { SelectionManager } from "./SelectionManager"; -import { WebField } from "../../fields/URLField"; -import { DocumentManager } from "./DocumentManager"; -import { DocumentLinksButton } from "../views/nodes/DocumentLinksButton"; -import { simulateMouseClick, Utils } from "../../Utils"; -import { DocumentView } from "../views/nodes/DocumentView"; -import { Id } from "../../fields/FieldSymbols"; +import { StrCast, Cast } from '../../fields/Types'; +import { SearchUtil } from './SearchUtil'; +import { action, runInAction } from 'mobx'; +import { Doc, Opt } from '../../fields/Doc'; +import { DocumentType } from '../documents/DocumentTypes'; +import { Docs } from '../documents/Documents'; +import { SelectionManager } from './SelectionManager'; +import { WebField } from '../../fields/URLField'; +import { DocumentManager } from './DocumentManager'; +import { DocumentLinksButton } from '../views/nodes/DocumentLinksButton'; +import { simulateMouseClick, Utils } from '../../Utils'; +import { DocumentView } from '../views/nodes/DocumentView'; +import { Id } from '../../fields/FieldSymbols'; export namespace Hypothesis { - /** - * Retrieve a WebDocument with the given url, prioritizing results that are on screen. + * Retrieve a WebDocument with the given url, prioritizing results that are on screen. * If none exist, create and return a new WebDocument. */ export const getSourceWebDoc = async (uri: string) => { const result = await findWebDoc(uri); - console.log(result ? "existing doc found" : "existing doc NOT found"); + console.log(result ? 'existing doc found' : 'existing doc NOT found'); return result || Docs.Create.WebDocument(uri, { title: uri, _nativeWidth: 850, _height: 512, _width: 400, useCors: true }); // create and return a new Web doc with given uri if no matching docs are found }; - /** * Search for a WebDocument whose url field matches the given uri, return undefined if not found */ @@ -33,18 +31,18 @@ export namespace Hypothesis { if (currentDoc && Cast(currentDoc.data, WebField)?.url.href === uri) return currentDoc; // always check first whether the currently selected doc is the annotation's source, only use Search otherwise const results: Doc[] = []; - await SearchUtil.Search("web", true).then(action(async (res: SearchUtil.DocSearchResult) => { - const docs = res.docs; - const filteredDocs = docs.filter(doc => - doc.author === Doc.CurrentUserEmail && doc.type === DocumentType.WEB && doc.data - ); - filteredDocs.forEach(doc => { - uri === Cast(doc.data, WebField)?.url.href && results.push(doc); // TODO check visited sites history? - }); - })); + await SearchUtil.Search('web', true).then( + action(async (res: SearchUtil.DocSearchResult) => { + const docs = res.docs; + const filteredDocs = docs.filter(doc => doc.author === Doc.CurrentUserEmail && doc.type === DocumentType.WEB && doc.data); + filteredDocs.forEach(doc => { + uri === Cast(doc.data, WebField)?.url.href && results.push(doc); // TODO check visited sites history? + }); + }) + ); const onScreenResults = results.filter(doc => DocumentManager.Instance.getFirstDocumentView(doc)); - return onScreenResults.length ? onScreenResults[0] : (results.length ? results[0] : undefined); // prioritize results that are currently on the screen + return onScreenResults.length ? onScreenResults[0] : results.length ? results[0] : undefined; // prioritize results that are currently on the screen }; /** @@ -52,17 +50,19 @@ export namespace Hypothesis { */ export const linkListener = async (e: any) => { const annotationId: string = e.detail.id; - const annotationUri: string = StrCast(e.detail.uri).split("#annotations:")[0]; // clean hypothes.is URLs that reference a specific annotation + const annotationUri: string = StrCast(e.detail.uri).split('#annotations:')[0]; // clean hypothes.is URLs that reference a specific annotation const sourceDoc: Doc = await getSourceWebDoc(annotationUri); - if (!DocumentLinksButton.StartLink || sourceDoc === DocumentLinksButton.StartLink) { // start new link if there were none already started, or if the old startLink came from the same web document (prevent links to itself) + if (!DocumentLinksButton.StartLink || sourceDoc === DocumentLinksButton.StartLink) { + // start new link if there were none already started, or if the old startLink came from the same web document (prevent links to itself) runInAction(() => { 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 + } else { + // if a link has already been started, complete the link to sourceDoc runInAction(() => { DocumentLinksButton.AnnotationId = annotationId; DocumentLinksButton.AnnotationUri = annotationUri; @@ -81,31 +81,41 @@ export namespace Hypothesis { export const makeLink = async (title: string, url: string, annotationId: string, annotationSourceDoc: Doc) => { // if the annotation's source webpage isn't currently loaded in Dash, we're not able to access and edit the annotation from the client // so we're loading the webpage and its annotations invisibly in a WebBox in MainView.tsx, until the editing is done - !DocumentManager.Instance.getFirstDocumentView(annotationSourceDoc) && runInAction(() => DocumentLinksButton.invisibleWebDoc = annotationSourceDoc); + //!DocumentManager.Instance.getFirstDocumentView(annotationSourceDoc) && runInAction(() => DocumentLinksButton.invisibleWebDoc = annotationSourceDoc); var success = false; const onSuccess = action(() => { - console.log("Edit success!!"); + console.log('Edit success!!'); success = true; clearTimeout(interval); - DocumentLinksButton.invisibleWebDoc = undefined; - document.removeEventListener("editSuccess", onSuccess); + //DocumentLinksButton.invisibleWebDoc = undefined; + document.removeEventListener('editSuccess', onSuccess); }); const newHyperlink = `[${title}\n](${url})`; - const interval = setInterval(() => // keep trying to edit until annotations have loaded and editing is successful - !success && document.dispatchEvent(new CustomEvent<{ newHyperlink: string, id: string }>("addLink", { - detail: { newHyperlink: newHyperlink, id: annotationId }, - bubbles: true - })), 300); - - setTimeout(action(() => { - if (!success) { - clearInterval(interval); - DocumentLinksButton.invisibleWebDoc = undefined; - } - }), 10000); // give up if no success after 10s - document.addEventListener("editSuccess", onSuccess); + const interval = setInterval( + () => + // keep trying to edit until annotations have loaded and editing is successful + !success && + document.dispatchEvent( + new CustomEvent<{ newHyperlink: string; id: string }>('addLink', { + detail: { newHyperlink: newHyperlink, id: annotationId }, + bubbles: true, + }) + ), + 300 + ); + + setTimeout( + action(() => { + if (!success) { + clearInterval(interval); + //DocumentLinksButton.invisibleWebDoc = undefined; + } + }), + 10000 + ); // give up if no success after 10s + document.addEventListener('editSuccess', onSuccess); }; /** @@ -114,33 +124,40 @@ export namespace Hypothesis { export const deleteLink = async (linkDoc: Doc, sourceDoc: Doc, destinationDoc: Doc) => { if (Cast(destinationDoc.data, WebField)?.url.href !== StrCast(linkDoc.annotationUri)) return; // check that the destinationDoc is a WebDocument containing the target annotation - !DocumentManager.Instance.getFirstDocumentView(destinationDoc) && runInAction(() => DocumentLinksButton.invisibleWebDoc = destinationDoc); // see note in makeLink + //!DocumentManager.Instance.getFirstDocumentView(destinationDoc) && runInAction(() => DocumentLinksButton.invisibleWebDoc = destinationDoc); // see note in makeLink var success = false; const onSuccess = action(() => { - console.log("Edit success!"); + console.log('Edit success!'); success = true; clearTimeout(interval); - DocumentLinksButton.invisibleWebDoc = undefined; - document.removeEventListener("editSuccess", onSuccess); + // DocumentLinksButton.invisibleWebDoc = undefined; + document.removeEventListener('editSuccess', onSuccess); }); const annotationId = StrCast(linkDoc.annotationId); const linkUrl = Doc.globalServerPath(sourceDoc); - const interval = setInterval(() => {// keep trying to edit until annotations have loaded and editing is successful - !success && document.dispatchEvent(new CustomEvent<{ targetUrl: string, id: string }>("deleteLink", { - detail: { targetUrl: linkUrl, id: annotationId }, - bubbles: true - })); + const interval = setInterval(() => { + // keep trying to edit until annotations have loaded and editing is successful + !success && + document.dispatchEvent( + new CustomEvent<{ targetUrl: string; id: string }>('deleteLink', { + detail: { targetUrl: linkUrl, id: annotationId }, + bubbles: true, + }) + ); }, 300); - setTimeout(action(() => { - if (!success) { - clearInterval(interval); - DocumentLinksButton.invisibleWebDoc = undefined; - } - }), 10000); // give up if no success after 10s - document.addEventListener("editSuccess", onSuccess); + setTimeout( + action(() => { + if (!success) { + clearInterval(interval); + //DocumentLinksButton.invisibleWebDoc = undefined; + } + }), + 10000 + ); // give up if no success after 10s + document.addEventListener('editSuccess', onSuccess); }; /** @@ -149,17 +166,20 @@ export namespace Hypothesis { export const scrollToAnnotation = (annotationId: string, target: Doc) => { var success = false; const onSuccess = () => { - console.log("Scroll success!!"); + console.log('Scroll success!!'); document.removeEventListener('scrollSuccess', onSuccess); clearInterval(interval); success = true; }; - const interval = setInterval(() => { // keep trying to scroll every 250ms until annotations have loaded and scrolling is successful - document.dispatchEvent(new CustomEvent('scrollToAnnotation', { - detail: annotationId, - bubbles: true - })); + const interval = setInterval(() => { + // keep trying to scroll every 250ms until annotations have loaded and scrolling is successful + document.dispatchEvent( + new CustomEvent('scrollToAnnotation', { + detail: annotationId, + bubbles: true, + }) + ); const targetView: Opt<DocumentView> = DocumentManager.Instance.getFirstDocumentView(target); const position = targetView?.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); targetView && position && simulateMouseClick(targetView.ContentDiv!, position[0], position[1], position[0], position[1], false); @@ -168,4 +188,4 @@ export namespace Hypothesis { document.addEventListener('scrollSuccess', onSuccess); // listen for success message from client setTimeout(() => !success && clearInterval(interval), 10000); // give up if no success after 10s }; -}
\ No newline at end of file +} diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx index 37571ae01..7f0c8a3e8 100644 --- a/src/client/util/Import & Export/DirectoryImportBox.tsx +++ b/src/client/util/Import & Export/DirectoryImportBox.tsx @@ -1,27 +1,27 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { BatchedArray } from "array-batcher"; -import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; -import { observer } from "mobx-react"; -import { extname } from "path"; -import Measure, { ContentRect } from "react-measure"; -import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../fields/Doc"; -import { Id } from "../../../fields/FieldSymbols"; -import { List } from "../../../fields/List"; -import { listSpec } from "../../../fields/Schema"; -import { SchemaHeaderField } from "../../../fields/SchemaHeaderField"; -import { BoolCast, Cast, NumCast } from "../../../fields/Types"; -import { AcceptableMedia, Upload } from "../../../server/SharedMediaTypes"; -import { Utils } from "../../../Utils"; -import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils"; -import { Docs, DocumentOptions, DocUtils } from "../../documents/Documents"; -import { Networking } from "../../Network"; -import { FieldView, FieldViewProps } from "../../views/nodes/FieldView"; -import { DocumentManager } from "../DocumentManager"; -import "./DirectoryImportBox.scss"; -import ImportMetadataEntry, { keyPlaceholder, valuePlaceholder } from "./ImportMetadataEntry"; -import React = require("react"); +import { BatchedArray } from 'array-batcher'; +import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; +import { extname } from 'path'; +import Measure, { ContentRect } from 'react-measure'; +import { Doc, DocListCast, DocListCastAsync, Opt } from '../../../fields/Doc'; +import { Id } from '../../../fields/FieldSymbols'; +import { List } from '../../../fields/List'; +import { listSpec } from '../../../fields/Schema'; +import { SchemaHeaderField } from '../../../fields/SchemaHeaderField'; +import { BoolCast, Cast, NumCast } from '../../../fields/Types'; +import { AcceptableMedia, Upload } from '../../../server/SharedMediaTypes'; +import { Utils } from '../../../Utils'; +import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; +import { Docs, DocumentOptions, DocUtils } from '../../documents/Documents'; +import { Networking } from '../../Network'; +import { FieldView, FieldViewProps } from '../../views/nodes/FieldView'; +import { DocumentManager } from '../DocumentManager'; +import './DirectoryImportBox.scss'; +import ImportMetadataEntry, { keyPlaceholder, valuePlaceholder } from './ImportMetadataEntry'; +import React = require('react'); -const unsupported = ["text/html", "text/plain"]; +const unsupported = ['text/html', 'text/plain']; @observer export class DirectoryImportBox extends React.Component<FieldViewProps> { @@ -29,7 +29,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> { @observable private top = 0; @observable private left = 0; private dimensions = 50; - @observable private phase = ""; + @observable private phase = ''; private disposer: Opt<IReactionDisposer>; @observable private entries: ImportMetadataEntry[] = []; @@ -40,7 +40,9 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> { @observable private uploading = false; @observable private removeHover = false; - public static LayoutString(fieldKey: string) { return FieldView.LayoutString(DirectoryImportBox, fieldKey); } + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(DirectoryImportBox, fieldKey); + } constructor(props: FieldViewProps) { super(props); @@ -71,7 +73,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> { handleSelection = async (e: React.ChangeEvent<HTMLInputElement>) => { runInAction(() => { this.uploading = true; - this.phase = "Initializing download..."; + this.phase = 'Initializing download...'; }); const docs: Doc[] = []; @@ -79,7 +81,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> { const files = e.target.files; if (!files || files.length === 0) return; - const directory = (files.item(0) as any).webkitRelativePath.split("/", 1)[0]; + const directory = (files.item(0) as any).webkitRelativePath.split('/', 1)[0]; const validated: File[] = []; for (let i = 0; i < files.length; i++) { @@ -100,7 +102,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> { const sizes: number[] = []; const modifiedDates: number[] = []; - runInAction(() => this.phase = `Internal: uploading ${this.quota - this.completed} files to Dash...`); + runInAction(() => (this.phase = `Internal: uploading ${this.quota - this.completed} files to Dash...`)); const batched = BatchedArray.from(validated, { batchSize: 15 }); const uploads = await batched.batchedMapAsync<Upload.FileResponse<Upload.ImageInformation>>(async (batch, collector) => { @@ -109,23 +111,28 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> { modifiedDates.push(file.lastModified); }); collector.push(...(await Networking.UploadFilesToServer<Upload.ImageInformation>(batch))); - runInAction(() => this.completed += batch.length); + runInAction(() => (this.completed += batch.length)); }); - await Promise.all(uploads.map(async response => { - const { source: { type }, result } = response; - if (result instanceof Error) { - return; - } - const { accessPaths, exifData } = result; - const path = Utils.prepend(accessPaths.agnostic.client); - const document = type && await DocUtils.DocumentFromType(type, path, { _width: 300 }); - const { data, error } = exifData; - if (document) { - Doc.GetProto(document).exif = error || Doc.Get.FromJson({ data }); - docs.push(document); - } - })); + await Promise.all( + uploads.map(async response => { + const { + source: { type }, + result, + } = response; + if (result instanceof Error) { + return; + } + const { accessPaths, exifData } = result; + const path = Utils.prepend(accessPaths.agnostic.client); + const document = type && (await DocUtils.DocumentFromType(type, path, { _width: 300 })); + const { data, error } = exifData; + if (document) { + Doc.GetProto(document).exif = error || Doc.Get.FromJson({ data }); + docs.push(document); + } + }) + ); for (let i = 0; i < docs.length; i++) { const doc = docs[i]; @@ -146,7 +153,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> { _height: 500, _chromeHidden: true, x: NumCast(doc.x), - y: NumCast(doc.y) + offset + y: NumCast(doc.y) + offset, }; const parent = this.props.ContainingCollectionView; if (parent) { @@ -154,14 +161,14 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> { if (docs.length < 50) { importContainer = Docs.Create.MasonryDocument(docs, options); } else { - const headers = [new SchemaHeaderField("title"), new SchemaHeaderField("size")]; + const headers = [new SchemaHeaderField('title'), new SchemaHeaderField('size')]; importContainer = Docs.Create.SchemaDocument(headers, docs, options); } - runInAction(() => this.phase = 'External: uploading files to Google Photos...'); + runInAction(() => (this.phase = 'External: uploading files to Google Photos...')); await GooglePhotos.Export.CollectionToAlbum({ collection: importContainer }); - Doc.AddDocToList(Doc.GetProto(parent.props.Document), "data", importContainer); + Doc.AddDocToList(Doc.GetProto(parent.props.Document), 'data', importContainer); !this.persistent && this.props.removeDocument && this.props.removeDocument(doc); - DocumentManager.Instance.jumpToDocument(importContainer, true, undefined, []); + DocumentManager.Instance.jumpToDocument(importContainer, { willPanZoom: true }, undefined, []); } runInAction(() => { @@ -169,14 +176,14 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> { this.quota = 1; this.completed = 0; }); - } + }; componentDidMount() { - this.selector.current!.setAttribute("directory", ""); - this.selector.current!.setAttribute("webkitdirectory", ""); + this.selector.current!.setAttribute('directory', ''); + this.selector.current!.setAttribute('webkitdirectory', ''); this.disposer = reaction( () => this.completed, - completed => runInAction(() => this.phase = `Internal: uploading ${this.quota - completed} files to Dash...`) + completed => runInAction(() => (this.phase = `Internal: uploading ${this.quota - completed} files to Dash...`)) ); } @@ -193,7 +200,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> { const offset = this.dimensions / 2; this.left = bounds.width / 2 - offset; this.top = bounds.height / 2 - offset; - } + }; @action addMetadataEntry = async () => { @@ -201,8 +208,8 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> { entryDoc.checked = false; entryDoc.key = keyPlaceholder; entryDoc.value = valuePlaceholder; - Doc.AddDocToList(this.props.Document, "data", entryDoc); - } + Doc.AddDocToList(this.props.Document, 'data', entryDoc); + }; @action remove = async (entry: ImportMetadataEntry) => { @@ -217,7 +224,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> { } } } - } + }; render() { const dimensions = 50; @@ -228,193 +235,204 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> { const uploading = this.uploading; const showRemoveLabel = this.removeHover; const persistent = this.persistent; - let percent = `${completed / quota * 100}`; - percent = percent.split(".")[0]; - percent = percent.startsWith("100") ? "99" : percent; + let percent = `${(completed / quota) * 100}`; + percent = percent.split('.')[0]; + percent = percent.startsWith('100') ? '99' : percent; const marginOffset = (percent.length === 1 ? 5 : 0) - 1.6; - const message = <span className={"phase"}>{this.phase}</span>; - const centerPiece = this.phase.includes("Google Photos") ? - <img src={"/assets/google_photos.png"} style={{ - transition: "0.4s opacity ease", - width: 30, - height: 30, - opacity: uploading ? 1 : 0, - pointerEvents: "none", - position: "absolute", - left: 12, - top: this.top + 10, - fontSize: 18, - color: "white", - marginLeft: this.left + marginOffset - }} /> - : <div + const message = <span className={'phase'}>{this.phase}</span>; + const centerPiece = this.phase.includes('Google Photos') ? ( + <img + src={'/assets/google_photos.png'} style={{ - transition: "0.4s opacity ease", + transition: '0.4s opacity ease', + width: 30, + height: 30, opacity: uploading ? 1 : 0, - pointerEvents: "none", - position: "absolute", + pointerEvents: 'none', + position: 'absolute', + left: 12, + top: this.top + 10, + fontSize: 18, + color: 'white', + marginLeft: this.left + marginOffset, + }} + /> + ) : ( + <div + style={{ + transition: '0.4s opacity ease', + opacity: uploading ? 1 : 0, + pointerEvents: 'none', + position: 'absolute', left: 10, top: this.top + 12.3, fontSize: 18, - color: "white", - marginLeft: this.left + marginOffset - }}>{percent}%</div>; + color: 'white', + marginLeft: this.left + marginOffset, + }}> + {percent}% + </div> + ); return ( <Measure offset onResize={this.preserveCentering}> - {({ measureRef }) => - <div ref={measureRef} style={{ width: "100%", height: "100%", pointerEvents: "all" }} > + {({ measureRef }) => ( + <div ref={measureRef} style={{ width: '100%', height: '100%', pointerEvents: 'all' }}> {message} <input - id={"selector"} + id={'selector'} ref={this.selector} onChange={this.handleSelection} type="file" style={{ - position: "absolute", - display: "none" - }} /> + position: 'absolute', + display: 'none', + }} + /> <label - htmlFor={"selector"} + htmlFor={'selector'} style={{ opacity: isEditing ? 0 : 1, - pointerEvents: isEditing ? "none" : "all", - transition: "0.4s ease opacity" - }} - > - <div style={{ - width: dimensions, - height: dimensions, - borderRadius: "50%", - background: "black", - position: "absolute", - left: this.left, - top: this.top - }} /> - <div style={{ - position: "absolute", - left: this.left + 8, - top: this.top + 10, - opacity: uploading ? 0 : 1, - transition: "0.4s opacity ease" + pointerEvents: isEditing ? 'none' : 'all', + transition: '0.4s ease opacity', }}> - <FontAwesomeIcon icon={"cloud-upload-alt"} color="#FFFFFF" size={"2x"} /> + <div + style={{ + width: dimensions, + height: dimensions, + borderRadius: '50%', + background: 'black', + position: 'absolute', + left: this.left, + top: this.top, + }} + /> + <div + style={{ + position: 'absolute', + left: this.left + 8, + top: this.top + 10, + opacity: uploading ? 0 : 1, + transition: '0.4s opacity ease', + }}> + <FontAwesomeIcon icon={'cloud-upload-alt'} color="#FFFFFF" size={'2x'} /> </div> <img style={{ width: 80, height: 80, - transition: "0.4s opacity ease", + transition: '0.4s opacity ease', opacity: uploading ? 0.7 : 0, - position: "absolute", + position: 'absolute', top: this.top - 15, - left: this.left - 15 + left: this.left - 15, }} - src={"/assets/loading.gif"}></img> + src={'/assets/loading.gif'}></img> </label> <input - type={"checkbox"} - onChange={e => runInAction(() => this.persistent = e.target.checked)} + type={'checkbox'} + onChange={e => runInAction(() => (this.persistent = e.target.checked))} style={{ margin: 0, - position: "absolute", + position: 'absolute', left: 10, bottom: 10, opacity: isEditing || uploading ? 0 : 1, - transition: "0.4s opacity ease", - pointerEvents: isEditing || uploading ? "none" : "all" + transition: '0.4s opacity ease', + pointerEvents: isEditing || uploading ? 'none' : 'all', }} checked={this.persistent} - onPointerEnter={action(() => this.removeHover = true)} - onPointerLeave={action(() => this.removeHover = false)} + onPointerEnter={action(() => (this.removeHover = true))} + onPointerLeave={action(() => (this.removeHover = false))} /> <p style={{ - position: "absolute", + position: 'absolute', left: 27, bottom: 8.4, fontSize: 12, opacity: showRemoveLabel ? 1 : 0, - transition: "0.4s opacity ease" - }}>Template will be <span style={{ textDecoration: "underline", textDecorationColor: persistent ? "green" : "red", color: persistent ? "green" : "red" }}>{persistent ? "kept" : "removed"}</span> after upload</p> + transition: '0.4s opacity ease', + }}> + Template will be <span style={{ textDecoration: 'underline', textDecorationColor: persistent ? 'green' : 'red', color: persistent ? 'green' : 'red' }}>{persistent ? 'kept' : 'removed'}</span> after upload + </p> {centerPiece} <div style={{ - position: "absolute", + position: 'absolute', top: 10, right: 10, - borderRadius: "50%", + borderRadius: '50%', width: 25, height: 25, - background: "black", - pointerEvents: uploading ? "none" : "all", + background: 'black', + pointerEvents: uploading ? 'none' : 'all', opacity: uploading ? 0 : 1, - transition: "0.4s opacity ease" + transition: '0.4s opacity ease', }} - title={isEditing ? "Back to Upload" : "Add Metadata"} - onClick={action(() => this.editingMetadata = !this.editingMetadata)} + title={isEditing ? 'Back to Upload' : 'Add Metadata'} + onClick={action(() => (this.editingMetadata = !this.editingMetadata))} /> <FontAwesomeIcon style={{ - pointerEvents: "none", - position: "absolute", + pointerEvents: 'none', + position: 'absolute', right: isEditing ? 14 : 15, top: isEditing ? 15.4 : 16, opacity: uploading ? 0 : 1, - transition: "0.4s opacity ease" + transition: '0.4s opacity ease', }} - icon={isEditing ? "cloud-upload-alt" : "tag"} + icon={isEditing ? 'cloud-upload-alt' : 'tag'} color="#FFFFFF" - size={"1x"} + size={'1x'} /> <div style={{ - transition: "0.4s ease opacity", - width: "100%", - height: "100%", - pointerEvents: isEditing ? "all" : "none", + transition: '0.4s ease opacity', + width: '100%', + height: '100%', + pointerEvents: isEditing ? 'all' : 'none', opacity: isEditing ? 1 : 0, - overflowY: "scroll" - }} - > + overflowY: 'scroll', + }}> <div style={{ - borderRadius: "50%", + borderRadius: '50%', width: 25, height: 25, marginLeft: 10, - position: "absolute", + position: 'absolute', right: 41, - top: 10 + top: 10, }} - title={"Add Metadata Entry"} - onClick={this.addMetadataEntry} - > + title={'Add Metadata Entry'} + onClick={this.addMetadataEntry}> <FontAwesomeIcon style={{ - pointerEvents: "none", + pointerEvents: 'none', marginLeft: 6.4, - marginTop: 5.2 + marginTop: 5.2, }} - icon={"plus"} - size={"1x"} + icon={'plus'} + size={'1x'} /> </div> - <p style={{ paddingLeft: 10, paddingTop: 8, paddingBottom: 7 }} >Add metadata to your import...</p> - <hr style={{ margin: "6px 10px 12px 10px" }} /> - {entries.map(doc => + <p style={{ paddingLeft: 10, paddingTop: 8, paddingBottom: 7 }}>Add metadata to your import...</p> + <hr style={{ margin: '6px 10px 12px 10px' }} /> + {entries.map(doc => ( <ImportMetadataEntry Document={doc} key={doc[Id]} remove={this.remove} - ref={(el) => { if (el) this.entries.push(el); }} + ref={el => { + if (el) this.entries.push(el); + }} next={this.addMetadataEntry} /> - )} + ))} </div> </div> - } + )} </Measure> ); } - -}
\ No newline at end of file +} diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts index f75ac24f5..0f216e349 100644 --- a/src/client/util/LinkFollower.ts +++ b/src/client/util/LinkFollower.ts @@ -1,12 +1,15 @@ -import { action, observable, observe } from 'mobx'; -import { computedFn } from 'mobx-utils'; -import { DirectLinksSym, Doc, DocListCast, DocListCastAsync, Field, Opt } from '../../fields/Doc'; -import { List } from '../../fields/List'; -import { ProxyField } from '../../fields/Proxy'; -import { BoolCast, Cast, StrCast } from '../../fields/Types'; +import { action, runInAction } from 'mobx'; +import { Doc, DocListCast, Opt } from '../../fields/Doc'; +import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../fields/Types'; +import { CollectionViewType, DocumentType } from '../documents/DocumentTypes'; +import { CollectionDockingView } from '../views/collections/CollectionDockingView'; +import { DocumentDecorations } from '../views/DocumentDecorations'; import { LightboxView } from '../views/LightboxView'; -import { DocumentViewSharedProps, ViewAdjustment } from '../views/nodes/DocumentView'; +import { DocFocusOptions, DocumentViewSharedProps, OpenWhere, OpenWhereMod, ViewAdjustment } from '../views/nodes/DocumentView'; +import { PresBox } from '../views/nodes/trails'; import { DocumentManager } from './DocumentManager'; +import { LinkManager } from './LinkManager'; +import { SelectionManager } from './SelectionManager'; import { UndoManager } from './UndoManager'; type CreateViewFunc = (doc: Doc, followLinkLocation: string, finished?: () => void) => void; @@ -26,20 +29,20 @@ export class LinkFollower { // 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 the lightbox, or in place // depending on the followLinkLocation property of the source (or the link itself as a fallback); - public static FollowLink = (linkDoc: Opt<Doc>, sourceDoc: Doc, docViewProps: DocumentViewSharedProps, altKey: boolean, zoom: boolean = false) => { + public static FollowLink = (linkDoc: Opt<Doc>, sourceDoc: Doc, docViewProps: DocumentViewSharedProps, 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 createTabForTarget = (didFocus: boolean) => new Promise<ViewAdjustment>(res => { - const where = LightboxView.LightboxDoc ? 'lightbox' : StrCast(sourceDoc.followLinkLocation, followLoc); + const where = LightboxView.LightboxDoc ? OpenWhere.inPlace : (StrCast(sourceDoc.followLinkLocation, followLoc) as OpenWhere); docViewProps.addDocTab(doc, where); setTimeout(() => { - const getFirstDocView = LightboxView.LightboxDoc ? DocumentManager.Instance.getLightboxDocumentView : DocumentManager.Instance.getFirstDocumentView; - const targDocView = getFirstDocView(doc); // get first document view available within the lightbox if that's open, or anywhere otherwise. + const targDocView = DocumentManager.Instance.getFirstDocumentView(doc); // get first document view available within the lightbox if that's open, or anywhere otherwise. if (targDocView) { targDocView.props.focus(doc, { - willZoom: BoolCast(sourceDoc.followLinkZoom, false), + willPan: true, + willPanZoom: BoolCast(sourceDoc.followLinkZoom, false), afterFocus: (didFocus: boolean) => { finished?.(); res(ViewAdjustment.resetView); @@ -47,63 +50,124 @@ export class LinkFollower { }, }); } else { - res(where !== 'inPlace' ? ViewAdjustment.resetView : ViewAdjustment.doNothing); // for 'inPlace' resetting the initial focus&zoom would negate the zoom into the target + finished?.(); + res(where !== OpenWhere.inPlace || BoolCast(sourceDoc.followLinkZoom) ? ViewAdjustment.resetView : ViewAdjustment.doNothing); // for 'inPlace' resetting the initial focus&zoom would negate the zoom into the target } - }); + }, 100); }); if (!sourceDoc.followLinkZoom) { createTabForTarget(false); } else { // first focus & zoom onto this (the clicked document). Then execute the function to focus on the target - docViewProps.focus(sourceDoc, { willZoom: BoolCast(sourceDoc.followLinkZoom, true), scale: 1, afterFocus: createTabForTarget }); + docViewProps.focus(sourceDoc, { willPan: true, willPanZoom: BoolCast(sourceDoc.followLinkZoom, true), zoomTime: 1000, zoomScale: 1, afterFocus: createTabForTarget }); } }; - LinkFollower.traverseLink(linkDoc, sourceDoc, createViewFunc, BoolCast(sourceDoc.followLinkZoom, zoom), docViewProps.ContainingCollectionDoc, batch.end, altKey ? true : undefined); + runInAction(() => (DocumentDecorations.Instance.overrideBounds = true)); // turn off decoration bounds while following links since animations may occur, and DocDecorations is based on screenToLocal which is not always an observable value + LinkFollower.traverseLink( + linkDoc, + sourceDoc, + createViewFunc, + docViewProps.ContainingCollectionDoc, + action(() => { + batch.end(); + Doc.AddUnHighlightWatcher(action(() => (DocumentDecorations.Instance.overrideBounds = false))); + }), + altKey ? true : undefined + ); }; - public static traverseLink(link: Opt<Doc>, sourceDoc: Doc, createViewFunc: CreateViewFunc, zoom = false, currentContext?: Doc, finished?: () => void, traverseBacklink?: boolean) { + public static traverseLink(link: Opt<Doc>, sourceDoc: Doc, createViewFunc: CreateViewFunc, currentContext?: Doc, finished?: () => void, traverseBacklink?: boolean) { const linkDocs = link ? [link] : DocListCast(sourceDoc.links); const firstDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor1 as Doc, sourceDoc) || Doc.AreProtosEqual((linkDoc.anchor1 as Doc).annotationOn as Doc, sourceDoc)); // link docs where 'doc' is anchor1 const secondDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor2 as Doc, sourceDoc) || Doc.AreProtosEqual((linkDoc.anchor2 as Doc).annotationOn as Doc, sourceDoc)); // 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 fwdLinkWithoutTargetView = firstDocs.find(d => DocumentManager.Instance.getDocumentViews((d.anchor2 as Doc).type === DocumentType.MARKER ? DocCast((d.anchor2 as Doc).annotationOn) : (d.anchor2 as Doc)).length === 0); + const backLinkWithoutTargetView = secondDocs.find(d => DocumentManager.Instance.getDocumentViews((d.anchor1 as Doc).type === DocumentType.MARKER ? DocCast((d.anchor1 as Doc).annotationOn) : (d.anchor1 as Doc)).length === 0); const linkWithoutTargetDoc = traverseBacklink === undefined ? fwdLinkWithoutTargetView || backLinkWithoutTargetView : traverseBacklink ? backLinkWithoutTargetView : fwdLinkWithoutTargetView; - const linkDocList = linkWithoutTargetDoc ? [linkWithoutTargetDoc] : traverseBacklink === undefined ? firstDocs.concat(secondDocs) : traverseBacklink ? secondDocs : firstDocs; - const followLinks = sourceDoc.isPushpin ? linkDocList : linkDocList.slice(0, 1); + const linkDocList = linkWithoutTargetDoc && !sourceDoc.followAllLinks ? [linkWithoutTargetDoc] : traverseBacklink === undefined ? firstDocs.concat(secondDocs) : traverseBacklink ? secondDocs : firstDocs; + const followLinks = sourceDoc.followLinkToggle || sourceDoc.followAllLinks ? linkDocList : linkDocList.slice(0, 1); var count = 0; const allFinished = () => ++count === followLinks.length && finished?.(); + if (!followLinks.length) finished?.(); followLinks.forEach(async linkDoc => { - if (linkDoc) { - const target = ( - sourceDoc === linkDoc.anchor1 - ? linkDoc.anchor2 - : sourceDoc === linkDoc.anchor2 - ? linkDoc.anchor1 - : Doc.AreProtosEqual(sourceDoc, linkDoc.anchor1 as Doc) || Doc.AreProtosEqual((linkDoc.anchor1 as Doc).annotationOn as Doc, sourceDoc) - ? linkDoc.anchor2 - : linkDoc.anchor1 - ) as Doc; - if (target) { + const target = ( + sourceDoc === linkDoc.anchor1 + ? linkDoc.anchor2 + : sourceDoc === linkDoc.anchor2 + ? linkDoc.anchor1 + : Doc.AreProtosEqual(sourceDoc, linkDoc.anchor1 as Doc) || Doc.AreProtosEqual((linkDoc.anchor1 as Doc).annotationOn as Doc, sourceDoc) + ? linkDoc.anchor2 + : linkDoc.anchor1 + ) as Doc; + if (target) { + const doFollow = async (canToggle?: boolean) => { + const options: DocFocusOptions = { + playAudio: BoolCast(sourceDoc.followLinkAudio), + toggleTarget: canToggle && BoolCast(sourceDoc.followLinkToggle), + willPan: true, + willPanZoom: BoolCast(LinkManager.getOppositeAnchor(linkDoc, target)?.followLinkZoom, false), + zoomTime: NumCast(LinkManager.getOppositeAnchor(linkDoc, target)?.followLinkTransitionTime, 500), + zoomScale: Cast(sourceDoc.followLinkZoomScale, 'number', null), + easeFunc: StrCast(sourceDoc.followLinkEase, 'ease') as any, + effect: sourceDoc, + originatingDoc: sourceDoc, + zoomTextSelections: false, + }; if (target.TourMap) { const fieldKey = Doc.LayoutFieldKey(target); const tour = DocListCast(target[fieldKey]).reverse(); LightboxView.SetLightboxDoc(currentContext, undefined, tour); setTimeout(LightboxView.Next); allFinished(); + } else if (target.type === DocumentType.PRES) { + const containerAnnoDoc = Cast(sourceDoc, Doc, null); + const containerDoc = containerAnnoDoc || sourceDoc; + var containerDocContext = containerDoc?.context ? [Cast(await containerDoc?.context, Doc, null)] : ([] as Doc[]); + while (containerDocContext.length && containerDocContext[0]?.context && DocCast(containerDocContext[0].context)?.viewType !== CollectionViewType.Docking) { + containerDocContext = [Cast(await containerDocContext[0].context, Doc, null), ...containerDocContext]; + } + if (!DocumentManager.Instance.getDocumentView(containerDocContext[0])) { + CollectionDockingView.AddSplit(containerDocContext[0], OpenWhereMod.right); + } + SelectionManager.DeselectAll(); + DocumentManager.Instance.AddViewRenderedCb(target, dv => containerDocContext.length && (dv.ComponentView as PresBox).PlayTrail(containerDocContext[0])); + PresBox.OpenPresMinimized(target, [0, 0]); + finished?.(); } else { const containerAnnoDoc = Cast(target.annotationOn, Doc, null); const containerDoc = containerAnnoDoc || target; - var containerDocContext = containerDoc?.context ? [Cast(containerDoc?.context, Doc, null)] : ([] as Doc[]); - while (containerDocContext.length && !DocumentManager.Instance.getDocumentView(containerDocContext[0]) && containerDocContext[0].context) { - containerDocContext = [Cast(containerDocContext[0].context, Doc, null), ...containerDocContext]; + var containerDocContext = containerDoc?.context ? [Cast(await containerDoc?.context, Doc, null)] : ([] as Doc[]); + while (containerDocContext.length && containerDocContext[0]?.context && !DocumentManager.Instance.getDocumentView(containerDocContext[0]) && DocCast(containerDocContext[0].context)?.viewType !== CollectionViewType.Docking) { + containerDocContext = [Cast(await containerDocContext[0].context, Doc, null), ...containerDocContext]; } const targetContexts = LightboxView.LightboxDoc ? [containerAnnoDoc || containerDocContext[0]].filter(a => a) : containerDocContext; - DocumentManager.Instance.jumpToDocument(target, zoom, (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, 'lightbox'), finished), targetContexts, linkDoc, undefined, sourceDoc, allFinished); + DocumentManager.Instance.jumpToDocument(target, options, (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, OpenWhere.inPlace), finished), targetContexts, allFinished); + } + }; + let movedTarget = false; + if (sourceDoc.followLinkLocation === OpenWhere.inParent) { + const sourceDocParent = DocCast(sourceDoc.context); + if (target.context instanceof Doc && target.context !== sourceDocParent) { + Doc.RemoveDocFromList(target.context, Doc.LayoutFieldKey(target.context), target); + movedTarget = true; + } + if (!DocListCast(sourceDocParent[Doc.LayoutFieldKey(sourceDocParent)]).includes(target)) { + Doc.AddDocToList(sourceDocParent, Doc.LayoutFieldKey(sourceDocParent), target); + movedTarget = true; + } + target.context = sourceDocParent; + const moveTo = [NumCast(sourceDoc.x) + NumCast(sourceDoc.followLinkXoffset), NumCast(sourceDoc.y) + NumCast(sourceDoc.followLinkYoffset)]; + if (sourceDoc.followLinkXoffset !== undefined && moveTo[0] !== target.x) { + target.x = moveTo[0]; + movedTarget = true; + } + if (sourceDoc.followLinkYoffset !== undefined && moveTo[1] !== target.y) { + target.y = moveTo[1]; + movedTarget = true; } - } else { - allFinished(); - } + if (movedTarget) setTimeout(doFollow); + else doFollow(true); + } else doFollow(true); } else { allFinished(); } diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index 7a12a8580..67c4669dd 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -3,7 +3,7 @@ import { computedFn } from 'mobx-utils'; import { DirectLinksSym, Doc, DocListCast, DocListCastAsync, Field, Opt } from '../../fields/Doc'; import { List } from '../../fields/List'; import { ProxyField } from '../../fields/Proxy'; -import { Cast, StrCast } from '../../fields/Types'; +import { Cast, DocCast, StrCast } from '../../fields/Types'; /* * link doc: * - anchor1: doc @@ -19,7 +19,8 @@ import { Cast, StrCast } from '../../fields/Types'; export class LinkManager { @observable static _instance: LinkManager; @observable static userLinkDBs: Doc[] = []; - public static currentLink: Opt<Doc>; + @observable public static currentLink: Opt<Doc>; + @observable public static currentLinkAnchor: Opt<Doc>; public static get Instance() { return LinkManager._instance; } @@ -31,22 +32,22 @@ export class LinkManager { this.createLinkrelationshipLists(); LinkManager.userLinkDBs = []; const addLinkToDoc = (link: Doc) => { - const a1Prom = link?.anchor1; - const a2Prom = link?.anchor2; - Promise.all([a1Prom, a2Prom]).then(all => { - const a1 = all[0]; - const a2 = all[1]; - const a1ProtoProm = (link?.anchor1 as Doc)?.proto; - const a2ProtoProm = (link?.anchor2 as Doc)?.proto; - Promise.all([a1ProtoProm, a2ProtoProm]).then( - action(all => { - if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) { - Doc.GetProto(a1)[DirectLinksSym].add(link); - Doc.GetProto(a2)[DirectLinksSym].add(link); - Doc.GetProto(link)[DirectLinksSym].add(link); - } - }) - ); + Promise.all([link]).then(linkdoc => { + const link = DocCast(linkdoc[0]); + Promise.all([link.proto]).then(linkproto => { + Promise.all([link.anchor1, link.anchor2]).then(linkdata => { + const a1 = DocCast(linkdata[0]); + const a2 = DocCast(linkdata[1]); + a1 && + a2 && + Promise.all([a1.proto, a2.proto]).then( + action(protos => { + (protos[0] as Doc)?.[DirectLinksSym].add(link); + (protos[1] as Doc)?.[DirectLinksSym].add(link); + }) + ); + }); + }); }); }; const remLinkFromDoc = (link: Doc) => { @@ -64,40 +65,42 @@ export class LinkManager { }; const watchUserLinkDB = (userLinkDBDoc: Doc) => { LinkManager.links.push(...DocListCast(userLinkDBDoc.data)); - const toRealField = (field: Field) => (field instanceof ProxyField ? field.value() : field); // see List.ts. data structure is not a simple list of Docs, but a list of ProxyField/Fields - observe( - userLinkDBDoc.data as Doc, - change => { - // observe pushes/splices on a user link DB 'data' field (should only happen for local changes) - switch (change.type as any) { - case 'splice': - (change as any).added.forEach((link: any) => addLinkToDoc(toRealField(link))); - (change as any).removed.forEach((link: any) => remLinkFromDoc(toRealField(link))); - break; - case 'update': //let oldValue = change.oldValue; - } - }, - true - ); - observe( - userLinkDBDoc, - 'data', // obsever when a new array of links is assigned as the link DB 'data' field (should happen whenever a remote user adds/removes a link) - change => { - switch (change.type as any) { - case 'update': - Promise.all([...((change.oldValue as any as Doc[]) || []), ...((change.newValue as any as Doc[]) || [])]).then(doclist => { - const oldDocs = doclist.slice(0, ((change.oldValue as any as Doc[]) || []).length); - const newDocs = doclist.slice(((change.oldValue as any as Doc[]) || []).length, doclist.length); + const toRealField = (field: Field) => (field instanceof ProxyField ? field.value : field); // see List.ts. data structure is not a simple list of Docs, but a list of ProxyField/Fields + if (userLinkDBDoc.data) { + observe( + userLinkDBDoc.data, + change => { + // observe pushes/splices on a user link DB 'data' field (should only happen for local changes) + switch (change.type as any) { + case 'splice': + (change as any).added.forEach((link: any) => addLinkToDoc(toRealField(link))); + (change as any).removed.forEach((link: any) => remLinkFromDoc(toRealField(link))); + break; + case 'update': //let oldValue = change.oldValue; + } + }, + true + ); + observe( + userLinkDBDoc, + 'data', // obsever when a new array of links is assigned as the link DB 'data' field (should happen whenever a remote user adds/removes a link) + change => { + switch (change.type as any) { + case 'update': + Promise.all([...((change.oldValue as any as Doc[]) || []), ...((change.newValue as any as Doc[]) || [])]).then(doclist => { + const oldDocs = doclist.slice(0, ((change.oldValue as any as Doc[]) || []).length); + const newDocs = doclist.slice(((change.oldValue as any as Doc[]) || []).length, doclist.length); - const added = newDocs?.filter(link => !(oldDocs || []).includes(link)); - const removed = oldDocs?.filter(link => !(newDocs || []).includes(link)); - added?.forEach((link: any) => addLinkToDoc(toRealField(link))); - removed?.forEach((link: any) => remLinkFromDoc(toRealField(link))); - }); - } - }, - true - ); + const added = newDocs?.filter(link => !(oldDocs || []).includes(link)); + const removed = oldDocs?.filter(link => !(newDocs || []).includes(link)); + added?.forEach((link: any) => addLinkToDoc(toRealField(link))); + removed?.forEach((link: any) => remLinkFromDoc(toRealField(link))); + }); + } + }, + true + ); + } }; observe( LinkManager.userLinkDBs, @@ -112,13 +115,6 @@ export class LinkManager { true ); LinkManager.addLinkDB(Doc.LinkDBDoc()); - DocListCastAsync(Doc.LinkDBDoc()?.data).then(dblist => - dblist?.forEach(async link => { - // make sure anchors are loaded to avoid incremental updates to computedFn's in LinkManager - const a1 = await Cast(link?.anchor1, Doc, null); - const a2 = await Cast(link?.anchor2, Doc, null); - }) - ); } public createLinkrelationshipLists = () => { @@ -144,28 +140,18 @@ export class LinkManager { return this.relatedLinker(anchor); } // finds all links that contain the given anchor public getAllDirectLinks(anchor: Doc): Doc[] { - // FIXME:glr Why is Doc undefined? - if (Doc.GetProto(anchor)[DirectLinksSym]) { - return Array.from(Doc.GetProto(anchor)[DirectLinksSym]); - } else { - return []; - } + return Array.from(Doc.GetProto(anchor)[DirectLinksSym] ?? []); } // finds all links that contain the given anchor relatedLinker = computedFn(function relatedLinker(this: any, anchor: Doc): Doc[] { - const lfield = Doc.LayoutFieldKey(anchor); if (!anchor || anchor instanceof Promise || Doc.GetProto(anchor) instanceof Promise) { console.log('WAITING FOR DOC/PROTO IN LINKMANAGER'); return []; } const dirLinks = Doc.GetProto(anchor)[DirectLinksSym]; - const annos = DocListCast(anchor[lfield + '-annotations']); - const timelineAnnos = DocListCast(anchor[lfield + '-annotations-timeline']); - if (!annos || !timelineAnnos) { - debugger; - } - const related = [...annos, ...timelineAnnos].reduce((list, anno) => [...list, ...LinkManager.Instance.relatedLinker(anno)], Array.from(dirLinks).slice()); - return related; + const annos = DocListCast(anchor[Doc.LayoutFieldKey(anchor) + '-annotations']); + if (!annos) debugger; + return annos.reduce((list, anno) => [...list, ...LinkManager.Instance.relatedLinker(anno)], Array.from(dirLinks).slice()); }, true); // returns map of group type to anchor's links in that group type diff --git a/src/client/util/ReplayMovements.ts b/src/client/util/ReplayMovements.ts index 86bc4c5de..d5bffc5e2 100644 --- a/src/client/util/ReplayMovements.ts +++ b/src/client/util/ReplayMovements.ts @@ -1,22 +1,24 @@ -import { CollectionFreeFormView } from "../views/collections/collectionFreeForm"; -import { IReactionDisposer, observable, observe, reaction } from "mobx"; -import { Doc } from "../../fields/Doc"; -import { VideoBox } from "../views/nodes/VideoBox"; -import { DocumentManager } from "./DocumentManager"; -import { CollectionDockingView } from "../views/collections/CollectionDockingView"; -import { DocServer } from "../DocServer"; -import { Movement, Presentation } from "./TrackMovements"; +import { CollectionFreeFormView } from '../views/collections/collectionFreeForm'; +import { IReactionDisposer, observable, observe, reaction } from 'mobx'; +import { Doc } from '../../fields/Doc'; +import { VideoBox } from '../views/nodes/VideoBox'; +import { DocumentManager } from './DocumentManager'; +import { CollectionDockingView } from '../views/collections/CollectionDockingView'; +import { DocServer } from '../DocServer'; +import { Movement, Presentation } from './TrackMovements'; +import { OpenWhereMod } from '../views/nodes/DocumentView'; export class ReplayMovements { - private timers: NodeJS.Timeout[] | null; + private timers: NodeJS.Timeout[] | null; private videoBoxDisposeFunc: IReactionDisposer | null; private videoBox: VideoBox | null; private isPlaying: boolean; - // create static instance and getter for global use @observable static _instance: ReplayMovements; - static get Instance(): ReplayMovements { return ReplayMovements._instance } + static get Instance(): ReplayMovements { + return ReplayMovements._instance; + } constructor() { // init the global instance ReplayMovements._instance = this; @@ -37,20 +39,27 @@ export class ReplayMovements { } Doc.UserDoc().presentationMode = 'none'; - this.isPlaying = false + this.isPlaying = false; // TODO: set userdoc presentMode to browsing - this.timers?.map(timer => clearTimeout(timer)) - } + this.timers?.map(timer => clearTimeout(timer)); + }; setVideoBox = async (videoBox: VideoBox) => { // console.info('setVideoBox', videoBox); - if (this.videoBox !== null) { console.warn('setVideoBox on already videoBox'); } - if (this.videoBoxDisposeFunc !== null) { console.warn('setVideoBox on already videoBox dispose func'); this.videoBoxDisposeFunc(); } - + if (this.videoBox !== null) { + console.warn('setVideoBox on already videoBox'); + } + if (this.videoBoxDisposeFunc !== null) { + console.warn('setVideoBox on already videoBox dispose func'); + this.videoBoxDisposeFunc(); + } const { presentation } = videoBox; - if (presentation == null) { console.warn('setVideoBox on null videoBox presentation'); return; } - + if (presentation == null) { + console.warn('setVideoBox on null videoBox presentation'); + return; + } + let docIdtoDoc: Map<string, Doc> = new Map(); try { docIdtoDoc = await this.loadPresentation(presentation); @@ -59,29 +68,30 @@ export class ReplayMovements { throw 'error loading docs from server'; } - - this.videoBoxDisposeFunc = - reaction(() => ({ playing: videoBox._playing, timeViewed: videoBox.player?.currentTime || 0 }), - ({ playing, timeViewed }) => - playing ? this.playMovements(presentation, docIdtoDoc, timeViewed) : this.pauseMovements() - ); + this.videoBoxDisposeFunc = reaction( + () => ({ playing: videoBox._playing, timeViewed: videoBox.player?.currentTime || 0 }), + ({ playing, timeViewed }) => (playing ? this.playMovements(presentation, docIdtoDoc, timeViewed) : this.pauseMovements()) + ); this.videoBox = videoBox; - } + }; removeVideoBox = () => { - if (this.videoBoxDisposeFunc == null) { console.warn('removeVideoBox on null videoBox'); return; } + if (this.videoBoxDisposeFunc == null) { + console.warn('removeVideoBox on null videoBox'); + return; + } this.videoBoxDisposeFunc(); this.videoBox = null; this.videoBoxDisposeFunc = null; - } + }; // should be called from interacting with the screen pauseFromInteraction = () => { this.videoBox?.Pause(); this.pauseMovements(); - } + }; loadPresentation = async (presentation: Presentation) => { const { movements } = presentation; @@ -91,7 +101,7 @@ export class ReplayMovements { // generate a set of all unique docIds const docIds = new Set<string>(); - for (const {docId} of movements) { + for (const { docId } of movements) { if (!docIds.has(docId)) docIds.add(docId); } @@ -107,27 +117,29 @@ export class ReplayMovements { // console.info('loadPresentation refFields', refFields, docIdtoDoc); return docIdtoDoc; - } + }; // returns undefined if the docView isn't open on the screen getCollectionFFView = (docId: string) => { const isInView = DocumentManager.Instance.getDocumentViewById(docId); - if (isInView) { return isInView.ComponentView as CollectionFreeFormView; } - } + if (isInView) { + return isInView.ComponentView as CollectionFreeFormView; + } + }; // will open the doc in a tab then return the CollectionFFView that holds it openTab = (docId: string, docIdtoDoc: Map<string, Doc>) => { const doc = docIdtoDoc.get(docId); if (doc == undefined) { - console.error(`docIdtoDoc did not contain docId ${docId}`) + console.error(`docIdtoDoc did not contain docId ${docId}`); return undefined; } // console.log('openTab', docId, doc); - CollectionDockingView.AddSplit(doc, 'right'); + CollectionDockingView.AddSplit(doc, OpenWhereMod.right); const docView = DocumentManager.Instance.getDocumentView(doc); // BUG - this returns undefined if the doc is already open return docView?.ComponentView as CollectionFreeFormView; - } + }; // helper to replay a movement zoomAndPan = (movement: Movement, document: CollectionFreeFormView) => { @@ -135,7 +147,7 @@ export class ReplayMovements { scale !== 0 && document.zoomSmoothlyAboutPt([panX, panY], scale, 0); document.Document._panX = panX; document.Document._panY = panY; - } + }; getFirstMovements = (movements: Movement[]): Map<string, Movement> => { if (movements === null) return new Map(); @@ -146,18 +158,19 @@ export class ReplayMovements { if (!docIdtoFirstMove.has(docId)) docIdtoFirstMove.set(docId, move); } return docIdtoFirstMove; - } + }; endPlayingPresentation = () => { this.isPlaying = false; Doc.UserDoc().presentationMode = 'none'; - } + }; public playMovements = (presentation: Presentation, docIdtoDoc: Map<string, Doc>, timeViewed: number = 0) => { // console.info('playMovements', presentation, timeViewed, docIdtoDoc); - if (presentation.movements === null || presentation.movements.length === 0) { //|| this.playFFView === null) { - return new Error('[recordingApi.ts] followMovements() failed: no presentation data') + if (presentation.movements === null || presentation.movements.length === 0) { + //|| this.playFFView === null) { + return new Error('[recordingApi.ts] followMovements() failed: no presentation data'); } if (this.isPlaying) return; @@ -165,7 +178,7 @@ export class ReplayMovements { Doc.UserDoc().presentationMode = 'watching'; // only get the movements that are remaining in the video time left - const filteredMovements = presentation.movements.filter(movement => movement.time > timeViewed * 1000) + const filteredMovements = presentation.movements.filter(movement => movement.time > timeViewed * 1000); const handleFirstMovements = () => { // if the first movement is a closed tab, open it @@ -179,13 +192,12 @@ export class ReplayMovements { const colFFView = this.getCollectionFFView(docId); if (colFFView) this.zoomAndPan(firstMove, colFFView); } - } + }; handleFirstMovements(); - // make timers that will execute each movement at the correct replay time this.timers = filteredMovements.map(movement => { - const timeDiff = movement.time - timeViewed * 1000 + const timeDiff = movement.time - timeViewed * 1000; return setTimeout(() => { const collectionFFView = this.getCollectionFFView(movement.docId); @@ -204,5 +216,5 @@ export class ReplayMovements { } }, timeDiff); }); - } + }; } diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index a3d6f5227..646942569 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -1,9 +1,10 @@ import { action, observable, ObservableMap } from 'mobx'; import { computedFn } from 'mobx-utils'; -import { Doc, Opt } from '../../fields/Doc'; +import { Doc, DocListCast, Opt } from '../../fields/Doc'; import { DocCast } from '../../fields/Types'; import { CollectionViewType, DocumentType } from '../documents/DocumentTypes'; import { DocumentView } from '../views/nodes/DocumentView'; +import { LinkManager } from './LinkManager'; import { ScriptingGlobals } from './ScriptingGlobals'; export namespace SelectionManager { @@ -21,6 +22,9 @@ export namespace SelectionManager { // if doc is not in SelectedDocuments, add it if (!manager.SelectedViews.get(docView) && docView.props.Document.type !== DocumentType.MARKER) { if (!ctrlPressed) { + if (LinkManager.currentLink && !DocListCast(docView.rootDoc.links).includes(LinkManager.currentLink) && docView.rootDoc !== LinkManager.currentLink) { + LinkManager.currentLink = undefined; + } this.DeselectAll(); } diff --git a/src/client/util/SerializationHelper.ts b/src/client/util/SerializationHelper.ts index 2d598c1ac..2d1f61cfb 100644 --- a/src/client/util/SerializationHelper.ts +++ b/src/client/util/SerializationHelper.ts @@ -1,6 +1,6 @@ -import { PropSchema, serialize, deserialize, custom, setDefaultModelSchema, getDefaultModelSchema } from "serializr"; -import { Field } from "../../fields/Doc"; -import { ClientUtils } from "./ClientUtils"; +import { PropSchema, serialize, deserialize, custom, setDefaultModelSchema, getDefaultModelSchema } from 'serializr'; +import { Field } from '../../fields/Doc'; +import { ClientUtils } from './ClientUtils'; let serializing = 0; export function afterDocDeserialize(cb: (err: any, val: any) => void, err: any, newValue: any) { @@ -25,7 +25,7 @@ export namespace SerializationHelper { serializing++; if (!(obj.constructor.name in reverseMap)) { serializing--; - throw Error("Error: " + `type '${obj.constructor.name}' not registered. Make sure you register it using a @Deserializable decorator`); + throw Error('Error: ' + `type '${obj.constructor.name}' not registered. Make sure you register it using a @Deserializable decorator`); } const json = serialize(obj); @@ -59,24 +59,24 @@ export namespace SerializationHelper { const type = serializationTypes[obj.__type]; const value = await new Promise(res => deserialize(type.ctor, obj, (err, result) => res(result))); if (type.afterDeserialize) { - await type.afterDeserialize(value); + type.afterDeserialize(value); } return value; } } -const serializationTypes: { [name: string]: { ctor: { new(): any }, afterDeserialize?: (obj: any) => void | Promise<any> } } = {}; +const serializationTypes: { [name: string]: { ctor: { new (): any }; afterDeserialize?: (obj: any) => void | Promise<any> } } = {}; const reverseMap: { [ctor: string]: string } = {}; export interface DeserializableOpts { - (constructor: { new(...args: any[]): any }): void; + (constructor: { new (...args: any[]): any }): void; withFields(fields: string[]): Function; } export function Deserializable(name: string, afterDeserialize?: (obj: any) => void | Promise<any>): DeserializableOpts; -export function Deserializable(constructor: { new(...args: any[]): any }): void; -export function Deserializable(constructor: { new(...args: any[]): any } | string, afterDeserialize?: (obj: any) => void): DeserializableOpts | void { - function addToMap(name: string, ctor: { new(...args: any[]): any }) { +export function Deserializable(constructor: { new (...args: any[]): any }): void; +export function Deserializable(constructor: { new (...args: any[]): any } | string, afterDeserialize?: (obj: any) => void): DeserializableOpts | void { + function addToMap(name: string, ctor: { new (...args: any[]): any }) { const schema = getDefaultModelSchema(ctor) as any; if (schema.targetClass !== ctor) { const newSchema = { ...schema, factory: () => new ctor() }; @@ -89,17 +89,20 @@ export function Deserializable(constructor: { new(...args: any[]): any } | strin throw new Error(`Name ${name} has already been registered as deserializable`); } } - if (typeof constructor === "string") { - return Object.assign((ctor: { new(...args: any[]): any }) => { - addToMap(constructor, ctor); - }, { withFields: (fields: string[]) => Deserializable.withFields(fields, constructor, afterDeserialize) }); + if (typeof constructor === 'string') { + return Object.assign( + (ctor: { new (...args: any[]): any }) => { + addToMap(constructor, ctor); + }, + { withFields: (fields: string[]) => Deserializable.withFields(fields, constructor, afterDeserialize) } + ); } addToMap(constructor.name, constructor); } export namespace Deserializable { export function withFields(fields: string[], name?: string, afterDeserialize?: (obj: any) => void | Promise<any>) { - return function (constructor: { new(...fields: any[]): any }) { + return function (constructor: { new (...fields: any[]): any }) { Deserializable(name || constructor.name, afterDeserialize)(constructor); let schema = getDefaultModelSchema(constructor); if (schema) { @@ -128,7 +131,7 @@ export namespace Deserializable { factory: context => { const args = fields.map(key => context.json[key]); return new constructor(...args); - } + }, }; setDefaultModelSchema(constructor, schema); } @@ -138,7 +141,7 @@ export namespace Deserializable { export function autoObject(): PropSchema { return custom( - (s) => SerializationHelper.Serialize(s), + s => SerializationHelper.Serialize(s), (json: any, context: any, oldValue: any, cb: (err: any, result: any) => void) => SerializationHelper.Deserialize(json).then(res => cb(null, res)) ); -}
\ No newline at end of file +} diff --git a/src/client/util/SettingsManager.scss b/src/client/util/SettingsManager.scss index b7199f433..1289ca2b4 100644 --- a/src/client/util/SettingsManager.scss +++ b/src/client/util/SettingsManager.scss @@ -1,4 +1,4 @@ -@import "../views/global/globalCssVariables"; +@import '../views/global/globalCssVariables'; .settings-interface { //background-color: whitesmoke !important; @@ -59,7 +59,6 @@ } } - .password-content { display: flex; flex-direction: column; @@ -76,7 +75,6 @@ color: black; border-radius: 5px; padding: 7px; - } } @@ -148,7 +146,6 @@ margin-top: 2; text-align: left; } - } } @@ -297,9 +294,8 @@ margin-bottom: 10px; } - .error-text { - color: #C40233; + color: #c40233; width: 300; margin-left: -20; font-size: 10; @@ -313,7 +309,7 @@ font-size: 10; margin-bottom: 4; margin-top: -3; - color: #009F6B; + color: #009f6b; } .focus-span { @@ -352,7 +348,6 @@ padding: 0 0 0 20px; color: black; } - } } @@ -421,7 +416,6 @@ .tab-content { display: flex; - margin: 20px 0; .tab-column { flex: 0 0 50%; @@ -437,9 +431,7 @@ .tab-column-content { padding-left: 16px; } - } - } .tab-column button { @@ -465,4 +457,4 @@ .settings-interface .settings-heading { font-size: 25; } -}
\ No newline at end of file +} diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index 5c1c836f7..a3d76591f 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -28,7 +28,7 @@ export enum ColorScheme { export enum freeformScrollMode { Pan = 'pan', - Zoom = 'zoom' + Zoom = 'zoom', } @observer @@ -307,11 +307,9 @@ export class SettingsManager extends React.Component<{}> { ); } - - setFreeformScrollMode = (mode: freeformScrollMode) => { Doc.UserDoc().freeformScrollMode = mode; - } + }; @computed get modesContent() { return ( @@ -334,12 +332,20 @@ export class SettingsManager extends React.Component<{}> { <div className="playground-text">Playground Mode</div> </div> </div> - <div className="tab-column-title">Freeform scroll mode</div> - <div> - <button onClick={() => this.setFreeformScrollMode(freeformScrollMode.Pan)}>Scroll to pan</button> - <div>Scrolling pans around the freeform, holding shift and scrolling zooms in and out.</div> - <button onClick={() => this.setFreeformScrollMode(freeformScrollMode.Zoom)}>Scroll to zoom</button> - <div>Scrolling zooms in and out of canvas</div> + <div className="tab-column-title" style={{ marginTop: 10, marginBottom: 0 }}> + Freeform scrolling + </div> + <div className="tab-column-content"> + <button style={{ backgroundColor: Doc.UserDoc().freeformScrollMode === freeformScrollMode.Pan ? 'blue' : '' }} onClick={() => this.setFreeformScrollMode(freeformScrollMode.Pan)}> + Scroll to pan + </button> + <div> + <div>Scrolling pans canvas, shift + scrolling zooms</div> + </div> + <button style={{ backgroundColor: Doc.UserDoc().freeformScrollMode === freeformScrollMode.Zoom ? 'blue' : '' }} onClick={() => this.setFreeformScrollMode(freeformScrollMode.Zoom)}> + Scroll to zoom + </button> + <div>Scrolling zooms canvas</div> </div> </div> <div className="tab-column"> diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index 4b0310e76..824a862cb 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -5,7 +5,8 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import Select from 'react-select'; import * as RequestPromise from 'request-promise'; -import { AclAugment, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, AclUnset, DataSym, Doc, DocListCast, DocListCastAsync, Opt, AclSelfEdit } from '../../fields/Doc'; +import { AclAdmin, AclPrivate, AclSym, AclUnset, DataSym, Doc, DocListCast, DocListCastAsync, HierarchyMapping, Opt } from '../../fields/Doc'; +import { Id } from '../../fields/FieldSymbols'; import { List } from '../../fields/List'; import { Cast, NumCast, StrCast } from '../../fields/Types'; import { distributeAcls, GetEffectiveAcl, normalizeEmail, SharingPermissions, TraceMobx } from '../../fields/util'; @@ -20,10 +21,9 @@ import { SearchBox } from '../views/search/SearchBox'; import { DocumentManager } from './DocumentManager'; import { GroupManager, UserOptions } from './GroupManager'; import { GroupMemberView } from './GroupMemberView'; +import { LinkManager } from './LinkManager'; import { SelectionManager } from './SelectionManager'; import './SharingManager.scss'; -import { LinkManager } from './LinkManager'; -import { Id } from '../../fields/FieldSymbols'; export interface User { email: string; @@ -82,16 +82,6 @@ export class SharingManager extends React.Component<{}> { @observable private layoutDocAcls: boolean = false; // whether the layout doc or data doc's acls are to be used @observable private myDocAcls: boolean = false; // whether the My Docs checkbox is selected or not - // maps acl symbols to SharingPermissions - private AclMap = new Map<symbol, string>([ - [AclPrivate, SharingPermissions.None], - [AclReadonly, SharingPermissions.View], - [AclAugment, SharingPermissions.Augment], - [AclSelfEdit, SharingPermissions.SelfEdit], - [AclEdit, SharingPermissions.Edit], - [AclAdmin, SharingPermissions.Admin], - ]); - // private get linkVisible() { // return this.targetDoc ? this.targetDoc["acl-" + PublicKey] !== SharingPermissions.None : false; // } @@ -163,7 +153,7 @@ export class SharingManager extends React.Component<{}> { for (const sharer of sharingDocs) { if (!this.users.find(user => user.user.email === sharer.user.email)) { this.users.push(sharer); - LinkManager.addLinkDB(sharer.linkDatabase); + //LinkManager.addLinkDB(sharer.linkDatabase); } } }); @@ -184,6 +174,7 @@ export class SharingManager extends React.Component<{}> { const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.props.Document); return !docs + .map(doc => (this.layoutDocAcls ? doc : doc[DataSym])) .map(doc => { doc.author === Doc.CurrentUserEmail && !doc[myAcl] && distributeAcls(myAcl, SharingPermissions.Admin, doc, undefined, undefined, isDashboard); @@ -217,6 +208,7 @@ export class SharingManager extends React.Component<{}> { // ! ensures it returns true if document has been shared successfully, false otherwise return !docs + .map(doc => (this.layoutDocAcls ? doc : doc[DataSym])) .map(doc => { doc.author === Doc.CurrentUserEmail && !doc[`acl-${Doc.CurrentUserEmailNormalized}`] && distributeAcls(`acl-${Doc.CurrentUserEmailNormalized}`, SharingPermissions.Admin, doc, undefined, undefined, isDashboard); @@ -283,6 +275,7 @@ export class SharingManager extends React.Component<{}> { docs.forEach(doc => { const isDashboard = dashboards.indexOf(doc) !== -1; if (GetEffectiveAcl(doc) === AclAdmin) distributeAcls(`acl-${shareWith}`, permission, doc, undefined, undefined, isDashboard); + this.setDashboardBackground(doc, permission as SharingPermissions); }); } }; @@ -290,14 +283,18 @@ export class SharingManager extends React.Component<{}> { /** * Sets the background of the Dashboard if it has been shared as a visual indicator */ - setDashboardBackground = async (doc: Doc, permission: SharingPermissions) => { + setDashboardBackground = (doc: Doc, permission: SharingPermissions) => { if (Doc.IndexOf(doc, DocListCast(Doc.MyDashboards.data)) !== -1) { if (permission !== SharingPermissions.None) { doc.isShared = true; doc.backgroundColor = 'green'; } else { const acls = doc[DataSym][AclSym]; - if (Object.keys(acls).every(key => (key === `acl-${Doc.CurrentUserEmailNormalized}` ? true : [AclUnset, AclPrivate].includes(acls[key])))) { + if ( + Object.keys(acls) + .filter(key => key !== `acl-${Doc.CurrentUserEmailNormalized}` && key !== 'acl-Me') + .every(key => [AclUnset, AclPrivate].includes(acls[key])) + ) { doc.isShared = undefined; doc.backgroundColor = undefined; } @@ -372,7 +369,7 @@ export class SharingManager extends React.Component<{}> { private sharingOptions(uniform: boolean, override?: boolean) { const dropdownValues: string[] = Object.values(SharingPermissions); if (!uniform) dropdownValues.unshift('-multiple-'); - if (override) dropdownValues.unshift('None'); + if (!override) dropdownValues.splice(dropdownValues.indexOf(SharingPermissions.Unset), 1); return dropdownValues .filter(permission => !Doc.noviceMode || ![SharingPermissions.SelfEdit].includes(permission as any)) .map(permission => ( @@ -392,7 +389,7 @@ export class SharingManager extends React.Component<{}> { onClick={() => { let context: Opt<CollectionView>; if (this.targetDoc && this.targetDocView && docs.length === 1 && (context = this.targetDocView.props.ContainingCollectionView)) { - DocumentManager.Instance.jumpToDocument(this.targetDoc, true, undefined, [context.props.Document]); + DocumentManager.Instance.jumpToDocument(this.targetDoc, { willPanZoom: true }, undefined, [context.props.Document]); } }} onPointerEnter={action(() => { @@ -458,17 +455,6 @@ export class SharingManager extends React.Component<{}> { } }; - // distributeOverCollection = (targetDoc?: Doc) => { - // const target = targetDoc || this.targetDoc!; - - // const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.props.Document); - // docs.forEach(doc => { - // for (const [key, value] of Object.entries(doc[AclSym])) { - // distributeAcls(key, this.AclMap.get(value)! as SharingPermissions, target); - // } - // }); - // } - /** * Sorting algorithm to sort users. */ @@ -491,6 +477,7 @@ export class SharingManager extends React.Component<{}> { * @returns the main interface of the SharingManager. */ @computed get sharingInterface() { + if (!this.targetDoc) return null; TraceMobx(); const groupList = GroupManager.Instance?.allGroups || []; const sortedUsers = this.users @@ -523,21 +510,21 @@ export class SharingManager extends React.Component<{}> { docs = newDocs.filter(doc => GetEffectiveAcl(doc) === AclAdmin); } - const targetDoc = docs[0]; + const targetDoc = this.layoutDocAcls ? docs[0] : docs[0]?.[DataSym]; // tslint:disable-next-line: no-unnecessary-callback-wrapper const effectiveAcls = docs.map(doc => GetEffectiveAcl(doc)); const admin = this.myDocAcls ? Boolean(docs.length) : 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 commonKeys = intersection(...docs.map(doc => (this.layoutDocAcls ? doc : doc[DataSym])).map(doc => doc?.[AclSym] && Object.keys(doc[AclSym]))); // the list of users shared with const userListContents: (JSX.Element | null)[] = users .filter(({ user }) => (docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(user.email)}`) : docs[0]?.author !== user.email)) .map(({ user, linkDatabase, sharingDoc, userColor }) => { const userKey = `acl-${normalizeEmail(user.email)}`; - const uniform = docs.every(doc => (this.layoutDocAcls ? doc?.[AclSym]?.[userKey] === docs[0]?.[AclSym]?.[userKey] : doc?.[DataSym]?.[AclSym]?.[userKey] === docs[0]?.[DataSym]?.[AclSym]?.[userKey])); + const uniform = docs.map(doc => (this.layoutDocAcls ? doc : doc[DataSym])).every(doc => doc?.[AclSym]?.[userKey] === docs[0]?.[AclSym]?.[userKey]); const permissions = uniform ? StrCast(targetDoc?.[userKey]) : '-multiple-'; return !permissions ? null : ( @@ -573,7 +560,7 @@ export class SharingManager extends React.Component<{}> { <div key={'me'} className={'container'}> <span className={'padding'}>Me</span> <div className="edit-actions"> - <div className={'permissions-dropdown'}>{effectiveAcls.every(acl => acl === effectiveAcls[0]) ? this.AclMap.get(effectiveAcls[0])! : '-multiple-'}</div> + <div className={'permissions-dropdown'}>{effectiveAcls.every(acl => acl === effectiveAcls[0]) ? HierarchyMapping.get(effectiveAcls[0])!.name : '-multiple-'}</div> </div> </div> ) : null @@ -584,7 +571,9 @@ export class SharingManager extends React.Component<{}> { groupListMap.unshift({ title: 'Public' }); //, { title: "ALL" }); const groupListContents = groupListMap.map(group => { const groupKey = `acl-${StrCast(group.title)}`; - const uniform = docs.every(doc => (this.layoutDocAcls ? doc?.[AclSym]?.[groupKey] === docs[0]?.[AclSym]?.[groupKey] : doc?.[DataSym]?.[AclSym]?.[groupKey] === docs[0]?.[DataSym]?.[AclSym]?.[groupKey])); + const uniform = docs + .map(doc => (this.layoutDocAcls ? doc : doc[DataSym])) + .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.title)}`]) : '-multiple-'; return !permissions ? null : ( diff --git a/src/client/util/request-image-size.js b/src/client/util/request-image-size.ts index 502e0fbac..57e8516ac 100644 --- a/src/client/util/request-image-size.js +++ b/src/client/util/request-image-size.ts @@ -13,7 +13,7 @@ const request = require('request'); const imageSize = require('image-size'); const HttpError = require('standard-http-error'); -module.exports = function requestImageSize(options) { +module.exports = function requestImageSize(options: any) { let opts = { encoding: null, }; @@ -36,28 +36,24 @@ module.exports = function requestImageSize(options) { return new Promise((resolve, reject) => { const req = request(opts); - req.on('response', res => { + req.on('response', (res: any) => { if (res.statusCode >= 400) { return reject(new HttpError(res.statusCode, res.statusMessage)); } let buffer = Buffer.from([]); - let size; + let size: any; - res.on('data', chunk => { + res.on('data', (chunk: any) => { buffer = Buffer.concat([buffer, chunk]); try { size = imageSize(buffer); - } catch (err) { - reject(err); - return req.abort(); - } - - if (size) { - resolve(size); - return req.abort(); - } + if (size) { + resolve(size); + return req.abort(); + } + } catch (err) {} }); res.on('error', reject); diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx index dc9c2eb6c..e87d2046b 100644 --- a/src/client/views/ContextMenuItem.tsx +++ b/src/client/views/ContextMenuItem.tsx @@ -29,6 +29,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select @observable private _items: Array<ContextMenuProps> = []; @observable private overItem = false; + @action componentDidMount() { this._items.length = 0; if ((this.props as SubmenuProps)?.subitems) { diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index 043a83d16..c9c09b63b 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -147,7 +147,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>() setTimeout(() => docs.map(doc => { // this allows 'addDocument' to see the annotationOn field in order to create a pushin - Doc.SetInPlace(doc, 'isPushpin', undefined, true); + Doc.SetInPlace(doc, 'followLinkToggle', undefined, true); doc.annotationOn === this.props.Document && Doc.SetInPlace(doc, 'annotationOn', undefined, true); }) ); diff --git a/src/client/views/DocumentButtonBar.scss b/src/client/views/DocumentButtonBar.scss index 1e93ba5e2..835d6c8bb 100644 --- a/src/client/views/DocumentButtonBar.scss +++ b/src/client/views/DocumentButtonBar.scss @@ -18,15 +18,61 @@ $linkGap: 3px; cursor: pointer; } +.documentButtonBar-followTypes, .documentButtonBar-pinTypes { position: absolute; - display: flex; + display: none; width: 60px; top: -14px; background: black; height: 20px; align-items: center; } +.documentButtonBar-linkTypes { + position: absolute; + display: none; + width: 60px; + top: -14px; + background: black; + height: 20px; + align-items: center; +} +.documentButtonBar-followTypes { + width: 20px; + display: none; +} +.documentButtonBar-followIcon { + align-items: center; + display: flex; + height: 100%; + &:hover { + background-color: lightblue; + } +} +.documentButtonBar-follow { + &:hover { + .documentButtonBar-followTypes { + display: flex; + } + } +} +.documentButtonBar-pin { + color: white; + &:hover { + .documentButtonBar-pinTypes { + display: flex; + } + } +} +.documentButtonBar-link { + color: white; + &:hover { + .documentButtonBar-linkTypes { + display: flex; + } + } +} + .documentButtonBar-pinIcon { &:hover { background-color: lightblue; diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index d42ff436f..90c6c040c 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -1,31 +1,32 @@ 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 { action, computed, observable, runInAction, trace } from 'mobx'; import { observer } from 'mobx-react'; import { Doc } from '../../fields/Doc'; import { RichTextField } from '../../fields/RichTextField'; import { Cast, NumCast } from '../../fields/Types'; -import { emptyFunction, setupMoveUpEvents, simulateMouseClick } from '../../Utils'; +import { emptyFunction, returnFalse, setupMoveUpEvents, simulateMouseClick } from '../../Utils'; import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager'; import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils'; import { Docs } from '../documents/Documents'; import { DragManager } from '../util/DragManager'; import { SelectionManager } from '../util/SelectionManager'; -import { SettingsManager } from '../util/SettingsManager'; import { SharingManager } from '../util/SharingManager'; -import { undoBatch } from '../util/UndoManager'; +import { undoBatch, UndoManager } from '../util/UndoManager'; import { CollectionDockingView } from './collections/CollectionDockingView'; import { TabDocView } from './collections/TabDocView'; import './DocumentButtonBar.scss'; import { Colors } from './global/globalEnums'; +import { LinkPopup } from './linking/LinkPopup'; import { MetadataEntryMenu } from './MetadataEntryMenu'; import { DocumentLinksButton } from './nodes/DocumentLinksButton'; -import { DocumentView, DocumentViewInternal } from './nodes/DocumentView'; +import { DocumentView, DocumentViewInternal, OpenWhereMod } from './nodes/DocumentView'; import { DashFieldView } from './nodes/formattedText/DashFieldView'; import { GoogleRef } from './nodes/formattedText/FormattedTextBox'; import { TemplateMenu } from './TemplateMenu'; import React = require('react'); +import { DocumentType } from '../documents/DocumentTypes'; const higflyout = require('@hig/flyout'); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -180,7 +181,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV googleDoc = Docs.Create.WebDocument(googleDocUrl, options); dataDoc.googleDoc = googleDoc; } - CollectionDockingView.AddSplit(googleDoc, 'right'); + CollectionDockingView.AddSplit(googleDoc, OpenWhereMod.right); } else if (e.altKey) { e.preventDefault(); window.open(googleDocUrl); @@ -209,21 +210,74 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV </Tooltip> ); } + @observable subFollow = ''; @computed get followLinkButton() { const targetDoc = this.view0?.props.Document; + const followBtn = (allDocs: boolean, click: (doc: Doc) => void, isSet: (doc?: Doc) => boolean, icon: IconProp) => { + const tooltip = `Follow ${this.subPin}documents`; + return !tooltip ? null : ( + <Tooltip title={<div className="dash-tooltip">{tooltip}</div>}> + <div className="documentButtonBar-followIcon" style={{ backgroundColor: isSet(targetDoc) ? Colors.LIGHT_BLUE : Colors.DARK_GRAY, color: isSet(targetDoc) ? Colors.BLACK : Colors.WHITE }}> + <FontAwesomeIcon + className="documentdecorations-icon" + style={{ width: 20 }} + key={icon.toString()} + size="sm" + icon={icon} + onPointerEnter={action(e => (this.subPin = allDocs ? 'All ' : ''))} + onPointerLeave={action(e => (this.subPin = ''))} + onClick={e => { + this.props.views().forEach(dv => click(dv!.rootDoc)); + e.stopPropagation(); + }} + /> + </div> + </Tooltip> + ); + }; return !targetDoc ? null : ( - <Tooltip title={<div className="dash-tooltip">{'Set onClick to follow primary link'}</div>}> + <Tooltip title={<div className="dash-tooltip">Set onClick to follow primary link</div>}> <div - className="documentButtonBar-icon" + className="documentButtonBar-icon documentButtonBar-follow" style={{ backgroundColor: targetDoc.isLinkButton ? Colors.LIGHT_BLUE : Colors.DARK_GRAY, color: targetDoc.isLinkButton ? Colors.BLACK : Colors.WHITE }} onClick={undoBatch(e => this.props.views().map(view => view?.docView?.toggleFollowLink(undefined, undefined, false)))}> + <div className="documentButtonBar-followTypes"> + {followBtn( + true, + (doc: Doc) => (doc.followAllLinks = !doc.followAllLinks), + (doc?: Doc) => (doc?.followAllLinks ? true : false), + 'window-maximize' + )} + </div> <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="hand-point-right" /> </div> </Tooltip> ); } - @observable expandPin = false; + + @observable subLink = ''; + @computed get linkButton() { + const targetDoc = this.view0?.props.Document; + return !targetDoc || !this.view0 ? null : ( + <div className="documentButtonBar-icon documentButtonBar-link"> + <div className="documentButtonBar-linkTypes"> + <Tooltip title={<div>search for target</div>}> + <div className="documentButtonBar-button"> + <button style={{ backgroundColor: 'transparent', width: 35, height: 35, display: 'flex', justifyContent: 'center', alignItems: 'center', position: 'relative' }} onPointerDown={this.toggleLinkSearch}> + <FontAwesomeIcon style={{ position: 'absolute', transform: 'scale(1.5)' }} icon={'search'} size="lg" /> + <FontAwesomeIcon style={{ position: 'absolute', transform: 'scale(0.5)', transformOrigin: 'center', top: 9, left: 2 }} icon={'link'} size="lg" /> + </button> + </div> + </Tooltip> + </div> + <div style={{ width: 25, height: 25 }}> + <DocumentLinksButton View={this.view0} AlwaysOn={true} InMenu={true} StartLink={true} /> + </div> + </div> + ); + } + @observable subPin = ''; @computed get pinButton() { @@ -253,7 +307,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV .views() .filter(v => v) .map(dv => dv!.rootDoc); - TabDocView.PinDoc(docs, { pinDocLayout, pinDocContent, activeFrame: Cast(docs.lastElement()?.activeFrame, 'number', null) }); + TabDocView.PinDoc(docs, { pinAudioPlay: true, pinDocLayout, pinDocContent, activeFrame: Cast(docs.lastElement()?.activeFrame, 'number', null), currentFrame: Cast(docs.lastElement()?.currentFrame, 'number', null) }); e.stopPropagation(); }} /> @@ -264,25 +318,20 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV return !targetDoc ? null : ( <Tooltip title={<div className="dash-tooltip">{`Pin Document ${SelectionManager.Views().length > 1 ? 'multiple documents' : ''} to Trail`}</div>}> <div - className="documentButtonBar-icon" - style={{ color: 'white' }} - onPointerEnter={action(e => (this.expandPin = true))} - onPointerLeave={action(e => (this.expandPin = false))} + className="documentButtonBar-icon documentButtonBar-pin" onClick={e => { const docs = this.props .views() .filter(v => v) .map(dv => dv!.rootDoc); - TabDocView.PinDoc(docs, { pinDocLayout: e.shiftKey, pinDocContent: e.altKey, activeFrame: Cast(docs.lastElement()?.activeFrame, 'number', null) }); + TabDocView.PinDoc(docs, { pinAudioPlay: true, pinDocLayout: e.shiftKey, pinDocContent: e.altKey, activeFrame: Cast(docs.lastElement()?.activeFrame, 'number', null) }); e.stopPropagation(); }}> - {this.expandPin ? ( - <div className="documentButtonBar-pinTypes"> - {pinBtn(true, false, 'window-maximize')} - {pinBtn(false, true, 'address-card')} - {pinBtn(true, true, 'id-card')} - </div> - ) : null} + <div className="documentButtonBar-pinTypes"> + {pinBtn(true, false, 'window-maximize')} + {pinBtn(false, true, 'address-card')} + {pinBtn(true, true, 'id-card')} + </div> <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="map-pin" /> </div> </Tooltip> @@ -293,12 +342,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV get shareButton() { const targetDoc = this.view0?.props.Document; return !targetDoc ? null : ( - <Tooltip - title={ - <> - <div className="dash-tooltip">{'Open Sharing Manager'}</div> - </> - }> + <Tooltip title={<div className="dash-tooltip">{'Open Sharing Manager'}</div>}> <div className="documentButtonBar-icon" style={{ color: 'white' }} onClick={e => SharingManager.Instance.open(this.view0, targetDoc)}> <FontAwesomeIcon className="documentdecorations-icon" icon="users" /> </div> @@ -317,34 +361,11 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV </Tooltip> ); } - - @computed - get moreButton() { - const targetDoc = this.view0?.props.Document; - return !targetDoc ? null : ( - <Tooltip - title={ - <> - <div className="dash-tooltip">{`${SettingsManager.propertiesWidth > 0 ? 'Close' : 'Open'} Properties Panel`}</div> - </> - }> - <div className="documentButtonBar-icon" style={{ color: 'white', cursor: 'e-resize' }} onClick={action(e => (SettingsManager.propertiesWidth = SettingsManager.propertiesWidth > 0 ? 0 : 250))}> - <FontAwesomeIcon className="documentdecorations-icon" 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> - </> - }> + <Tooltip title={<div className="dash-tooltip">Show metadata panel</div>}> <div className="documentButtonBar-linkFlyout"> <Flyout anchorPoint={anchorPoints.LEFT_TOP} @@ -367,28 +388,30 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV } @observable _isRecording = false; + _stopFunc: () => void = emptyFunction; @computed get recordButton() { const targetDoc = this.view0?.props.Document; return !targetDoc ? null : ( - <Tooltip title={<div className="dash-tooltip">{'Click to record 5 second annotation'}</div>}> + <Tooltip title={<div className="dash-tooltip">Press to record audio annotation</div>}> <div className="documentButtonBar-icon" - style={{ backgroundColor: this._isRecording ? 'red' : targetDoc.isLinkButton ? Colors.LIGHT_BLUE : Colors.DARK_GRAY, color: targetDoc.isLinkButton ? Colors.BLACK : Colors.WHITE }} - onClick={undoBatch( - action(e => { - this._isRecording = true; - this.props.views().map( - view => - view && - DocumentViewInternal.recordAudioAnnotation( - view.dataDoc, - view.LayoutFieldKey, - action(() => (this._isRecording = false)) - ) - ); - }) - )}> + style={{ backgroundColor: this._isRecording ? Colors.ERROR_RED : Colors.DARK_GRAY, color: Colors.WHITE }} + onPointerDown={action((e: React.PointerEvent) => { + this._isRecording = true; + this.props.views().map(view => view && DocumentViewInternal.recordAudioAnnotation(view.dataDoc, view.LayoutFieldKey, stopFunc => (this._stopFunc = stopFunc), emptyFunction)); + const b = UndoManager.StartBatch('Recording'); + setupMoveUpEvents( + this, + e, + returnFalse, + action(() => { + this._isRecording = false; + this._stopFunc(); + }), + emptyFunction + ); + })}> <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="microphone" /> </div> </Tooltip> @@ -454,6 +477,12 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV simulateMouseClick(child, e.clientX, e.clientY - 30, e.screenX, e.screenY - 30); }; + @observable _showLinkPopup = false; + @action + toggleLinkSearch = (e: React.PointerEvent) => { + this._showLinkPopup = !this._showLinkPopup; + e.stopPropagation(); + }; render() { if (!this.view0) return null; @@ -464,25 +493,34 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV return ( <div className="documentButtonBar"> <div className="documentButtonBar-button"> - <DocumentLinksButton View={this.view0} AlwaysOn={true} InMenu={true} StartLink={true} /> + <DocumentLinksButton View={this.view0} AlwaysOn={true} InMenu={true} ShowCount={true} /> </div> + {this._showLinkPopup ? ( + <div style={{ position: 'absolute', zIndex: 1000 }}> + <LinkPopup + key="popup" + showPopup={this._showLinkPopup} + linkCreated={link => (link.linkDisplay = !this.props.views().lastElement()?.rootDoc.isLinkButton)} + linkCreateAnchor={() => this.props.views().lastElement()?.ComponentView?.getAnchor?.()} + linkFrom={() => this.props.views().lastElement()?.rootDoc} + /> + </div> + ) : ( + <div className="documentButtonBar-button">{this.linkButton}</div> + )} + {(DocumentLinksButton.StartLink || Doc.UserDoc()['documentLinksButton-fullMenu']) && DocumentLinksButton.StartLink !== doc ? ( <div className="documentButtonBar-button"> <DocumentLinksButton View={this.view0} AlwaysOn={true} InMenu={true} StartLink={false} /> </div> ) : null} - {Doc.noviceMode ? null : <div className="documentButtonBar-button">{this.recordButton}</div>} { Doc.noviceMode ? null : <div className="documentButtonBar-button">{this.templateButton}</div> - /*<div className="documentButtonBar-button"> - {this.metadataButton} - </div> - <div className="documentButtonBar-button"> - {this.contextButton} - </div> */ + /*<div className="documentButtonBar-button"> {this.metadataButton} </div> */ } {!SelectionManager.Views()?.some(v => v.allLinks.length) ? null : <div className="documentButtonBar-button">{this.followLinkButton}</div>} <div className="documentButtonBar-button">{this.pinButton}</div> + <div className="documentButtonBar-button">{this.recordButton}</div> {!Doc.UserDoc()['documentLinksButton-fullMenu'] ? null : <div className="documentButtonBar-button">{this.shareButton}</div>} {!Doc.UserDoc()['documentLinksButton-fullMenu'] ? null : ( <div className="documentButtonBar-button" style={{ display: !considerPush ? 'none' : '' }}> @@ -493,9 +531,6 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV {this.considerGoogleDocsPull} </div> <div className="documentButtonBar-button">{this.menuButton}</div> - {/* {Doc.noviceMode ? (null) : <div className="documentButtonBar-button"> - {this.moreButton} - </div>} */} </div> ); } diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss index 4e0b061a6..ccac5ffe4 100644 --- a/src/client/views/DocumentDecorations.scss +++ b/src/client/views/DocumentDecorations.scss @@ -1,7 +1,7 @@ @import 'global/globalCssVariables'; $linkGap: 3px; -$headerHeight: 25px; +$headerHeight: 20px; $resizeHandler: 8px; .documentDecorations-Dark, @@ -25,6 +25,10 @@ $resizeHandler: 8px; align-items: center; text-align: center; font-size: 30px; + opacity: 0.1; + &:hover { + opacity: 1; + } } .documentDecorations-rotationCenter { position: absolute; @@ -72,10 +76,12 @@ $resizeHandler: 8px; height: 20px; min-width: 20px; border-radius: 100%; + opacity: 0.5; cursor: pointer; &:hover { color: #02600d; + opacity: 1; } } @@ -93,10 +99,12 @@ $resizeHandler: 8px; height: 20px; min-width: 20px; border-radius: 100%; + opacity: 0.5; cursor: pointer; &:hover { color: #a94442; + opacity: 1; } > svg { @@ -120,10 +128,12 @@ $resizeHandler: 8px; height: 20px; min-width: 20px; border-radius: 100%; + opacity: 0.5; cursor: pointer; &:hover { color: #a94442; + opacity: 1; } > svg { @@ -144,6 +154,10 @@ $resizeHandler: 8px; border-radius: 8px; outline: none; border: none; + opacity: 0.3; + &:hover { + opacity: 1; + } .documentDecorations-titleSpan, .documentDecorations-titleSpan-Dark { @@ -206,7 +220,7 @@ $resizeHandler: 8px; .documentDecorations-rightResizer { pointer-events: auto; background: $medium-gray; - opacity: 0.1; + opacity: 0.2; &:hover { opacity: 1; } @@ -248,10 +262,8 @@ $resizeHandler: 8px; } .documentDecorations-lock { - position: absolute; + position: relative; background: black; - right: 11; - top: 30px; color: gray; height: 14; width: 14; @@ -260,6 +272,8 @@ $resizeHandler: 8px; display: flex; align-items: center; flex-direction: column; + border-radius: 15%; + cursor: default; } .documentDecorations-rotationPath { @@ -276,7 +290,6 @@ $resizeHandler: 8px; .documentDecorations-bottomRightResizer { cursor: nwse-resize; background: unset; - opacity: 1; transform: scale(2); } @@ -316,7 +329,6 @@ $resizeHandler: 8px; .documentDecorations-bottomRightResizer { cursor: nwse-resize; background: unset; - opacity: 1; } .documentDecorations-topLeftResizer { @@ -332,7 +344,6 @@ $resizeHandler: 8px; .documentDecorations-topLeftResizer:hover, .documentDecorations-bottomRightResizer:hover { opacity: 1; - background: black; } .documentDecorations-bottomRightResizer { @@ -343,7 +354,6 @@ $resizeHandler: 8px; .documentDecorations-bottomLeftResizer { cursor: nesw-resize; background: unset; - opacity: 1; transform: scale(2); } @@ -359,7 +369,6 @@ $resizeHandler: 8px; .documentDecorations-topRightResizer:hover, .documentDecorations-bottomLeftResizer:hover { - background: black; opacity: 1; } @@ -413,6 +422,13 @@ $resizeHandler: 8px; gap: 5px; top: 4px; background: $light-gray; + opacity: 0.2; + pointer-events: all; + transition: opacity 1s; + &:hover { + transition: unset; + opacity: 1; + } } .linkButtonWrapper { diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 06eb6c6d7..d1f0bf2ac 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -27,7 +27,7 @@ import { Colors } from './global/globalEnums'; import { InkingStroke } from './InkingStroke'; import { InkStrokeProperties } from './InkStrokeProperties'; import { LightboxView } from './LightboxView'; -import { DocumentView } from './nodes/DocumentView'; +import { DocumentView, OpenWhereMod } from './nodes/DocumentView'; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; import { ImageBox } from './nodes/ImageBox'; import React = require('react'); @@ -70,8 +70,10 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P ); } + @observable overrideBounds = false; @computed get Bounds() { + if (this.overrideBounds) return { x: 0, y: 0, r: 0, b: 0 }; const views = SelectionManager.Views(); return views .filter(dv => dv.props.renderDepth > 0) @@ -251,17 +253,17 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P if (selectedDocs.length) { if (e.ctrlKey) { // open an alias in a new tab with Ctrl Key - CollectionDockingView.AddSplit(Doc.BestAlias(selectedDocs[0].props.Document), 'right'); + CollectionDockingView.AddSplit(Doc.BestAlias(selectedDocs[0].props.Document), OpenWhereMod.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.AddSplit(Docs.Create.FreeformDocument([alias], { title: 'Tab for ' + alias.title }), 'right'); + CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([alias], { title: 'Tab for ' + alias.title }), OpenWhereMod.right); } else if (e.altKey) { // open same document in new tab - CollectionDockingView.ToggleSplit(selectedDocs[0].props.Document, 'right'); + CollectionDockingView.ToggleSplit(selectedDocs[0].props.Document, OpenWhereMod.right); } else { var openDoc = selectedDocs[0].props.Document; if (openDoc.layoutKey === 'layout_icon') { @@ -706,24 +708,27 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P } render() { - const { b, c, r, x, y } = this.Bounds; - const bounds = { b, c, r, x, y }; + const { b, r, x, y } = this.Bounds; + const bounds = { b, r, x, y }; const seldocview = SelectionManager.Views().slice(-1)[0]; if (SnappingManager.GetIsDragging() || bounds.r - bounds.x < 1 || bounds.x === Number.MAX_VALUE || !seldocview || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) { return null; } // hide the decorations if the parent chooses to hide it or if the document itself hides it - const hideResizers = seldocview.props.hideResizeHandles || seldocview.rootDoc.hideResizeHandles || seldocview.rootDoc._isGroup || this._isRounding || this._isRotating; - const hideTitle = seldocview.props.hideDecorationTitle || seldocview.rootDoc.hideDecorationTitle || this._isRounding || this._isRotating; - const hideDocumentButtonBar = seldocview.props.hideDocumentButtonBar || seldocview.rootDoc.hideDocumentButtonBar || this._isRounding || this._isRotating; + const hideDecorations = seldocview.props.hideDecorations || seldocview.rootDoc.hideDecorations; + const hideResizers = hideDecorations || seldocview.props.hideResizeHandles || seldocview.rootDoc.hideResizeHandles || seldocview.rootDoc._isGroup || this._isRounding || this._isRotating; + const hideTitle = hideDecorations || seldocview.props.hideDecorationTitle || seldocview.rootDoc.hideDecorationTitle || this._isRounding || this._isRotating; + const hideDocumentButtonBar = hideDecorations || seldocview.props.hideDocumentButtonBar || seldocview.rootDoc.hideDocumentButtonBar || this._isRounding || this._isRotating; // if multiple documents have been opened at the same time, then don't show open button const hideOpenButton = + hideDecorations || seldocview.props.hideOpenButton || seldocview.rootDoc.hideOpenButton || SelectionManager.Views().some(docView => docView.props.Document._stayInCollection || docView.props.Document.isGroup || docView.props.Document.hideOpenButton) || this._isRounding || this._isRotating; const hideDeleteButton = + hideDecorations || this._isRounding || this._isRotating || seldocview.props.hideDeleteButton || @@ -755,24 +760,6 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P ); const colorScheme = StrCast(Doc.ActiveDashboard?.colorScheme); - const titleArea = this._editingTitle ? ( - <input - ref={this._keyinput} - className={`documentDecorations-title${colorScheme}`} - type="text" - name="dynbox" - autoComplete="on" - value={hideTitle ? '' : this._accumulatedTitle} - onBlur={e => !hideTitle && this.titleBlur()} - onChange={action(e => !hideTitle && (this._accumulatedTitle = e.target.value))} - onKeyDown={hideTitle ? emptyFunction : this.titleEntered} - onPointerDown={e => e.stopPropagation()} - /> - ) : ( - <div className="documentDecorations-title" key="title" onPointerDown={this.onTitleDown}> - <span className={`documentDecorations-titleSpan${colorScheme}`}>{`${hideTitle ? '' : this.selectionTitle}`}</span> - </div> - ); const leftBounds = this.props.boundsLeft; const topBounds = LightboxView.LightboxDoc ? 0 : this.props.boundsTop; @@ -782,7 +769,8 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P bounds.r = Math.max(bounds.x, Math.max(leftBounds, Math.min(window.innerWidth, bounds.r + borderRadiusDraggerWidth + this._resizeBorderWidth / 2) - this._resizeBorderWidth / 2 - borderRadiusDraggerWidth)); bounds.b = Math.max(bounds.y, Math.max(topBounds, Math.min(window.innerHeight, bounds.b + this._resizeBorderWidth / 2 + this._linkBoxHeight) - this._resizeBorderWidth / 2 - this._linkBoxHeight)); - const useRotation = seldocview.rootDoc.type !== DocumentType.EQUATION; // when do we want an object to not rotate? + const useLock = bounds.r - bounds.x > 135 && seldocview.props.CollectionFreeFormDocumentView; + const useRotation = !hideResizers && seldocview.rootDoc.type !== DocumentType.EQUATION && seldocview.props.CollectionFreeFormDocumentView; // when do we want an object to not rotate? const rotation = NumCast(seldocview.rootDoc._rotation); const resizerScheme = colorScheme ? 'documentDecorations-resizer' + colorScheme : ''; @@ -794,6 +782,32 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P const maxDist = Math.min((this.Bounds.r - this.Bounds.x) / 2, (this.Bounds.b - this.Bounds.y) / 2); const radiusHandle = (borderRadius / docMax) * maxDist; const radiusHandleLocation = Math.min(radiusHandle, maxDist); + + const titleArea = this._editingTitle ? ( + <input + ref={this._keyinput} + className={`documentDecorations-title${colorScheme}`} + type="text" + name="dynbox" + autoComplete="on" + value={hideTitle ? '' : this._accumulatedTitle} + onBlur={e => !hideTitle && this.titleBlur()} + onChange={action(e => !hideTitle && (this._accumulatedTitle = e.target.value))} + onKeyDown={hideTitle ? emptyFunction : this.titleEntered} + onPointerDown={e => e.stopPropagation()} + /> + ) : ( + <div className="documentDecorations-title" key="title" onPointerDown={this.onTitleDown}> + <span className={`documentDecorations-titleSpan${colorScheme}`}>{`${hideTitle ? '' : this.selectionTitle}`}</span> + {!useLock ? null : ( + <Tooltip key="lock" title={<div className="dash-tooltip">toggle ability to interact with document</div>} placement="top"> + <div className="documentDecorations-lock" style={{ color: seldocview.rootDoc._lockedPosition ? 'red' : undefined }} onPointerDown={this.onLockDown} onContextMenu={e => e.preventDefault()}> + <FontAwesomeIcon size="sm" icon="lock" /> + </div> + </Tooltip> + )} + </div> + ); return ( <div className={`documentDecorations${colorScheme}`}> <div @@ -826,7 +840,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P <div className="documentDecorations-topbar" onPointerDown={this.onContainerDown}> {hideDeleteButton ? <div /> : topBtn('close', 'times', undefined, e => this.onCloseClick(true), 'Close')} {hideResizers || hideDeleteButton ? <div /> : topBtn('minimize', 'window-maximize', undefined, e => this.onCloseClick(undefined), 'Minimize')} - {titleArea} + {hideTitle ? null : titleArea} {hideOpenButton ? <div /> : topBtn('open', 'external-link-alt', this.onMaximizeDown, undefined, 'Open in Lightbox (ctrl: as alias, shift: in new collection)')} </div> {hideResizers ? null : ( @@ -856,9 +870,6 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P onContextMenu={e => e.preventDefault()} /> )} - <div key="lock" className="documentDecorations-lock" style={{ color: seldocview.rootDoc._lockedPosition ? 'red' : undefined }} onPointerDown={this.onLockDown} onContextMenu={e => e.preventDefault()}> - <FontAwesomeIcon size="sm" icon="lock" /> - </div> {hideDocumentButtonBar ? null : ( <div diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index a29073f14..e3328fb4c 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -122,7 +122,6 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> { onPointerUp: data.pointerUp ? ScriptField.MakeScript(data.pointerUp) : undefined, onPointerDown: data.pointerDown ? ScriptField.MakeScript(data.pointerDown) : undefined, backgroundColor: data.backgroundColor, - _removeDropProperties: new List<string>(['dropAction']), dragFactory: data.dragFactory, system: true, }) diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index 8e9c18cf3..5e700e281 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -6,7 +6,7 @@ import { Id } from '../../fields/FieldSymbols'; import { InkTool } from '../../fields/InkField'; import { List } from '../../fields/List'; import { ScriptField } from '../../fields/ScriptField'; -import { Cast, PromiseValue } from '../../fields/Types'; +import { Cast, DocCast, PromiseValue } from '../../fields/Types'; import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager'; import { DocServer } from '../DocServer'; import { DocumentType } from '../documents/DocumentTypes'; @@ -26,6 +26,7 @@ import { InkStrokeProperties } from './InkStrokeProperties'; import { LightboxView } from './LightboxView'; import { MainView } from './MainView'; import { DocumentLinksButton } from './nodes/DocumentLinksButton'; +import { OpenWhereMod } from './nodes/DocumentView'; import { AnchorMenu } from './pdf/AnchorMenu'; const modifiers = ['control', 'meta', 'shift', 'alt']; @@ -139,7 +140,7 @@ export class KeyManager { document.body.focus(); break; case 'enter': { - DocumentDecorations.Instance.onCloseClick(false); + //DocumentDecorations.Instance.onCloseClick(false); break; } case 'delete': @@ -151,11 +152,6 @@ export class KeyManager { SelectionManager.DeselectAll(); } else DocumentDecorations.Instance.onCloseClick(true); }, 'backspace'); - // const selected = SelectionManager.Views().filter(dv => !dv.topMost); - // UndoManager.RunInBatch(() => { - // SelectionManager.DeselectAll(); - // selected.map(dv => !dv.props.Document._stayInCollection && dv.props.removeDocument?.(dv.props.Document)); - // }, "delete"); return { stopPropagation: true, preventDefault: true }; } break; @@ -230,7 +226,7 @@ export class KeyManager { if (document.activeElement?.tagName === 'INPUT' || document.activeElement?.tagName === 'TEXTAREA') { return { stopPropagation: false, preventDefault: false }; } - MainView.Instance.mainFreeform && CollectionDockingView.AddSplit(MainView.Instance.mainFreeform, 'right'); + MainView.Instance.mainFreeform && CollectionDockingView.AddSplit(MainView.Instance.mainFreeform, OpenWhereMod.right); break; case 'arrowleft': if (document.activeElement?.tagName === 'INPUT' || document.activeElement?.tagName === 'TEXTAREA') { @@ -250,7 +246,7 @@ export class KeyManager { if (SelectionManager.Views().length === 1 && SelectionManager.Views()[0].ComponentView?.search) { SelectionManager.Views()[0].ComponentView?.search?.('', false, false); } else { - const searchBtn = Doc.MySearcher; + const searchBtn = DocListCast(Doc.MyLeftSidebarMenu.data).find(d => d.target === Doc.MySearcher); if (searchBtn) { MainView.Instance.selectMenu(searchBtn); } diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index b83ba97e7..a6fa2f04b 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -41,7 +41,7 @@ import { InkControlPtHandles, InkEndPtHandles } from './InkControlPtHandles'; import './InkStroke.scss'; import { InkStrokeProperties } from './InkStrokeProperties'; import { InkTangentHandles } from './InkTangentHandles'; -import { DocComponentView } from './nodes/DocumentView'; +import { DocComponentView, DocFocusOptions } from './nodes/DocumentView'; import { FieldView, FieldViewProps } from './nodes/FieldView'; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; import { StyleProp } from './StyleProvider'; @@ -79,12 +79,11 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { screenToLocal = () => this.props.ScreenToLocalTransform().scale(this.props.NativeDimScaling?.() || 1); getAnchor = () => { - console.log(document.activeElement); return this._subContentView?.getAnchor?.() || this.rootDoc; }; - scrollFocus = (textAnchor: Doc, smooth: boolean) => { - return this._subContentView?.scrollFocus?.(textAnchor, smooth); + scrollFocus = (textAnchor: Doc, options: DocFocusOptions) => { + return this._subContentView?.scrollFocus?.(textAnchor, options); }; /** * @returns the center of the ink stroke in the ink document's coordinate space (not screen space, and not the ink data coordinate space); @@ -124,10 +123,6 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { */ public static toggleMask = action((inkDoc: Doc) => { inkDoc.isInkMask = !inkDoc.isInkMask; - inkDoc._backgroundColor = inkDoc.isInkMask ? 'rgba(0,0,0,0.7)' : undefined; - inkDoc.mixBlendMode = inkDoc.isInkMask ? 'hard-light' : undefined; - inkDoc.color = '#9b9b9bff'; - inkDoc._stayInCollection = inkDoc.isInkMask ? true : undefined; }); /** * Drags the a simple bezier segment of the stroke. @@ -356,7 +351,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { const markerScale = NumCast(this.layoutDoc.strokeMarkerScale, 1); const closed = InkingStroke.IsClosed(inkData); const isInkMask = BoolCast(this.layoutDoc.isInkMask); - const fillColor = isInkMask ? '#aaaaaa' : StrCast(this.layoutDoc.fillColor, 'transparent'); + const fillColor = isInkMask ? Colors.WHITE : StrCast(this.layoutDoc.fillColor, 'transparent'); const strokeColor = !closed && fillColor && fillColor !== 'transparent' ? fillColor : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color) ?? StrCast(this.layoutDoc.color); // bcz: Hack!! Not really sure why, but having fractional values for width/height of mask ink strokes causes the dragging clone (see DragManager) to be offset from where it should be. @@ -390,10 +385,9 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { 1.0, false ); - const highlightIndex = /*BoolCast(this.props.Document.isLinkButton) && */ Doc.isBrushedHighlightedDegree(this.props.Document); // bcz: Argh!! need to identify a tree view doc better than a LayoutTemlatString - const highlightColor = !highlightIndex - ? StrCast(this.layoutDoc.strokeOutlineColor, !closed && fillColor && fillColor !== 'transparent' ? StrCast(this.layoutDoc.color, 'transparent') : 'transparent') - : ['transparent', 'rgb(68, 118, 247)', 'rgb(68, 118, 247)', 'yellow', 'magenta', 'cyan', 'orange'][highlightIndex]; + const highlight = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Highlighting); + const highlightIndex = highlight?.highlightIndex; + const highlightColor = highlight?.highlightIndex ? highlight?.highlightColor : StrCast(this.layoutDoc.strokeOutlineColor, !closed && fillColor && fillColor !== 'transparent' ? StrCast(this.layoutDoc.color, 'transparent') : 'transparent'); // Invisible polygonal line that enables the ink to be selected by the user. const clickableLine = (downHdlr?: (e: React.PointerEvent) => void, suppressFill: boolean = false) => InteractionUtils.CreatePolyline( @@ -402,7 +396,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { inkTop, highlightColor, inkStrokeWidth, - Math.max(5, fillColor && closed && highlightIndex ? highlightIndex / 2 : inkStrokeWidth + (fillColor ? (closed ? 0 : highlightIndex + 2) : 0)), + inkStrokeWidth + (fillColor ? (closed ? 2 : (highlightIndex ?? 0) + 2) : 2), StrCast(this.layoutDoc.strokeLineJoin), StrCast(this.layoutDoc.strokeLineCap), StrCast(this.layoutDoc.strokeBezier), @@ -444,9 +438,9 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { // mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? 'multiply' : 'unset', cursor: this.props.isSelected() ? 'default' : undefined, }} - {...(!closed ? interactions : {})}> - {closed ? inkLine : clickableLine(this.onPointerDown)} - {closed ? clickableLine(this.onPointerDown) : inkLine} + {...interactions}> + {clickableLine(this.onPointerDown)} + {inkLine} </svg> {!closed || (!RTFCast(this.rootDoc.text)?.Text && !this.props.isSelected()) ? null : ( <div @@ -473,19 +467,6 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { /> </div> )} - {!closed ? null : ( - <svg - className="inkStroke" - style={{ - transform: this.props.Document.isInkMask ? `translate(${InkingStroke.MaskDim / 2}px, ${InkingStroke.MaskDim / 2}px)` : undefined, - mixBlendMode: 'unset', - cursor: this.props.isSelected() ? 'default' : undefined, - position: 'absolute', - }} - {...interactions}> - {clickableLine(this.onPointerDown, true)} - </svg> - )} </div> ); } diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx index 22b0380a2..d79b696a3 100644 --- a/src/client/views/LightboxView.tsx +++ b/src/client/views/LightboxView.tsx @@ -4,7 +4,8 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast, Opt } from '../../fields/Doc'; import { InkTool } from '../../fields/InkField'; -import { Cast, NumCast, StrCast } from '../../fields/Types'; +import { List } from '../../fields/List'; +import { Cast, DocCast, NumCast, StrCast } from '../../fields/Types'; import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../Utils'; import { DocUtils } from '../documents/Documents'; import { DocumentManager } from '../util/DocumentManager'; @@ -17,7 +18,7 @@ import { TabDocView } from './collections/TabDocView'; import { GestureOverlay } from './GestureOverlay'; import './LightboxView.scss'; import { MainView } from './MainView'; -import { DocumentView } from './nodes/DocumentView'; +import { DocumentView, OpenWhere, OpenWhereMod } from './nodes/DocumentView'; import { DefaultStyleProvider, wavyBorderPath } from './StyleProvider'; interface LightboxViewProps { @@ -49,7 +50,6 @@ export class LightboxView extends React.Component<LightboxViewProps> { this.LightboxDoc._panY = this._savedState.panY; this.LightboxDoc._scrollTop = this._savedState.scrollTop; this.LightboxDoc._viewScale = this._savedState.scale; - this.LightboxDoc._viewTransition = undefined; } if (!doc) { this._docFilters && (this._docFilters.length = 0); @@ -140,7 +140,14 @@ export class LightboxView extends React.Component<LightboxViewProps> { this._docFilters = (f => (this._docFilters ? [this._docFilters.push(f) as any, this._docFilters][1] : [f]))(`cookies:${cookie}:provide`); } } - public static AddDocTab = (doc: Doc, location: string, layoutTemplate?: Doc, openInTabFunc?: any) => { + public static AddDocTab = (doc: Doc, location: OpenWhere, layoutTemplate?: Doc, openInTabFunc?: any) => { + if (location !== OpenWhere.lightbox) { + const inPlaceView = DocCast(doc.context) ? DocumentManager.Instance.getFirstDocumentView(DocCast(doc.context)) : undefined; + if (inPlaceView) { + inPlaceView.dataDoc[Doc.LayoutFieldKey(inPlaceView.rootDoc)] = new List<Doc>([doc]); + return true; + } + } LightboxView.openInTabFunc = openInTabFunc; SelectionManager.DeselectAll(); return LightboxView.SetLightboxDoc( @@ -159,7 +166,7 @@ export class LightboxView extends React.Component<LightboxViewProps> { if (targetDocView && target) { const l = DocUtils.MakeLinkToActiveAudio(() => targetDocView.ComponentView?.getAnchor?.() || target).lastElement(); l && (Cast(l.anchor2, Doc, null).backgroundColor = 'lightgreen'); - targetDocView.focus(target, { originalTarget: target, willZoom: true, scale: 0.9 }); + targetDocView.focus(target, { originalTarget: target, willPanZoom: true, zoomScale: 0.9 }); if (LightboxView._history?.lastElement().target !== target) LightboxView._history?.push({ doc, target }); } else { if (!target && LightboxView.path.length) { @@ -169,7 +176,6 @@ export class LightboxView extends React.Component<LightboxViewProps> { LightboxView.LightboxDoc._panY = saved.panY; LightboxView.LightboxDoc._viewScale = saved.scale; LightboxView.LightboxDoc._scrollTop = saved.scrollTop; - LightboxView.LightboxDoc._viewTransition = undefined; } const pop = LightboxView.path.pop(); if (pop) { @@ -203,7 +209,7 @@ export class LightboxView extends React.Component<LightboxViewProps> { if (docView) { LightboxView._docTarget = target; if (!target) docView.ComponentView?.shrinkWrap?.(); - else docView.focus(target, { willZoom: true, scale: 0.9 }); + else docView.focus(target, { willPanZoom: true, zoomScale: 0.9 }); } else { LightboxView.SetLightboxDoc(doc, target); } @@ -282,7 +288,7 @@ export class LightboxView extends React.Component<LightboxViewProps> { const target = LightboxView._docTarget; const doc = LightboxView._doc; //const targetView = target && DocumentManager.Instance.getLightboxDocumentView(target); - if (doc === r.props.Document && (!target || target === doc)) r.ComponentView?.shrinkWrap?.(); + //if (doc === r.props.Document && (!target || target === doc)) r.ComponentView?.shrinkWrap?.(); //else target?.focus(target, { willZoom: true, scale: 0.9, instant: true }); // bcz: why was this here? it breaks smooth navigation in lightbox using 'next' button }) ); @@ -292,7 +298,7 @@ export class LightboxView extends React.Component<LightboxViewProps> { PanelWidth={this.lightboxWidth} PanelHeight={this.lightboxHeight} LayoutTemplate={LightboxView.LightboxDocTemplate} - isDocumentActive={returnFalse} + isDocumentActive={returnTrue} // without this being true, sidebar annotations need to be activated before text can be selected. isContentActive={returnTrue} styleProvider={DefaultStyleProvider} ScreenToLocalTransform={this.lightboxScreenToLocal} @@ -354,7 +360,7 @@ export class LightboxView extends React.Component<LightboxViewProps> { title={'open in tab'} onClick={e => { e.stopPropagation(); - CollectionDockingView.AddSplit(LightboxView._docTarget || LightboxView._doc!, ''); + CollectionDockingView.AddSplit(LightboxView._docTarget || LightboxView._doc!, OpenWhereMod.none); //LightboxView.openInTabFunc(LightboxView._docTarget || LightboxView._doc!, "inPlace"); SelectionManager.DeselectAll(); LightboxView.SetLightboxDoc(undefined); diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 3e6539c5e..e0b4c5159 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -3,9 +3,9 @@ // } import * as React from 'react'; -import * as ReactDOM from 'react-dom'; +import * as ReactDOM from 'react-dom/client'; import { AssignAllExtensions } from '../../extensions/General/Extensions'; -import { Utils } from '../../Utils'; +import { FieldLoader } from '../../fields/FieldLoader'; import { DocServer } from '../DocServer'; import { Docs } from '../documents/Documents'; import { CurrentUserUtils } from '../util/CurrentUserUtils'; @@ -14,13 +14,15 @@ import { ReplayMovements } from '../util/ReplayMovements'; import { TrackMovements } from '../util/TrackMovements'; import { CollectionView } from './collections/CollectionView'; import { MainView } from './MainView'; -import * as dotenv from 'dotenv' // see https://github.com/motdotla/dotenv#how-do-i-use-dotenv-with-import +import * as dotenv from 'dotenv'; // see https://github.com/motdotla/dotenv#how-do-i-use-dotenv-with-import dotenv.config(); AssignAllExtensions(); +FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0 }; // bcz: not sure why this is needed to get the code loaded properly... (async () => { MainView.Live = window.location.search.includes('live'); + ReactDOM.createRoot(document.getElementById('root')!).render(<FieldLoader />); window.location.search.includes('safe') && CollectionView.SetSafeMode(true); const info = await CurrentUserUtils.loadCurrentUser(); if (info.email === 'guest') DocServer.Control.makeReadOnly(); @@ -45,6 +47,6 @@ AssignAllExtensions(); new LinkManager(); new TrackMovements(); new ReplayMovements(); - ReactDOM.render(<MainView />, document.getElementById('root')); + ReactDOM.createRoot(document.getElementById('root')!).render(<MainView />); }, 0); })(); diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss index c5ac1cf52..b95ce0e99 100644 --- a/src/client/views/MainView.scss +++ b/src/client/views/MainView.scss @@ -1,5 +1,12 @@ @import 'global/globalCssVariables'; @import 'nodeModuleOverrides'; +html { + overscroll-behavior-x: none; +} +body { + overscroll-behavior-x: none; +} + h1, .h1 { // reverts change to h1 made by normalize.css @@ -371,12 +378,3 @@ h1, width: 200px; height: 800px; } - -.mainVew-invisibleWebRef { - position: absolute; - left: 50; - top: 50; - display: block; - width: 500px; - height: 1000px; -} diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 4dc1ebd99..4494166f2 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -3,15 +3,15 @@ import { faBuffer, faHireAHelper } from '@fortawesome/free-brands-svg-icons'; import * as far from '@fortawesome/free-regular-svg-icons'; import * as fa from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import 'browndash-components/dist/styles/global.min.css'; import { action, computed, configure, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import 'normalize.css'; import * as React from 'react'; -import * as ReactDOM from 'react-dom'; import { Doc, DocListCast, Opt } from '../../fields/Doc'; import { ScriptField } from '../../fields/ScriptField'; import { DocCast, StrCast } from '../../fields/Types'; -import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, returnZero, setupMoveUpEvents, simulateMouseClick, Utils } from '../../Utils'; +import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, returnZero, setupMoveUpEvents, Utils } from '../../Utils'; import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager'; import { DocServer } from '../DocServer'; import { Docs, DocUtils } from '../documents/Documents'; @@ -49,7 +49,7 @@ import { LinkMenu } from './linking/LinkMenu'; import './MainView.scss'; import { AudioBox } from './nodes/AudioBox'; import { DocumentLinksButton } from './nodes/DocumentLinksButton'; -import { DocumentView } from './nodes/DocumentView'; +import { DocumentView, OpenWhere, OpenWhereMod } from './nodes/DocumentView'; import { DashFieldViewMenu } from './nodes/formattedText/DashFieldView'; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; import { RichTextMenu } from './nodes/formattedText/RichTextMenu'; @@ -58,14 +58,12 @@ import { LinkDocPreview } from './nodes/LinkDocPreview'; import { RadialMenu } from './nodes/RadialMenu'; import { TaskCompletionBox } from './nodes/TaskCompletedBox'; import { PresBox } from './nodes/trails'; -import { WebBox } from './nodes/WebBox'; import { OverlayView } from './OverlayView'; import { AnchorMenu } from './pdf/AnchorMenu'; import { PreviewCursor } from './PreviewCursor'; import { PropertiesView } from './PropertiesView'; import { DashboardStyleProvider, DefaultStyleProvider } from './StyleProvider'; import { TopBar } from './topbar/TopBar'; -import 'browndash-components/dist/styles/global.min.css'; const _global = (window /* browser */ || global) /* node */ as any; @observer @@ -208,7 +206,7 @@ export class MainView extends React.Component { 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 => (doc instanceof Doc ? DocumentManager.Instance.jumpToDocument(doc, false, undefined, []) : null)); + DocServer.GetRefField(id).then(doc => (doc instanceof Doc ? DocumentManager.Instance.jumpToDocument(doc, { willPan: false }, undefined, []) : null)); }); document.addEventListener('linkAnnotationToDash', Hypothesis.linkListener); this.initEventListeners(); @@ -484,6 +482,8 @@ export class MainView extends React.Component { } globalPointerDown = action((e: PointerEvent) => { + DocumentManager.removeOverlayViews(); + Doc.linkFollowUnhighlight(); AudioBox.Enabled = true; const targets = document.elementsFromPoint(e.x, e.y); if (targets.length) { @@ -537,7 +537,7 @@ export class MainView extends React.Component { @action createNewPresentation = () => { const pres = Doc.MakeCopy(Doc.UserDoc().emptyTrail as Doc, true); - CollectionDockingView.AddSplit(pres, 'right'); + CollectionDockingView.AddSplit(pres, OpenWhereMod.right); Doc.MyTrails && Doc.AddDocToList(Doc.MyTrails, 'data', pres); // Doc.MyTrails should be created in createDashboard Doc.ActivePresentation = pres; }; @@ -545,7 +545,7 @@ export class MainView extends React.Component { @action openPresentation = (pres: Doc) => { if (pres.type === DocumentType.PRES) { - CollectionDockingView.AddSplit(pres, 'right'); + CollectionDockingView.AddSplit(pres, OpenWhereMod.right); Doc.MyTrails && (Doc.ActivePresentation = pres); Doc.AddDocToList(Doc.MyTrails, 'data', pres); this.closeFlyout(); @@ -574,7 +574,7 @@ export class MainView extends React.Component { Document={this.headerBarDoc} DataDoc={undefined} addDocument={undefined} - addDocTab={this.addDocTabFunc} + addDocTab={MainView.addDocTabFunc} pinToPres={emptyFunction} docViewPath={returnEmptyDoclist} styleProvider={DefaultStyleProvider} @@ -611,7 +611,7 @@ export class MainView extends React.Component { Document={this.mainContainer!} DataDoc={undefined} addDocument={undefined} - addDocTab={this.addDocTabFunc} + addDocTab={MainView.addDocTabFunc} pinToPres={emptyFunction} docViewPath={returnEmptyDoclist} styleProvider={this._hideUI ? DefaultStyleProvider : undefined} @@ -682,20 +682,19 @@ export class MainView extends React.Component { sidebarScreenToLocal = () => new Transform(0, -this.topOfSidebarDoc, 1); mainContainerXf = () => this.sidebarScreenToLocal().translate(-this.leftScreenOffsetOfMainDocView, 0); - addDocTabFunc = (doc: Doc, location: string): boolean => { - const locationFields = doc._viewType === CollectionViewType.Docking ? ['dashboard'] : location.split(':'); - const locationParams = locationFields.length > 1 ? locationFields[1] : ''; + static addDocTabFunc = (doc: Doc, location: OpenWhere): boolean => { + const whereFields = doc._viewType === CollectionViewType.Docking ? [OpenWhere.dashboard] : location.split(':'); + const whereMods = whereFields.length > 1 ? (whereFields[1] as OpenWhereMod) : OpenWhereMod.none; if (doc.dockingConfig) return DashboardView.openDashboard(doc); // prettier-ignore - switch (locationFields[0]) { - default: - case 'inPlace': - case 'add': return CollectionDockingView.AddSplit(doc, locationParams); - case 'dashboard': return DashboardView.openDashboard(doc); - case 'close': return CollectionDockingView.CloseSplit(doc, locationParams); - case 'fullScreen': return CollectionDockingView.OpenFullScreen(doc); - case 'lightbox': return LightboxView.AddDocTab(doc, location); - case 'toggle': return CollectionDockingView.ToggleSplit(doc, locationParams); + switch (whereFields[0]) { + case OpenWhere.inPlace: // fall through to lightbox + case OpenWhere.lightbox: return LightboxView.AddDocTab(doc, location); + case OpenWhere.dashboard: return DashboardView.openDashboard(doc); + case OpenWhere.fullScreen: return CollectionDockingView.OpenFullScreen(doc); + case OpenWhere.close: return CollectionDockingView.CloseSplit(doc, whereMods); + case OpenWhere.toggle: return CollectionDockingView.ToggleSplit(doc, whereMods); + case OpenWhere.add:default:return CollectionDockingView.AddSplit(doc, whereMods); } }; @@ -711,7 +710,7 @@ export class MainView extends React.Component { Document={this._sidebarContent.proto || this._sidebarContent} DataDoc={undefined} addDocument={undefined} - addDocTab={this.addDocTabFunc} + addDocTab={MainView.addDocTabFunc} pinToPres={emptyFunction} docViewPath={returnEmptyDoclist} styleProvider={this._sidebarContent.proto === Doc.MyDashboards || this._sidebarContent.proto === Doc.MyFilesystem ? DashboardStyleProvider : DefaultStyleProvider} @@ -745,7 +744,7 @@ export class MainView extends React.Component { Document={Doc.MyLeftSidebarMenu} DataDoc={undefined} addDocument={undefined} - addDocTab={this.addDocTabFunc} + addDocTab={MainView.addDocTabFunc} pinToPres={emptyFunction} rootSelected={returnTrue} removeDocument={returnFalse} @@ -809,7 +808,7 @@ export class MainView extends React.Component { </div> )} <div className="properties-container" style={{ width: this.propertiesWidth() }}> - {this.propertiesWidth() < 10 ? null : <PropertiesView styleProvider={DefaultStyleProvider} addDocTab={this.addDocTabFunc} width={this.propertiesWidth()} height={this.propertiesHeight()} />} + {this.propertiesWidth() < 10 ? null : <PropertiesView styleProvider={DefaultStyleProvider} addDocTab={MainView.addDocTabFunc} width={this.propertiesWidth()} height={this.propertiesHeight()} />} </div> </div> </div> @@ -841,15 +840,12 @@ export class MainView extends React.Component { } expandFlyout = action((button: Doc) => { - // bcz: What's going on here!? + // bcz: What's going on here!? --- may be fixed now, so commenting out ... // Chrome(not firefox) seems to have a bug when the flyout expands and there's a zoomed freeform tab. All of the div below the CollectionFreeFormView's main div // generate the wrong value from getClientRectangle() -- specifically they return an 'x' that is the flyout's width greater than it should be. // interactively adjusting the flyout fixes the problem. So does programmatically changing the value after a timeout to something *fractionally* different (ie, 1.5, not 1);) this._leftMenuFlyoutWidth = this._leftMenuFlyoutWidth || 250; - setTimeout( - action(() => (this._leftMenuFlyoutWidth += 0.5)), - 0 - ); + //setTimeout(action(() => (this._leftMenuFlyoutWidth += 0.5))); this._sidebarContent.proto = button.target as any; this.LastButton = button; @@ -892,7 +888,7 @@ export class MainView extends React.Component { moveDocument={this.moveButtonDoc} CollectionView={undefined} addDocument={this.addButtonDoc} - addDocTab={this.addDocTabFunc} + addDocTab={MainView.addDocTabFunc} pinToPres={emptyFunction} removeDocument={this.remButtonDoc} ScreenToLocalTransform={this.buttonBarXf} @@ -951,40 +947,6 @@ export class MainView extends React.Component { ); } - @computed get invisibleWebBox() { - // see note under the makeLink method in HypothesisUtils.ts - return !DocumentLinksButton.invisibleWebDoc ? null : ( - <div className="mainView-invisibleWebRef" ref={DocumentLinksButton.invisibleWebRef}> - <WebBox - fieldKey={'data'} - ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} - Document={DocumentLinksButton.invisibleWebDoc} - dropAction={'move'} - styleProvider={undefined} - isSelected={returnFalse} - select={returnFalse} - setHeight={returnFalse} - rootSelected={returnFalse} - renderDepth={0} - addDocTab={returnFalse} - pinToPres={returnFalse} - ScreenToLocalTransform={Transform.Identity} - bringToFront={returnFalse} - isContentActive={emptyFunction} - whenChildContentsActiveChanged={returnFalse} - focus={returnFalse} - docViewPath={returnEmptyDoclist} - PanelWidth={() => 500} - PanelHeight={() => 800} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} - searchFilterDocs={returnEmptyDoclist} - /> - </div> - ); - } - render() { return ( <div @@ -1040,68 +1002,10 @@ export class MainView extends React.Component { <RichTextMenu /> <InkTranscription /> {this.snapLines} - <div className="mainView-webRef" ref={this.makeWebRef} /> <LightboxView key="lightbox" PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} maxBorder={[200, 50]} /> </div> ); } - - makeWebRef = (ele: HTMLDivElement) => { - reaction( - () => DocumentLinksButton.invisibleWebDoc, - invisibleDoc => { - ReactDOM.unmountComponentAtNode(ele); - invisibleDoc && - ReactDOM.render( - <span title="Drag as document" className="invisible-webbox"> - <div className="mainView-webRef" ref={DocumentLinksButton.invisibleWebRef}> - <WebBox - fieldKey={'data'} - ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} - Document={invisibleDoc} - dropAction={'move'} - isSelected={returnFalse} - docViewPath={returnEmptyDoclist} - select={returnFalse} - rootSelected={returnFalse} - renderDepth={0} - setHeight={returnFalse} - styleProvider={undefined} - addDocTab={returnFalse} - pinToPres={returnFalse} - ScreenToLocalTransform={Transform.Identity} - bringToFront={returnFalse} - isContentActive={emptyFunction} - whenChildContentsActiveChanged={returnFalse} - focus={returnFalse} - PanelWidth={() => 500} - PanelHeight={() => 800} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} - searchFilterDocs={returnEmptyDoclist} - /> - </div> - ; - </span>, - ele - ); - - 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, - // so we keep simulating clicks until annotations have loaded and editing is successful - 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); - } - ); - }; } ScriptingGlobals.add(function selectMainMenu(doc: Doc, title: string) { diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx index d9a989309..bf1242346 100644 --- a/src/client/views/MarqueeAnnotator.tsx +++ b/src/client/views/MarqueeAnnotator.tsx @@ -18,7 +18,7 @@ const _global = (window /* browser */ || global) /* node */ as any; export interface MarqueeAnnotatorProps { rootDoc: Doc; - down: number[]; + down?: number[]; iframe?: () => undefined | HTMLIFrameElement; scrollTop: number; scaling?: () => number; @@ -27,12 +27,14 @@ export interface MarqueeAnnotatorProps { mainCont: HTMLDivElement; docView: DocumentView; savedAnnotations: () => ObservableMap<number, HTMLDivElement[]>; + selectionText: () => string; annotationLayer: HTMLDivElement; addDocument: (doc: Doc) => boolean; getPageFromScroll?: (top: number) => number; finishMarquee: (x?: number, y?: number, PointerEvent?: PointerEvent) => void; anchorMenuClick?: () => undefined | ((anchor: Doc) => void); anchorMenuCrop?: (anchor: Doc | undefined, addCrop: boolean) => Doc | undefined; + highlightDragSrcColor?: string; } @observer export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { @@ -43,6 +45,17 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { @observable private _width: number = 0; @observable private _height: number = 0; + constructor(props: any) { + super(props); + + AnchorMenu.Instance.OnCrop = (e: PointerEvent) => this.props.anchorMenuCrop?.(this.highlight('', true), true); + AnchorMenu.Instance.OnClick = (e: PointerEvent) => this.props.anchorMenuClick?.()?.(this.highlight(this.props.highlightDragSrcColor ?? 'rgba(173, 216, 230, 0.75)', true)); + AnchorMenu.Instance.OnAudio = unimplementedFunction; + AnchorMenu.Instance.Highlight = this.highlight; + AnchorMenu.Instance.GetAnchor = (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => this.highlight('rgba(173, 216, 230, 0.75)', true, savedAnnotations); + AnchorMenu.Instance.onMakeAnchor = AnchorMenu.Instance.GetAnchor; + } + @action static clearAnnotations(savedAnnotations: ObservableMap<number, HTMLDivElement[]>) { AnchorMenu.Instance.Status = 'marquee'; @@ -52,24 +65,21 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { savedAnnotations.clear(); } - @action componentDidMount() { - // set marquee x and y positions to the spatially transformed position - const boundingRect = this.props.mainCont.getBoundingClientRect(); - this._startX = this._left = (this.props.down[0] - boundingRect.left) * (this.props.mainCont.offsetWidth / boundingRect.width); - this._startY = this._top = (this.props.down[1] - boundingRect.top) * (this.props.mainCont.offsetHeight / boundingRect.height) + this.props.mainCont.scrollTop; - this._height = this._width = 0; + @action gotDownPoint() { + if (!this._width && !this._height) { + const downPt = this.props.down!; + // set marquee x and y positions to the spatially transformed position + const boundingRect = this.props.mainCont.getBoundingClientRect(); + this._startX = this._left = (downPt[0] - boundingRect.left) * (this.props.mainCont.offsetWidth / boundingRect.width); + this._startY = this._top = (downPt[1] - boundingRect.top) * (this.props.mainCont.offsetHeight / boundingRect.height) + this.props.mainCont.scrollTop; + } const doc = this.props.iframe?.()?.contentDocument ?? document; + doc.removeEventListener('pointermove', this.onSelectMove); + doc.removeEventListener('pointerup', this.onSelectEnd); doc.addEventListener('pointermove', this.onSelectMove); doc.addEventListener('pointerup', this.onSelectEnd); - AnchorMenu.Instance.OnCrop = (e: PointerEvent) => this.props.anchorMenuCrop?.(this.highlight('rgba(173, 216, 230, 0.75)', true), true); - AnchorMenu.Instance.OnClick = (e: PointerEvent) => this.props.anchorMenuClick?.()?.(this.highlight('rgba(173, 216, 230, 0.75)', true)); - AnchorMenu.Instance.OnAudio = unimplementedFunction; - AnchorMenu.Instance.Highlight = this.highlight; - AnchorMenu.Instance.GetAnchor = (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => this.highlight('rgba(173, 216, 230, 0.75)', true, savedAnnotations); - AnchorMenu.Instance.onMakeAnchor = AnchorMenu.Instance.GetAnchor; - /** * This function is used by the AnchorMenu to create an anchor highlight and a new linked text annotation. * It also initiates a Drag/Drop interaction to place the text annotation. @@ -78,7 +88,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { e.preventDefault(); e.stopPropagation(); const sourceAnchorCreator = () => { - const annoDoc = this.highlight('rgba(173, 216, 230, 0.75)', true); // hyperlink color + const annoDoc = this.highlight(this.props.highlightDragSrcColor ?? 'rgba(173, 216, 230, 0.75)', true); // hyperlink color annoDoc && this.props.addDocument(annoDoc); return annoDoc; }; @@ -90,7 +100,8 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.docView, sourceAnchorCreator, targetCreator), e.pageX, e.pageY, { dragComplete: e => { if (!e.aborted && e.annoDragData && e.annoDragData.linkSourceDoc && e.annoDragData.dropDocument && e.linkDocument) { - e.annoDragData.linkSourceDoc.isPushpin = e.annoDragData.dropDocument.annotationOn === this.props.rootDoc; + e.annoDragData.linkSourceDoc.followLinkToggle = e.annoDragData.dropDocument.annotationOn === this.props.rootDoc; + e.annoDragData.linkSourceDoc.followLinkZoom = false; } }, }); @@ -106,7 +117,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { e.stopPropagation(); var cropRegion: Doc | undefined; const sourceAnchorCreator = () => { - cropRegion = this.highlight('rgba(173, 216, 230, 0.75)', true); // hyperlink color + cropRegion = this.highlight('', true); // hyperlink color cropRegion && this.props.addDocument(cropRegion); return cropRegion; }; @@ -116,12 +127,13 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { if (!e.aborted && e.linkDocument) { Doc.GetProto(e.linkDocument).linkRelationship = 'cropped image'; Doc.GetProto(e.linkDocument).title = 'crop: ' + this.props.docView.rootDoc.title; + Doc.GetProto(e.linkDocument).linkDisplay = false; } }, }); }); } - componentWillUnmount() { + releaseDownPt() { const doc = this.props.iframe?.()?.contentDocument ?? document; doc.removeEventListener('pointermove', this.onSelectMove); doc.removeEventListener('pointerup', this.onSelectEnd); @@ -147,7 +159,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { return marqueeAnno; } - const textRegionAnno = Docs.Create.HTMLAnchorDocument([], { annotationOn: this.props.rootDoc, backgroundColor: 'transparent', title: 'Selection on ' + this.props.rootDoc.title }); + const textRegionAnno = Docs.Create.HTMLAnchorDocument([], { annotationOn: this.props.rootDoc, text: this.props.selectionText(), backgroundColor: 'transparent', title: 'Selection on ' + this.props.rootDoc.title }); let minX = Number.MAX_VALUE; let maxX = -Number.MAX_VALUE; let minY = Number.MAX_VALUE; @@ -255,6 +267,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { this.highlight('rgba(245, 230, 95, 0.75)', false); // yellowish highlight color for highlighted text (should match AnchorMenu's highlight color) } this.props.finishMarquee(undefined, undefined, e); + runInAction(() => (this._width = this._height = 0)); } else { runInAction(() => (this._width = this._height = 0)); this.props.finishMarquee(cliX, cliY, e); @@ -262,8 +275,9 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { }; render() { - return ( + return !this.props.down ? null : ( <div + ref={r => (r ? this.gotDownPoint() : this.releaseDownPt())} className="marqueeAnnotator-dragBox" style={{ left: `${this._left}px`, @@ -271,7 +285,8 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { width: `${this._width}px`, height: `${this._height}px`, border: `${this._width === 0 ? '' : '2px dashed black'}`, - }}/> + }} + /> ); } } diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx index 6d7f2c037..08285ff0c 100644 --- a/src/client/views/OverlayView.tsx +++ b/src/client/views/OverlayView.tsx @@ -1,4 +1,4 @@ -import { action, computed, observable } from 'mobx'; +import { action, computed, observable, trace } from 'mobx'; import { observer } from 'mobx-react'; import { computedFn } from 'mobx-utils'; import * as React from 'react'; @@ -8,15 +8,17 @@ import { Id } from '../../fields/FieldSymbols'; import { NumCast } from '../../fields/Types'; import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents, Utils } from '../../Utils'; import { DocUtils } from '../documents/Documents'; +import { DocumentType } from '../documents/DocumentTypes'; import { DragManager } from '../util/DragManager'; import { ScriptingGlobals } from '../util/ScriptingGlobals'; import { Transform } from '../util/Transform'; import { CollectionFreeFormLinksView } from './collections/collectionFreeForm/CollectionFreeFormLinksView'; import { LightboxView } from './LightboxView'; +import { MainView } from './MainView'; import { DocumentView } from './nodes/DocumentView'; import './OverlayView.scss'; import { ScriptingRepl } from './ScriptingRepl'; -import { DefaultStyleProvider } from './StyleProvider'; +import { DefaultStyleProvider, testDocProps } from './StyleProvider'; export type OverlayDisposer = () => void; @@ -170,74 +172,70 @@ export class OverlayView extends React.Component { ); @computed get overlayDocs() { - return LightboxView.LightboxDoc - ? null - : DocListCast(Doc.MyOverlayDocs?.data).map(d => { - let offsetx = 0, - offsety = 0; - const dref = React.createRef<HTMLDivElement>(); - const onPointerMove = action((e: PointerEvent, down: number[]) => { - if (e.buttons === 1) { - d.overlayX = e.clientX + offsetx; - d.overlayY = e.clientY + offsety; - } - if (e.metaKey) { - const dragData = new DragManager.DocumentDragData([d]); - dragData.offset = [-offsetx, -offsety]; - dragData.dropAction = 'move'; - dragData.removeDocument = (doc: Doc | Doc[]) => { - const docs = doc instanceof Doc ? [doc] : doc; - docs.forEach(d => Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, d)); - return true; - }; - dragData.moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => { - return dragData.removeDocument!(doc) ? addDocument(doc) : false; - }; - DragManager.StartDocumentDrag([dref.current!], dragData, down[0], down[1]); - return true; - } - return false; - }); + return DocListCast(Doc.MyOverlayDocs?.data) + .filter(d => !LightboxView.LightboxDoc || d.type === DocumentType.PRES) + .map(d => { + let offsetx = 0, + offsety = 0; + const dref = React.createRef<HTMLDivElement>(); + const onPointerMove = action((e: PointerEvent, down: number[]) => { + if (e.buttons === 1) { + d.overlayX = e.clientX + offsetx; + d.overlayY = e.clientY + offsety; + } + if (e.metaKey) { + const dragData = new DragManager.DocumentDragData([d]); + dragData.offset = [-offsetx, -offsety]; + dragData.dropAction = 'move'; + dragData.removeDocument = this.removeOverlayDoc; + dragData.moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => { + return dragData.removeDocument!(doc) ? addDocument(doc) : false; + }; + DragManager.StartDocumentDrag([dref.current!], dragData, down[0], down[1]); + return true; + } + return false; + }); - const onPointerDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, onPointerMove, emptyFunction, emptyFunction); - offsetx = NumCast(d.overlayX) - e.clientX; - offsety = NumCast(d.overlayY) - e.clientY; - }; - return ( - <div - className="overlayView-doc" - ref={dref} - key={d[Id]} - onPointerDown={onPointerDown} - style={{ top: d.type === 'presentation' ? 0 : undefined, width: NumCast(d._width), height: NumCast(d._height), transform: `translate(${d.overlayX}px, ${d.overlayY}px)` }}> - <DocumentView - Document={d} - rootSelected={returnTrue} - bringToFront={emptyFunction} - addDocument={undefined} - removeDocument={this.removeOverlayDoc} - PanelWidth={d[WidthSym]} - PanelHeight={d[HeightSym]} - ScreenToLocalTransform={this.docScreenToLocalXf(d)} - renderDepth={1} - isDocumentActive={returnTrue} - isContentActive={returnTrue} - whenChildContentsActiveChanged={emptyFunction} - focus={DocUtils.DefaultFocus} - styleProvider={DefaultStyleProvider} - docViewPath={returnEmptyDoclist} - addDocTab={returnFalse} - pinToPres={emptyFunction} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} - searchFilterDocs={returnEmptyDoclist} - ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} - /> - </div> - ); - }); + const onPointerDown = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, onPointerMove, emptyFunction, emptyFunction); + offsetx = NumCast(d.overlayX) - e.clientX; + offsety = NumCast(d.overlayY) - e.clientY; + }; + return ( + <div + className="overlayView-doc" + ref={dref} + key={d[Id]} + onPointerDown={onPointerDown} + style={{ top: d.type === DocumentType.PRES ? 0 : undefined, width: NumCast(d._width), height: NumCast(d._height), transform: `translate(${d.overlayX}px, ${d.overlayY}px)` }}> + <DocumentView + Document={d} + rootSelected={returnTrue} + bringToFront={emptyFunction} + addDocument={undefined} + removeDocument={this.removeOverlayDoc} + PanelWidth={d[WidthSym]} + PanelHeight={d[HeightSym]} + ScreenToLocalTransform={this.docScreenToLocalXf(d)} + renderDepth={1} + isDocumentActive={returnTrue} + isContentActive={returnTrue} + whenChildContentsActiveChanged={emptyFunction} + focus={DocUtils.DefaultFocus} + styleProvider={DefaultStyleProvider} + docViewPath={returnEmptyDoclist} + addDocTab={d.type === DocumentType.PRES ? MainView.addDocTabFunc : returnFalse} + pinToPres={emptyFunction} + docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} + /> + </div> + ); + }); } public static ShowSpinner() { diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx index 119476210..3712fff58 100644 --- a/src/client/views/PreviewCursor.tsx +++ b/src/client/views/PreviewCursor.tsx @@ -100,9 +100,9 @@ export class PreviewCursor extends React.Component<{}> { batch.end(); e.stopPropagation(); } else { - // creates text document FormattedTextBox.PasteOnLoad = e; - UndoManager.RunInBatch(() => PreviewCursor._addLiveTextDoc(DocUtils.GetNewTextDoc('-pasted text-', newPoint[0], newPoint[1], 500, undefined, undefined, undefined, 750)), 'paste'); + if (e.clipboardData.getData('dash/pdfAnchor')) e.preventDefault(); + UndoManager.RunInBatch(() => PreviewCursor._addLiveTextDoc(DocUtils.GetNewTextDoc('', newPoint[0], newPoint[1], 500, undefined, undefined, undefined, 750)), 'paste'); } } //pasting in images diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx index 80c2c7705..66c3ed439 100644 --- a/src/client/views/PropertiesButtons.tsx +++ b/src/client/views/PropertiesButtons.tsx @@ -2,7 +2,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; -import { Doc, Opt } from '../../fields/Doc'; +import { Doc, DocListCast, Opt } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; import { InkField } from '../../fields/InkField'; import { RichTextField } from '../../fields/RichTextField'; @@ -14,7 +14,7 @@ import { SelectionManager } from '../util/SelectionManager'; import { undoBatch } from '../util/UndoManager'; import { Colors } from './global/globalEnums'; import { InkingStroke } from './InkingStroke'; -import { DocumentView } from './nodes/DocumentView'; +import { DocumentView, OpenWhere } from './nodes/DocumentView'; import { VideoBox } from './nodes/VideoBox'; import { pasteImageBitmap } from './nodes/WebBoxRenderer'; import './PropertiesButtons.scss'; @@ -93,6 +93,14 @@ export class PropertiesButtons extends React.Component<{}, {}> { on => 'lock' ); } + @computed get forceActiveButton() { + return this.propertyToggleBtn( + 'Active', + '_forceActive', + on => `${on ? 'Select to activate' : 'Contents always active'} `, + on => 'eye' + ); + } @computed get fitContentButton() { return this.propertyToggleBtn( 'View All', @@ -101,6 +109,43 @@ export class PropertiesButtons extends React.Component<{}, {}> { on => 'eye' ); } + // this implments a container pattern by marking the targetDoc (collection) as an inPlace container, + // and then making the contained collection be a "menu" such that when any of its contents are clicked, + // they will open their targets in the outer container. To get back to the "menu", you click on the main container. + @computed get inPlaceContainerButton() { + return this.propertyToggleBtn( + 'In Place', + 'isInPlaceContainer', + on => `${on ? 'Make' : 'Remove'} in place container flag`, + on => 'window-restore', + onClick => { + SelectionManager.Views().forEach(dv => { + const containerDoc = dv.rootDoc; + containerDoc.followAllLinks = + containerDoc.noShadow = + containerDoc.noHighlighting = + containerDoc._isLinkButton = + containerDoc._fitContentsToBox = + containerDoc._forceActive = + containerDoc._isInPlaceContainer = + !containerDoc._isInPlaceContainer; + containerDoc.followLinkLocation = containerDoc._isInPlaceContainer ? OpenWhere.inPlace : undefined; + containerDoc._xPadding = containerDoc._yPadding = containerDoc._isInPlaceContainer ? 10 : undefined; + const menuDoc = DocListCast(dv.dataDoc[dv.props.fieldKey ?? Doc.LayoutFieldKey(containerDoc)]).lastElement(); + if (menuDoc) { + menuDoc.hideDecorations = menuDoc._forceActive = menuDoc._fitContentsToBox = menuDoc._isLinkButton = menuDoc._noShadow = menuDoc.noHighlighting = containerDoc._isInPlaceContainer; + if (!dv.allLinks.find(link => link.anchor1 === menuDoc || link.anchor2 === menuDoc)) { + DocUtils.MakeLink({ doc: dv.rootDoc }, { doc: menuDoc }, 'back link to container'); + } + DocListCast(menuDoc[Doc.LayoutFieldKey(menuDoc)]).forEach(menuItem => { + menuItem.followLinkAudio = menuItem.followAllLinks = menuItem._isLinkButton = true; + menuItem._followLinkLocation = OpenWhere.inPlace; + }); + } + }); + } + ); + } @computed get fitWidthButton() { return this.propertyToggleBtn( 'Fit\xA0Width', @@ -256,7 +301,7 @@ export class PropertiesButtons extends React.Component<{}, {}> { docView.setToggleDetail(); break; case 'linkInPlace': - docView.toggleFollowLink('inPlace', true, false); + docView.toggleFollowLink('inPlace', false, false); break; case 'linkOnRight': docView.toggleFollowLink('add:right', false, false); @@ -277,7 +322,7 @@ export class PropertiesButtons extends React.Component<{}, {}> { ['nothing', 'Select Document'], ['enterPortal', 'Enter Portal'], ['toggleDetail', 'Toggle Detail'], - ['linkInPlace', 'Follow Link'], + ['linkInPlace', 'Open in Place'], ['linkOnRight', 'Open Link on Right'], ]; const currentSelection = this.selectedDoc.onClickBehavior; @@ -360,7 +405,9 @@ export class PropertiesButtons extends React.Component<{}, {}> { {toggle(this.onClickButton)} {toggle(this.fitWidthButton)} {toggle(this.freezeThumb)} + {toggle(this.forceActiveButton, { display: !isFreeForm && !isMap ? 'none' : '' })} {toggle(this.fitContentButton, { display: !isFreeForm && !isMap ? 'none' : '' })} + {toggle(this.inPlaceContainerButton, { display: !isFreeForm && !isMap ? 'none' : '' })} {toggle(this.autoHeightButton, { display: !isText && !isStacking && !isTree ? 'none' : '' })} {toggle(this.maskButton, { display: !isInk ? 'none' : '' })} {toggle(this.chromeButton, { display: !isCollection || isNovice ? 'none' : '' })} diff --git a/src/client/views/PropertiesDocBacklinksSelector.tsx b/src/client/views/PropertiesDocBacklinksSelector.tsx index 4ead8eaf0..f7173a593 100644 --- a/src/client/views/PropertiesDocBacklinksSelector.tsx +++ b/src/client/views/PropertiesDocBacklinksSelector.tsx @@ -1,20 +1,20 @@ -import { computed } from "mobx"; -import { observer } from "mobx-react"; -import * as React from "react"; -import { Doc, DocListCast } from "../../fields/Doc"; -import { Cast } from "../../fields/Types"; -import { emptyFunction } from "../../Utils"; -import { DocumentType } from "../documents/DocumentTypes"; -import { LinkManager } from "../util/LinkManager"; -import { SelectionManager } from "../util/SelectionManager"; -import { LinkMenu } from "./linking/LinkMenu"; +import { computed } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { Doc, DocListCast } from '../../fields/Doc'; +import { Cast } from '../../fields/Types'; +import { DocumentType } from '../documents/DocumentTypes'; +import { LinkManager } from '../util/LinkManager'; +import { SelectionManager } from '../util/SelectionManager'; +import { LinkMenu } from './linking/LinkMenu'; +import { OpenWhere, OpenWhereMod } from './nodes/DocumentView'; import './PropertiesDocBacklinksSelector.scss'; type PropertiesDocBacklinksSelectorProps = { - Document: Doc, - Stack?: any, - hideTitle?: boolean, - addDocTab(doc: Doc, location: string): void + Document: Doc; + Stack?: any; + hideTitle?: boolean; + addDocTab(doc: Doc, location: OpenWhere): void; }; @observer @@ -40,14 +40,16 @@ export class PropertiesDocBacklinksSelector extends React.Component<PropertiesDo if (otherdoc) { otherdoc.hidden = false; - this.props.addDocTab(Doc.IsPrototype(otherdoc) ? Doc.MakeDelegate(otherdoc) : otherdoc, "toggle:right"); + this.props.addDocTab(Doc.IsPrototype(otherdoc) ? Doc.MakeDelegate(otherdoc) : otherdoc, (OpenWhere.toggle + ':' + OpenWhereMod.right) as OpenWhere); } - } + }; render() { - return !SelectionManager.Views().length ? (null) : <div> - {this.props.hideTitle ? (null) : <p key="contexts">Contexts:</p>} - <LinkMenu docView={SelectionManager.Views().lastElement()} clearLinkEditor={emptyFunction} itemHandler={this.getOnClick} position={{ x: 0 }} /> - </div>; + return !SelectionManager.Views().length ? null : ( + <div className="preroptiesDocBacklinksSelector"> + {this.props.hideTitle ? null : <p key="contexts">Contexts:</p>} + <LinkMenu docView={SelectionManager.Views().lastElement()} clearLinkEditor={undefined} itemHandler={this.getOnClick} style={{ left: 0, top: 0 }} /> + </div> + ); } -}
\ No newline at end of file +} diff --git a/src/client/views/PropertiesDocContextSelector.tsx b/src/client/views/PropertiesDocContextSelector.tsx index 9d89ee036..2c7da5931 100644 --- a/src/client/views/PropertiesDocContextSelector.tsx +++ b/src/client/views/PropertiesDocContextSelector.tsx @@ -7,14 +7,14 @@ import { Cast, NumCast, StrCast } from '../../fields/Types'; import { CollectionViewType } from '../documents/DocumentTypes'; import { DocFocusOrOpen } from '../util/DocumentManager'; import { CollectionDockingView } from './collections/CollectionDockingView'; -import { DocumentView } from './nodes/DocumentView'; +import { DocumentView, OpenWhere, OpenWhereMod } from './nodes/DocumentView'; import './PropertiesDocContextSelector.scss'; type PropertiesDocContextSelectorProps = { DocView?: DocumentView; Stack?: any; hideTitle?: boolean; - addDocTab(doc: Doc, location: string): void; + addDocTab(doc: Doc, location: OpenWhere): void; }; @observer @@ -53,7 +53,7 @@ export class PropertiesDocContextSelector extends React.Component<PropertiesDocC col._panY = NumCast(target.y) + NumCast(target._height) / 2; } col.hidden = false; - this.props.addDocTab(col, 'toggle:right'); + this.props.addDocTab(col, (OpenWhere.toggle + ':' + OpenWhereMod.right) as OpenWhere); setTimeout(() => DocFocusOrOpen(Doc.GetProto(this.props.DocView!.props.Document), col), 100); }; diff --git a/src/client/views/PropertiesView.scss b/src/client/views/PropertiesView.scss index 437df4739..897be9a32 100644 --- a/src/client/views/PropertiesView.scss +++ b/src/client/views/PropertiesView.scss @@ -1,11 +1,16 @@ -@import "./global/globalCssVariables.scss"; +@import './global/globalCssVariables.scss'; .propertiesView { height: 100%; width: 250; - font-family: "Roboto"; + font-family: 'Roboto'; + font-size: 12px; cursor: auto; + .slider-text { + font-size: 8px; + } + overflow-x: hidden; overflow-y: auto; @@ -843,7 +848,7 @@ } .propertiesView-section { - padding: 10px 0; + padding-left: 20px; } .propertiesView-input { @@ -865,7 +870,15 @@ } .propertiesButton { - width: 4rem; + width: 2rem; + height: 2rem; + display: flex; + justify-content: center; + align-items: center; + > svg { + width: 15px; + height: 15px; + } } } @@ -877,4 +890,4 @@ color: white; padding-left: 8px; background-color: rgb(51, 51, 51); -}
\ No newline at end of file +} diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index 842664402..8d495d286 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -7,7 +7,7 @@ import { intersection } from 'lodash'; import { action, autorun, computed, Lambda, observable } from 'mobx'; import { observer } from 'mobx-react'; import { ColorState, SketchPicker } from 'react-color'; -import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, AclSelfEdit, AclSym, AclUnset, DataSym, Doc, DocListCast, Field, HeightSym, NumListCast, Opt, StrListCast, WidthSym } from '../../fields/Doc'; +import { AclAdmin, AclSym, HierarchyMapping, DataSym, Doc, DocListCast, Field, HeightSym, NumListCast, Opt, StrListCast, WidthSym } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; import { InkField } from '../../fields/InkField'; import { List } from '../../fields/List'; @@ -23,17 +23,17 @@ import { SharingManager } from '../util/SharingManager'; import { Transform } from '../util/Transform'; import { undoBatch, UndoManager } from '../util/UndoManager'; import { EditableView } from './EditableView'; +import { Colors } from './global/globalEnums'; import { InkStrokeProperties } from './InkStrokeProperties'; -import { DocumentView, StyleProviderFunc } from './nodes/DocumentView'; +import { DocumentView, OpenWhere, StyleProviderFunc } from './nodes/DocumentView'; import { FilterBox } from './nodes/FilterBox'; import { KeyValueBox } from './nodes/KeyValueBox'; -import { PresBox } from './nodes/trails'; +import { PresBox, PresEffect, PresEffectDirection } from './nodes/trails'; import { PropertiesButtons } from './PropertiesButtons'; import { PropertiesDocBacklinksSelector } from './PropertiesDocBacklinksSelector'; import { PropertiesDocContextSelector } from './PropertiesDocContextSelector'; import './PropertiesView.scss'; import { DefaultStyleProvider } from './StyleProvider'; -import { listSpec } from '../../fields/Schema'; const higflyout = require('@hig/flyout'); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -43,7 +43,7 @@ interface PropertiesViewProps { width: number; height: number; styleProvider?: StyleProviderFunc; - addDocTab: (doc: Doc, where: string) => boolean; + addDocTab: (doc: Doc, where: OpenWhere) => boolean; } @observer @@ -95,7 +95,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { //Pres Trails booleans: @observable openPresTransitions: boolean = false; - @observable openPresProgressivize: boolean = false; @observable openAddSlide: boolean = false; @observable openSlideOptions: boolean = false; @@ -116,12 +115,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { return this.selectedDoc?.type === DocumentType.INK; } - rtfWidth = () => { - return !this.selectedDoc ? 0 : Math.min(this.selectedDoc?.[WidthSym](), this.props.width - 20); - }; - rtfHeight = () => { - return !this.selectedDoc ? 0 : this.rtfWidth() <= this.selectedDoc?.[WidthSym]() ? Math.min(this.selectedDoc?.[HeightSym](), this.MAX_EMBED_HEIGHT) : this.MAX_EMBED_HEIGHT; - }; + rtfWidth = () => (!this.selectedDoc ? 0 : Math.min(this.selectedDoc?.[WidthSym](), this.props.width - 20)); + rtfHeight = () => (!this.selectedDoc ? 0 : this.rtfWidth() <= this.selectedDoc?.[WidthSym]() ? Math.min(this.selectedDoc?.[HeightSym](), this.MAX_EMBED_HEIGHT) : this.MAX_EMBED_HEIGHT); @action docWidth = () => { @@ -311,7 +306,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { } @computed get links() { - return !this.selectedDoc ? null : <PropertiesDocBacklinksSelector Document={this.selectedDoc} hideTitle={true} addDocTab={this.props.addDocTab} />; + const selAnchor = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.currentLinkAnchor; + return !selAnchor ? null : <PropertiesDocBacklinksSelector Document={selAnchor} hideTitle={true} addDocTab={this.props.addDocTab} />; } @computed get layoutPreview() { @@ -368,7 +364,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { */ @undoBatch changePermissions = (e: any, user: string) => { - const docs = SelectionManager.Views().length < 2 ? (this.selectedDoc ? [this.selectedDoc] : []) : SelectionManager.Views().map(docView => docView.props.Document); + const docs = (SelectionManager.Views().length < 2 ? [this.selectedDoc] : SelectionManager.Views().map(dv => dv.props.Document)).filter(doc => doc).map(doc => (this.layoutDocAcls ? doc : DocCast(doc)[DataSym])); SharingManager.Instance.shareFromPropertiesSidebar(user, e.currentTarget.value as SharingPermissions, docs); }; @@ -378,7 +374,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { getPermissionsSelect(user: string, permission: string) { const dropdownValues: string[] = Object.values(SharingPermissions); if (permission === '-multiple-') dropdownValues.unshift(permission); - if (user === 'Override') dropdownValues.unshift('None'); + if (user !== 'Override') dropdownValues.splice(dropdownValues.indexOf(SharingPermissions.Unset), 1); return ( <select className="permissions-select" value={permission} onChange={e => this.changePermissions(e, user)}> {dropdownValues @@ -411,7 +407,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { */ @computed get expansionIcon() { return ( - <Tooltip title={<div className="dash-tooltip">{'Show more permissions'}</div>}> + <Tooltip title={<div className="dash-tooltip">Show more permissions</div>}> <div className="expansion-button" onPointerDown={() => { @@ -453,16 +449,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { * @returns the sharing and permissions panel. */ @computed get sharingTable() { - const AclMap = new Map<symbol, string>([ - [AclUnset, 'None'], - [AclPrivate, SharingPermissions.None], - [AclReadonly, SharingPermissions.View], - [AclAugment, SharingPermissions.Augment], - [AclSelfEdit, SharingPermissions.SelfEdit], - [AclEdit, SharingPermissions.Edit], - [AclAdmin, SharingPermissions.Admin], - ]); - // all selected docs const docs = SelectionManager.Views().length < 2 ? [this.layoutDocAcls ? this.selectedDoc : this.selectedDoc?.[DataSym]] : SelectionManager.Views().map(docView => (this.layoutDocAcls ? docView.props.Document : docView.props.Document[DataSym])); @@ -474,7 +460,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { const showAdmin = effectiveAcls.every(acl => acl === AclAdmin); // users in common between all docs - const commonKeys: string[] = intersection(...docs.map(doc => (this.layoutDocAcls ? doc?.[AclSym] && Object.keys(doc[AclSym]) : doc?.[DataSym][AclSym] && Object.keys(doc[DataSym][AclSym])))); + const commonKeys: string[] = intersection(...docs.map(doc => doc?.[AclSym] && Object.keys(doc[AclSym]).filter(key => key !== 'acl-Me'))); const tableEntries = []; @@ -482,9 +468,9 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { if (commonKeys.length) { for (const key of commonKeys) { const name = denormalizeEmail(key.substring(4)); - const uniform = docs.every(doc => (this.layoutDocAcls ? doc?.[AclSym]?.[key] === docs[0]?.[AclSym]?.[key] : doc?.[DataSym]?.[AclSym]?.[key] === docs[0]?.[DataSym]?.[AclSym]?.[key])); + const uniform = docs.every(doc => doc?.[AclSym]?.[key] === docs[0]?.[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-')); + tableEntries.push(this.sharingItem(name, showAdmin, uniform ? HierarchyMapping.get(target[AclSym][key])!.name : '-multiple-')); } } } @@ -492,11 +478,16 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { 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("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-')); + if (ownerSame) tableEntries.unshift(this.sharingItem(StrCast(target.author), showAdmin, 'Owner')); + tableEntries.unshift(this.sharingItem('Public', showAdmin, docs.filter(doc => doc).every(doc => doc['acl-Public'] === target['acl-Public']) ? target['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) + this.sharingItem( + 'Me', + showAdmin, + docs.filter(doc => doc).every(doc => doc.author === Doc.CurrentUserEmail) ? 'Owner' : effectiveAcls.every(acl => acl === effectiveAcls[0]) ? HierarchyMapping.get(effectiveAcls[0])!.name : '-multiple-', + !ownerSame + ) ); - if (ownerSame) tableEntries.unshift(this.sharingItem(StrCast(target.author), showAdmin, 'Owner')); return <div className="propertiesView-sharingTable">{tableEntries}</div>; } @@ -1336,8 +1327,12 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { ); } - @observable description = Field.toString(LinkManager.currentLink?.description as any as Field); - @observable relationship = StrCast(LinkManager.currentLink?.linkRelationship); + @computed get description() { + return Field.toString(LinkManager.currentLink?.description as any as Field); + } + @computed get relationship() { + return StrCast(LinkManager.currentLink?.linkRelationship); + } @observable private relationshipButtonColor: string = ''; // @action @@ -1347,8 +1342,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @undoBatch handleDescriptionChange = action((value: string) => { if (LinkManager.currentLink && this.selectedDoc) { - this.selectedDoc.description = value; - this.description = value; + this.setDescripValue(value); return true; } }); @@ -1356,8 +1350,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @undoBatch handleLinkRelationshipChange = action((value: string) => { if (LinkManager.currentLink && this.selectedDoc) { - this.selectedDoc.linkRelationship = value; - this.relationship = value; + this.setLinkRelationshipValue(value); return true; } }); @@ -1413,19 +1406,34 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { }); @undoBatch - changeFollowBehavior = action((follow: string) => { - if (LinkManager.currentLink && this.selectedDoc) { - this.selectedDoc.followLinkLocation = follow; - return true; - } - }); + changeFollowBehavior = action((follow: string) => this.sourceAnchor && (this.sourceAnchor.followLinkLocation = follow)); + + @undoBatch + changeAnimationBehavior = action((behavior: string) => this.sourceAnchor && (this.sourceAnchor.followLinkAnimEffect = behavior)); + + @undoBatch + changeEffectDirection = action((effect: PresEffectDirection) => this.sourceAnchor && (this.sourceAnchor.linkAnimDirection = effect)); + + animationDirection = (direction: PresEffectDirection, icon: string, gridColumn: number, gridRow: number, opts: object) => { + const lanch = this.sourceAnchor; + const color = lanch?.linkAnimDirection === direction || (direction === PresEffectDirection.Center && !lanch?.linkAnimDirection) ? Colors.MEDIUM_BLUE : ''; + return ( + <Tooltip title={<div className="dash-tooltip">{direction}</div>}> + <div + style={{ ...opts, border: direction === PresEffectDirection.Center ? `solid 2px ${color}` : undefined, borderRadius: '20%', cursor: 'pointer', gridColumn, gridRow, justifySelf: 'center', background: color, color: 'black' }} + onClick={() => this.changeEffectDirection(direction)}> + {icon ? <FontAwesomeIcon icon={icon as any} /> : null} + </div> + </Tooltip> + ); + }; onSelectOutDesc = () => { this.setDescripValue(this.description); document.getElementById('link_description_input')?.blur(); }; - onDescriptionKey = (e: React.KeyboardEvent<HTMLInputElement>) => { + onDescriptionKey = (e: React.KeyboardEvent<HTMLTextAreaElement>) => { if (e.key === 'Enter') { this.setDescripValue(this.description); document.getElementById('link_description_input')?.blur(); @@ -1444,19 +1452,37 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { } }; - toggleAnchor = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => this.selectedDoc && (this.selectedDoc.linkAutoMove = !this.selectedDoc.linkAutoMove)))); + toggleLinkProp = (e: React.PointerEvent, prop: string) => { + setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => LinkManager.currentLink && (LinkManager.currentLink[prop] = !LinkManager.currentLink[prop])))); }; - toggleArrow = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => this.selectedDoc && (this.selectedDoc.displayArrow = !this.selectedDoc.displayArrow)))); - }; + @computed get destinationAnchor() { + const ldoc = LinkManager.currentLink; + const lanch = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.currentLinkAnchor; + if (ldoc && lanch) return LinkManager.getOppositeAnchor(ldoc, lanch) ?? lanch; + return ldoc ? DocCast(ldoc.anchor2) : ldoc; + } - toggleZoomToTarget1 = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => this.selectedDoc && (DocCast(this.selectedDoc.anchor1).followLinkZoom = !DocCast(this.selectedDoc.anchor1).followLinkZoom)))); - }; - toggleZoomToTarget2 = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => this.selectedDoc && (DocCast(this.selectedDoc.anchor2).followLinkZoom = !DocCast(this.selectedDoc.anchor2).followLinkZoom)))); + @computed get sourceAnchor() { + const selAnchor = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.currentLinkAnchor; + + return selAnchor ?? (LinkManager.currentLink && this.destinationAnchor ? LinkManager.getOppositeAnchor(LinkManager.currentLink, this.destinationAnchor) : LinkManager.currentLink); + } + + toggleAnchorProp = (e: React.PointerEvent, prop: string, anchor?: Doc, value: any = true, ovalue: any = false, cb: (val: any) => any = val => val) => { + anchor && + setupMoveUpEvents( + this, + e, + returnFalse, + emptyFunction, + undoBatch( + action(() => { + anchor[prop] = anchor[prop] === value ? ovalue : value; + this.selectedDoc && cb(anchor[prop]); + }) + ) + ); }; @computed @@ -1478,19 +1504,28 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @computed get editDescription() { return ( - <input - autoComplete={'off'} + <textarea + autoComplete="off" + style={{ textAlign: 'left' }} id="link_description_input" value={StrCast(this.selectedDoc?.description)} onKeyDown={this.onDescriptionKey} onBlur={this.onSelectOutDesc} onChange={e => this.handleDescriptionChange(e.currentTarget.value)} className="text" - type="text" /> ); } + // Converts seconds to ms and updates presTransition + setZoom = (number: String, change?: number) => { + let scale = Number(number) / 100; + if (change) scale += change; + if (scale < 0.01) scale = 0.01; + if (scale > 1) scale = 1; + this.sourceAnchor && (this.sourceAnchor.followLinkZoomScale = scale); + }; + /** * Handles adding and removing members from the sharing panel */ @@ -1504,6 +1539,10 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { render() { const isNovice = Doc.noviceMode; + const zoom = Number((NumCast(this.sourceAnchor?.followLinkZoomScale, 1) * 100).toPrecision(3)); + const targZoom = this.sourceAnchor?.followLinkZoom; + const indent = 30; + const hasSelectedAnchor = SelectionManager.Views().some(dv => DocListCast(this.sourceAnchor?.links).includes(LinkManager.currentLink!)); if (!this.selectedDoc && !this.isPres) { return ( <div className="propertiesView" style={{ width: this.props.width }}> @@ -1513,81 +1552,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { </div> ); } else { - if (this.selectedDoc && this.isLink) { - return ( - <div className="propertiesView"> - <div className="propertiesView-title">Linking</div> - <div className="propertiesView-section"> - <p className="propertiesView-label">Information</p> - <div className="propertiesView-input first"> - <p>Link Relationship</p> - {this.editRelationship} - </div> - <div className="propertiesView-input"> - <p>Description</p> - {this.editDescription} - </div> - </div> - <div className="propertiesView-section"> - <p className="propertiesView-label">Behavior</p> - <div className="propertiesView-input inline first"> - <p>Follow</p> - <select name="selectList" id="selectList" onChange={e => this.changeFollowBehavior(e.currentTarget.value)} value={StrCast(this.selectedDoc.followLinkLocation, 'default')}> - <option value="default">Default</option> - <option value="add:left">Open in new left pane</option> - <option value="add:right">Open in new right pane</option> - <option value="replace:left">Replace left tab</option> - <option value="replace:right">Replace right tab</option> - <option value="fullScreen">Open full screen</option> - <option value="add">Open in new tab</option> - <option value="replace">Replace current tab</option> - {this.selectedDoc.linksToAnnotation ? <option value="openExternal">Open in external page</option> : null} - </select> - </div> - <div className="propertiesView-input inline"> - <p>Auto-move anchor</p> - <button - style={{ background: this.selectedDoc.hidden ? 'gray' : !this.selectedDoc.linkAutoMove ? '' : '#4476f7', borderRadius: 3 }} - onPointerDown={this.toggleAnchor} - onClick={e => e.stopPropagation()} - className="propertiesButton"> - <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" /> - </button> - </div> - <div className="propertiesView-input inline"> - <p>Display arrow</p> - <button - style={{ background: this.selectedDoc.hidden ? 'gray' : !this.selectedDoc.displayArrow ? '' : '#4476f7', borderRadius: 3 }} - onPointerDown={this.toggleArrow} - onClick={e => e.stopPropagation()} - className="propertiesButton"> - <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" /> - </button> - </div> - <div className="propertiesView-input inline"> - <p>Zoom to target</p> - <button - style={{ background: this.selectedDoc.hidden ? 'gray' : !Cast(this.selectedDoc.anchor1, Doc, null).followLinkZoom ? '' : '#4476f7', borderRadius: 3 }} - onPointerDown={this.toggleZoomToTarget1} - onClick={e => e.stopPropagation()} - className="propertiesButton"> - <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" /> - </button> - </div> - <div className="propertiesView-input inline"> - <p>Zoom to source</p> - <button - style={{ background: this.selectedDoc.hidden ? 'gray' : !Cast(this.selectedDoc.anchor2, Doc, null).followLinkZoom ? '' : '#4476f7', borderRadius: 3 }} - onPointerDown={this.toggleZoomToTarget2} - onClick={e => e.stopPropagation()} - className="propertiesButton"> - <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" /> - </button> - </div> - </div> - </div> - ); - } if (this.selectedDoc && !this.isPres) { return ( <div @@ -1605,6 +1569,199 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { {this.contextsSubMenu} {this.linksSubMenu} + {!this.selectedDoc || !LinkManager.currentLink || (!hasSelectedAnchor && this.selectedDoc !== LinkManager.currentLink) ? null : ( + <> + <div className="propertiesView-section" style={{ background: 'darkgray' }}> + <div className="propertiesView-input first" style={{ display: 'grid', gridTemplateColumns: '84px auto' }}> + <p>Relationship</p> + {this.editRelationship} + </div> + <div className="propertiesView-input" style={{ display: 'grid', gridTemplateColumns: '84px auto' }}> + <p>Description</p> + {this.editDescription} + </div> + <div className="propertiesView-input inline"> + <p>Show link</p> + <button + style={{ background: !LinkManager.currentLink?.linkDisplay ? '' : '#4476f7', borderRadius: 3 }} + onPointerDown={e => this.toggleLinkProp(e, 'linkDisplay')} + onClick={e => e.stopPropagation()} + className="propertiesButton"> + <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" /> + </button> + </div> + <div className="propertiesView-input inline" style={{ marginLeft: 10 }}> + <p>Auto-move anchors</p> + <button + style={{ background: !LinkManager.currentLink?.linkAutoMove ? '' : '#4476f7', borderRadius: 3 }} + onPointerDown={e => this.toggleLinkProp(e, 'linkAutoMove')} + onClick={e => e.stopPropagation()} + className="propertiesButton"> + <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" /> + </button> + </div> + <div className="propertiesView-input inline" style={{ marginLeft: 10 }}> + <p>Display arrow</p> + <button + style={{ background: !LinkManager.currentLink?.linkDisplayArrow ? '' : '#4476f7', borderRadius: 3 }} + onPointerDown={e => this.toggleLinkProp(e, 'linkDisplayArrow')} + onClick={e => e.stopPropagation()} + className="propertiesButton"> + <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" /> + </button> + </div> + </div> + {!hasSelectedAnchor ? null : ( + <div className="propertiesView-section"> + <div className="propertiesView-input inline first" style={{ display: 'grid', gridTemplateColumns: '84px calc(100% - 84px)' }}> + <p>Follow by</p> + <select onChange={e => this.changeFollowBehavior(e.currentTarget.value)} value={StrCast(this.sourceAnchor?.followLinkLocation, 'default')}> + <option value="default">Default</option> + <option value={OpenWhere.addLeft}>Opening in new left pane</option> + <option value={OpenWhere.addRight}>Opening in new right pane</option> + <option value={OpenWhere.replaceLeft}>Replacing left tab</option> + <option value={OpenWhere.replaceRight}>Replacing right tab</option> + <option value={OpenWhere.fullScreen}>Overlaying current tab</option> + <option value={OpenWhere.lightbox}>Opening in lightbox</option> + <option value={OpenWhere.add}>Opening in new tab</option> + <option value={OpenWhere.replace}>Replacing current tab</option> + <option value={OpenWhere.inParent}>Opening in same collection</option> + <option value={OpenWhere.inPlace}>Opening in place</option> + {LinkManager.currentLink?.linksToAnnotation ? <option value="openExternal">Open in external page</option> : null} + </select> + </div> + <div className="propertiesView-input inline first" style={{ display: 'grid', gridTemplateColumns: '84px calc(100% - 134px) 50px' }}> + <p>Animation</p> + <select style={{ width: '100%', gridColumn: 2 }} onChange={e => this.changeAnimationBehavior(e.currentTarget.value)} value={StrCast(this.sourceAnchor?.followLinkAnimEffect, 'default')}> + <option value="default">Default</option> + {[PresEffect.None, PresEffect.Zoom, PresEffect.Lightspeed, PresEffect.Fade, PresEffect.Flip, PresEffect.Rotate, PresEffect.Bounce, PresEffect.Roll].map(effect => ( + <option value={effect.toString()}>{effect.toString()}</option> + ))} + </select> + <div className="effectDirection" style={{ marginLeft: '10px', display: 'grid', width: 40, height: 36, gridColumn: 3, gridTemplateRows: '12px 12px 12px' }}> + {this.animationDirection(PresEffectDirection.Left, 'angle-right', 1, 2, {})} + {this.animationDirection(PresEffectDirection.Right, 'angle-left', 3, 2, {})} + {this.animationDirection(PresEffectDirection.Top, 'angle-down', 2, 1, {})} + {this.animationDirection(PresEffectDirection.Bottom, 'angle-up', 2, 3, {})} + {this.animationDirection(PresEffectDirection.Center, '', 2, 2, { width: 10, height: 10, alignSelf: 'center' })} + </div> + </div> + {PresBox.inputter( + '0.1', + '0.1', + '10', + NumCast(this.sourceAnchor?.followLinkTransitionTime) / 1000, + true, + (val: string) => PresBox.SetTransitionTime(val, (timeInMS: number) => this.sourceAnchor && (this.sourceAnchor.followLinkTransitionTime = timeInMS)), + indent + )}{' '} + <div + className={'slider-headers'} + style={{ + display: 'grid', + justifyContent: 'space-between', + width: `calc(100% - ${indent * 2}px)`, + marginLeft: indent, + marginRight: indent, + gridTemplateColumns: 'auto auto', + borderTop: 'solid', + }}> + <div className="slider-text">Fast</div> + <div className="slider-text">Slow</div> + </div>{' '} + <div className="propertiesView-input inline"> + <p>Play Target Audio</p> + <button + style={{ background: !this.sourceAnchor?.followLinkAudio ? '' : '#4476f7', borderRadius: 3 }} + onPointerDown={e => this.toggleAnchorProp(e, 'followLinkAudio', this.sourceAnchor)} + onClick={e => e.stopPropagation()} + className="propertiesButton"> + <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" /> + </button> + </div> + <div className="propertiesView-input inline"> + <p>Toggle Target (Show/Hide)</p> + <button + style={{ background: !this.sourceAnchor?.followLinkToggle ? '' : '#4476f7', borderRadius: 3 }} + onPointerDown={e => this.toggleAnchorProp(e, 'followLinkToggle', this.sourceAnchor)} + onClick={e => e.stopPropagation()} + className="propertiesButton"> + <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" /> + </button> + </div> + <div className="propertiesView-input inline"> + <p>Ease Transitions</p> + <button + style={{ background: this.sourceAnchor?.followLinkEase === 'linear' ? '' : '#4476f7', borderRadius: 3 }} + onPointerDown={e => this.toggleAnchorProp(e, 'followLinkEase', this.sourceAnchor, 'ease', 'linear')} + onClick={e => e.stopPropagation()} + className="propertiesButton"> + <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" /> + </button> + </div> + <div className="propertiesView-input inline"> + <p>Capture Offset to Target</p> + <button + style={{ background: this.sourceAnchor?.followLinkXoffset === undefined ? '' : '#4476f7', borderRadius: 3 }} + onPointerDown={e => { + this.toggleAnchorProp(e, 'followLinkXoffset', this.sourceAnchor, NumCast(this.destinationAnchor?.x) - NumCast(this.sourceAnchor?.x), undefined); + this.toggleAnchorProp(e, 'followLinkYoffset', this.sourceAnchor, NumCast(this.destinationAnchor?.y) - NumCast(this.sourceAnchor?.y), undefined); + }} + onClick={e => e.stopPropagation()} + className="propertiesButton"> + <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" /> + </button> + </div> + <div className="propertiesView-input inline"> + <p>Center Target (no zoom)</p> + <button + style={{ background: this.sourceAnchor?.followLinkZoomScale !== 0 ? '' : '#4476f7', borderRadius: 3 }} + onPointerDown={e => this.toggleAnchorProp(e, 'followLinkZoomScale', this.sourceAnchor, 0, 0.5, val => !val && this.sourceAnchor && (this.sourceAnchor.followLinkZoom = true))} + onClick={e => e.stopPropagation()} + className="propertiesButton"> + <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" /> + </button> + </div> + <div className="propertiesView-input inline" style={{ display: 'grid', gridTemplateColumns: '78px calc(100% - 108px) 50px' }}> + <p>Zoom %</p> + <div className="ribbon-property" style={{ display: !targZoom || this.sourceAnchor?.followLinkZoomScale === 0 ? 'none' : 'inline-flex' }}> + <input className="presBox-input" style={{ width: '100%' }} type="number" value={zoom} /> + <div className="ribbon-propertyUpDown" style={{ display: 'flex', flexDirection: 'column' }}> + <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoom(String(zoom), 0.1))}> + <FontAwesomeIcon icon={'caret-up'} /> + </div> + <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoom(String(zoom), -0.1))}> + <FontAwesomeIcon icon={'caret-down'} /> + </div> + </div> + </div> + <button + style={{ background: !targZoom || this.sourceAnchor?.followLinkZoomScale === 0 ? '' : '#4476f7', borderRadius: 3, gridColumn: 3 }} + onPointerDown={e => this.toggleAnchorProp(e, 'followLinkZoom', this.sourceAnchor)} + onClick={e => e.stopPropagation()} + className="propertiesButton"> + <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" /> + </button> + </div> + {!targZoom || this.sourceAnchor?.followLinkZoomScale === 0 ? null : PresBox.inputter('0', '1', '100', zoom, true, this.setZoom, 30)} + <div + className={'slider-headers'} + style={{ + display: !targZoom || this.sourceAnchor?.followLinkZoomScale === 0 ? 'none' : 'grid', + justifyContent: 'space-between', + width: `calc(100% - ${indent * 2}px)`, + marginLeft: indent, + marginRight: indent, + gridTemplateColumns: 'auto auto', + borderTop: 'solid', + }}> + <div className="slider-text">0%</div> + <div className="slider-text">100%</div> + </div>{' '} + </div> + )} + </> + )} {this.inkSubMenu} @@ -1651,19 +1808,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { {this.openPresTransitions ? <div className="propertiesView-presTrails-content">{PresBox.Instance.transitionDropdown}</div> : null} </div> )} - {/* {!selectedItem || type === DocumentType.VID || type === DocumentType.AUDIO ? (null) : <div className="propertiesView-presTrails"> - <div className="propertiesView-presTrails-title" - onPointerDown={action(() => this.openPresProgressivize = !this.openPresProgressivize)} - style={{ backgroundColor: this.openPresProgressivize ? "black" : "" }}> - <FontAwesomeIcon style={{ alignSelf: "center" }} icon={"tasks"} /> Progressivize - <div className="propertiesView-presTrails-title-icon"> - <FontAwesomeIcon icon={this.openPresProgressivize ? "caret-down" : "caret-right"} size="lg" color="white" /> - </div> - </div> - {this.openPresProgressivize ? <div className="propertiesView-presTrails-content"> - {PresBox.Instance.progressivizeDropdown} - </div> : null} - </div>} */} {!selectedItem || (type !== DocumentType.VID && type !== DocumentType.AUDIO) ? null : ( <div className="propertiesView-presTrails"> <div diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx index 12f41394d..6d06bbbf6 100644 --- a/src/client/views/SidebarAnnos.tsx +++ b/src/client/views/SidebarAnnos.tsx @@ -1,9 +1,9 @@ import { computed } from 'mobx'; import { observer } from 'mobx-react'; -import { Doc, DocListCast, StrListCast } from '../../fields/Doc'; +import { Doc, DocListCast, Field, FieldResult, StrListCast } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; import { List } from '../../fields/List'; -import { NumCast, StrCast } from '../../fields/Types'; +import { DocCast, NumCast, StrCast } from '../../fields/Types'; import { emptyFunction, OmitKeys, returnAll, returnOne, returnTrue, returnZero } from '../../Utils'; import { Docs, DocUtils } from '../documents/Documents'; import { CollectionViewType, DocumentType } from '../documents/DocumentTypes'; @@ -15,6 +15,7 @@ import { SearchBox } from './search/SearchBox'; import './SidebarAnnos.scss'; import { StyleProp } from './StyleProvider'; import React = require('react'); +import { RichTextField } from '../../fields/RichTextField'; interface ExtraProps { fieldKey: string; @@ -38,12 +39,13 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { } _stackRef = React.createRef<CollectionStackingView>(); @computed get allMetadata() { - const keys = new Set<string>(); - DocListCast(this.props.rootDoc[this.sidebarKey]).forEach(doc => SearchBox.documentKeys(doc).forEach(key => keys.add(key))); - return Array.from(keys.keys()) - .filter(key => key[0]) - .filter(key => key[0] !== '_' && key[0] === key[0].toUpperCase()) - .sort(); + const keys = new Map<string, FieldResult<Field>>(); + DocListCast(this.props.rootDoc[this.sidebarKey]).forEach(doc => + SearchBox.documentKeys(doc) + .filter(key => key[0] && key[0] !== '_' && key[0] === key[0].toUpperCase()) + .map(key => keys.set(key, doc[key])) + ); + return keys; } @computed get allUsers() { const keys = new Set<string>(); @@ -70,10 +72,67 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { }); FormattedTextBox.SelectOnLoad = target[Id]; FormattedTextBox.DontSelectInitialText = true; - this.allMetadata.map(tag => (target[tag] = tag)); - DocUtils.MakeLink({ doc: anchor }, { doc: target }, 'inline comment:comment on'); + const link = DocUtils.MakeLink({ doc: anchor }, { doc: target }, 'inline comment:comment on'); + link && (link.linkDisplay = false); + + const taggedContent = this.docFilters() + .filter(data => data.split(':')[0]) + .map(data => { + const key = data.split(':')[0]; + const val = Field.Copy(this.allMetadata.get(key)); + Doc.GetProto(target)[key] = val; + return { + type: 'dashField', + attrs: { fieldKey: key, docid: '', hideKey: false, editable: true }, + marks: [{ type: 'pFontSize', attrs: { fontSize: '12px' } }, { type: 'strong' }, { type: 'user_mark', attrs: { userid: Doc.CurrentUserEmail, modified: 0 } }], + }; + }); + + const textLines: any = [ + { + type: 'paragraph', + attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null }, + content: [ + { + type: 'dashField', + marks: [ + { + type: 'linkAnchor', + attrs: { + allAnchors: [{ href: `/doc/${target[Id]}`, title: 'Anchored Selection', anchorId: `${target[Id]}` }], + location: 'add:right', + title: 'Anchored Selection', + noPreview: true, + docref: false, + }, + }, + { type: 'pFontSize', attrs: { fontSize: '8px' } }, + { type: 'em' }, + ], + attrs: { fieldKey: 'text', docid: anchor[Id], hideKey: true, editable: false }, + }, + ], + }, + { type: 'paragraph', attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null } }, + ]; + const metadatatext = { + type: 'paragraph', + attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null }, + content: taggedContent, + }; + if (taggedContent.length) textLines.push(metadatatext); + Doc.GetProto(target).text = new RichTextField( + JSON.stringify({ + doc: { + type: 'doc', + content: textLines, + }, + selection: { type: 'text', anchor: 4, head: 4 }, // set selection to middle paragraph + }), + '' + ); this.addDocument(target); - this._stackRef.current?.focusDocument(target, {}); + setTimeout(() => this._stackRef.current?.focusDocument(target, {})); return target; }; makeDocUnfiltered = (doc: Doc) => { @@ -111,6 +170,11 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { docFilters = () => [...StrListCast(this.props.layoutDoc._docFilters), ...StrListCast(this.props.layoutDoc[this.filtersKey])]; showTitle = () => 'title'; setHeightCallback = (height: number) => this.props.setHeight?.(height + this.filtersHeight()); + sortByLinkAnchorY = (a: Doc, b: Doc) => { + const ay = DocListCast(a.links).length && DocCast(DocListCast(a.links)[0].anchor1).y; + const by = DocListCast(b.links).length && DocCast(DocListCast(b.links)[0].anchor1).y; + return NumCast(ay) - NumCast(by); + }; render() { const renderTag = (tag: string) => { const active = StrListCast(this.props.rootDoc[this.filtersKey]).includes(`${tag}:${tag}:check`); @@ -120,10 +184,10 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { </div> ); }; - const renderMeta = (tag: string) => { - const active = StrListCast(this.props.rootDoc[this.filtersKey]).includes(`${tag}:${tag}:exists`); + const renderMeta = (tag: string, dflt: FieldResult<Field>) => { + const active = StrListCast(this.props.rootDoc[this.filtersKey]).includes(`${tag}:${dflt}:exists`); return ( - <div key={tag} className={`sidebarAnnos-filterTag${active ? '-active' : ''}`} onClick={e => Doc.setDocFilter(this.props.rootDoc, tag, tag, 'exists', true, this.sidebarKey, e.shiftKey)}> + <div key={tag} className={`sidebarAnnos-filterTag${active ? '-active' : ''}`} onClick={e => Doc.setDocFilter(this.props.rootDoc, tag, dflt, 'exists', true, this.sidebarKey, e.shiftKey)}> {tag} </div> ); @@ -150,7 +214,9 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { }}> <div className="sidebarAnnos-tagList" style={{ height: this.filtersHeight() }} onWheel={e => e.stopPropagation()}> {this.allUsers.map(renderUsers)} - {this.allMetadata.map(renderMeta)} + {Array.from(this.allMetadata.keys()) + .sort() + .map(key => renderMeta(key, this.allMetadata.get(key)))} </div> <div style={{ width: '100%', height: `calc(100% - 38px)`, position: 'relative' }}> <CollectionStackingView @@ -162,6 +228,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { PanelWidth={this.panelWidth} docFilters={this.docFilters} scaleField={this.sidebarKey + '-scale'} + sortFunc={this.sortByLinkAnchorY} setHeight={this.setHeightCallback} isAnnotationOverlay={false} select={emptyFunction} diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index bc8bd7b7f..ece224c68 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -2,7 +2,7 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, runInAction } from 'mobx'; import { extname } from 'path'; -import { Doc, Opt } from '../../fields/Doc'; +import { Doc, DocListCast, Opt } from '../../fields/Doc'; import { BoolCast, Cast, DocCast, ImageCast, NumCast, StrCast } from '../../fields/Types'; import { DashColor, emptyFunction, lightOrDark } from '../../Utils'; import { CollectionViewType, DocumentType } from '../documents/DocumentTypes'; @@ -18,6 +18,7 @@ import { FieldViewProps } from './nodes/FieldView'; import { SliderBox } from './nodes/SliderBox'; import './StyleProvider.scss'; import React = require('react'); +import { Shadows } from 'browndash-components'; export enum StyleProp { TreeViewIcon = 'treeViewIcon', @@ -108,14 +109,14 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps allSorts[TreeSort.None] = { color: 'darkgray', label: '\u00A0\u00A0\u00A0' }; return allSorts; case StyleProp.Highlighting: - if (doc) { + if (doc && !doc.noHighlighting) { const highlightIndex = Doc.isBrushedHighlightedDegree(doc); const highlightColor = ['transparent', 'rgb(68, 118, 247)', 'rgb(68, 118, 247)', 'orange', 'lightBlue'][highlightIndex]; const highlightStyle = ['solid', 'dashed', 'solid', 'solid', 'solid'][highlightIndex]; - const excludeTypes = !props?.treeViewDoc ? [DocumentType.FONTICON, DocumentType.INK] : [DocumentType.FONTICON]; + const excludeTypes = [DocumentType.FONTICON]; let highlighting = !props?.disableDocBrushing && highlightIndex && !excludeTypes.includes(doc.type as any) && doc._viewType !== CollectionViewType.Linear; // bcz: hack to turn off highlighting onsidebar panel documents. need to flag a document as not highlightable in a more direct way if (highlighting && props?.focus !== emptyFunction && StrCast(doc.title) !== '[pres element template]') { - return { highlightStyle, highlightColor, highlightIndex }; + return { highlightStyle, highlightColor, highlightIndex, highlightStroke: doc.type === DocumentType.INK }; } } return undefined; @@ -128,7 +129,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps case StyleProp.Opacity: return Cast(doc?._opacity, 'number', Cast(doc?.opacity, 'number', null)); case StyleProp.HideLinkButton: - return props?.hideLinkButton || (!selected && (doc?.isLinkButton || doc?.hideLinkButton)); + return props?.hideLinkButton || (!selected && doc?.hideLinkButton); case StyleProp.FontSize: return StrCast(doc?.[fieldKey + 'fontSize'], StrCast(doc?._fontSize, StrCast(Doc.UserDoc().fontSize))); case StyleProp.FontFamily: @@ -235,7 +236,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps ? Colors.DARK_GRAY : Colors.LIGHT_GRAY // system docs (seen in treeView) get a grayish background : doc.annotationOn - ? '#00000015' // faint interior for collections on PDFs, images, etc + ? '#00000010' // faint interior for collections on PDFs, images, etc : doc?._isGroup ? undefined : Cast((props?.renderDepth || 0) > 0 ? Doc.UserDoc().activeCollectionNestedBackground : Doc.UserDoc().activeCollectionBackground, 'string') ?? (darkScheme() ? Colors.BLACK : 'linear-gradient(#065fff, #85c1f9)')); @@ -249,10 +250,9 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps return docColor; } case StyleProp.BoxShadow: { - if (!doc || opacity() === 0) return undefined; // if it's not visible, then no shadow) - - if (doc?.isLinkButton && ![DocumentType.LINK, DocumentType.INK].includes(doc.type as any)) return StrCast(doc?._linkButtonShadow, 'lightblue 0em 0em 1em'); - + if (!doc || opacity() === 0 || doc.noShadow) return undefined; // if it's not visible, then no shadow) + if (doc.boxShadow === 'standard') return Shadows.STANDARD_SHADOW; + if (doc?.isLinkButton && DocListCast(doc?.links).length && ![DocumentType.LINK, DocumentType.INK].includes(doc.type as any)) return StrCast(doc?._linkButtonShadow, 'lightblue 0em 0em 1em'); switch (doc?.type) { case DocumentType.COL: return StrCast( @@ -279,6 +279,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps } } case StyleProp.PointerEvents: + if (MainView.Instance._exploreMode) return 'all'; if (doc?.pointerEvents) return StrCast(doc.pointerEvents); if (props?.pointerEvents?.() === 'none') return 'none'; const isInk = doc && StrCast(Doc.Layout(doc).layout).includes(InkingStroke.name); @@ -287,11 +288,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps return undefined; case StyleProp.Decorations: if (props?.ContainingCollectionDoc?._viewType === CollectionViewType.Freeform || doc?.x !== undefined || doc?.y !== undefined) { - return doc && - isBackground() && - !Doc.IsSystem(doc) && - (props?.renderDepth || 0) > 0 && - ((doc.type === DocumentType.COL && doc._viewType !== CollectionViewType.Pile) || [DocumentType.RTF, DocumentType.IMG, DocumentType.INK].includes(doc.type as DocumentType)) ? ( + return doc && doc.pointerEvents === 'none' && isBackground() && !Doc.IsSystem(doc) && (props?.renderDepth || 0) > 0 ? ( <div className="styleProvider-lock" onClick={() => toggleLockedPosition(doc)}> <FontAwesomeIcon icon={isBackground() ? 'lock' : 'unlock'} style={{ color: isBackground() ? 'red' : undefined }} size="lg" /> </div> diff --git a/src/client/views/animationtimeline/Keyframe.tsx b/src/client/views/animationtimeline/Keyframe.tsx index 92d3e2bed..21a5af83f 100644 --- a/src/client/views/animationtimeline/Keyframe.tsx +++ b/src/client/views/animationtimeline/Keyframe.tsx @@ -1,38 +1,37 @@ -import { action, computed, observable, runInAction } from "mobx"; -import { observer } from "mobx-react"; -import * as React from "react"; -import { Doc, DocListCast, Opt } from "../../../fields/Doc"; -import { List } from "../../../fields/List"; -import { createSchema, defaultSpec, listSpec, makeInterface } from "../../../fields/Schema"; -import { Cast, NumCast } from "../../../fields/Types"; -import { Docs } from "../../documents/Documents"; -import { Transform } from "../../util/Transform"; -import { CollectionDockingView } from "../collections/CollectionDockingView"; -import "../global/globalCssVariables.scss"; -import "./Keyframe.scss"; -import "./Timeline.scss"; -import { TimelineMenu } from "./TimelineMenu"; - +import { action, computed, observable, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { Doc, DocListCast, Opt } from '../../../fields/Doc'; +import { List } from '../../../fields/List'; +import { createSchema, defaultSpec, listSpec, makeInterface } from '../../../fields/Schema'; +import { Cast, NumCast } from '../../../fields/Types'; +import { Docs } from '../../documents/Documents'; +import { Transform } from '../../util/Transform'; +import { CollectionDockingView } from '../collections/CollectionDockingView'; +import '../global/globalCssVariables.scss'; +import { OpenWhereMod } from '../nodes/DocumentView'; +import './Keyframe.scss'; +import './Timeline.scss'; +import { TimelineMenu } from './TimelineMenu'; /** - * Useful static functions that you can use. Mostly for logic, but you can also add UI logic here also + * Useful static functions that you can use. Mostly for logic, but you can also add UI logic here also */ export namespace KeyframeFunc { - export enum KeyframeType { - end = "end", - fade = "fade", - default = "default", + end = 'end', + fade = 'fade', + default = 'default', } export enum Direction { - left = "left", - right = "right" + left = 'left', + right = 'right', } - export const findAdjacentRegion = (dir: KeyframeFunc.Direction, currentRegion: Doc, regions: Doc[]): (RegionData | undefined) => { - let leftMost: (RegionData | undefined) = undefined; - let rightMost: (RegionData | undefined) = undefined; + export const findAdjacentRegion = (dir: KeyframeFunc.Direction, currentRegion: Doc, regions: Doc[]): RegionData | undefined => { + let leftMost: RegionData | undefined = undefined; + let rightMost: RegionData | undefined = undefined; regions.forEach(region => { const neighbor = RegionData(region); if (currentRegion.position! > neighbor.position) { @@ -52,11 +51,12 @@ export namespace KeyframeFunc { } }; - export const calcMinLeft = (region: Doc, currentBarX: number, ref?: Doc) => { //returns the time of the closet keyframe to the left + export const calcMinLeft = (region: Doc, currentBarX: number, ref?: Doc) => { + //returns the time of the closet keyframe to the left let leftKf: Opt<Doc>; let time: number = 0; const keyframes = DocListCast(region.keyframes!); - keyframes.map((kf) => { + keyframes.map(kf => { let compTime = currentBarX; if (ref) compTime = NumCast(ref.time); if (NumCast(kf.time) < compTime && NumCast(kf.time) >= time) { @@ -67,11 +67,11 @@ export namespace KeyframeFunc { return leftKf; }; - - export const calcMinRight = (region: Doc, currentBarX: number, ref?: Doc) => { //returns the time of the closest keyframe to the right + export const calcMinRight = (region: Doc, currentBarX: number, ref?: Doc) => { + //returns the time of the closest keyframe to the right let rightKf: Opt<Doc>; let time: number = Infinity; - DocListCast(region.keyframes!).forEach((kf) => { + DocListCast(region.keyframes!).forEach(kf => { let compTime = currentBarX; if (ref) compTime = NumCast(ref.time); if (NumCast(kf.time) > compTime && NumCast(kf.time) <= NumCast(time)) { @@ -93,27 +93,31 @@ export namespace KeyframeFunc { return regiondata; }; - - export const convertPixelTime = (pos: number, unit: "mili" | "sec" | "min" | "hr", dir: "pixel" | "time", tickSpacing: number, tickIncrement: number) => { - const time = dir === "pixel" ? (pos * tickSpacing) / tickIncrement : (pos / tickSpacing) * tickIncrement; + export const convertPixelTime = (pos: number, unit: 'mili' | 'sec' | 'min' | 'hr', dir: 'pixel' | 'time', tickSpacing: number, tickIncrement: number) => { + const time = dir === 'pixel' ? (pos * tickSpacing) / tickIncrement : (pos / tickSpacing) * tickIncrement; switch (unit) { - case "mili": return time; - case "sec": return dir === "pixel" ? time / 1000 : time * 1000; - case "min": return dir === "pixel" ? time / 60000 : time * 60000; - case "hr": return dir === "pixel" ? time / 3600000 : time * 3600000; - default: return time; + case 'mili': + return time; + case 'sec': + return dir === 'pixel' ? time / 1000 : time * 1000; + case 'min': + return dir === 'pixel' ? time / 60000 : time * 60000; + case 'hr': + return dir === 'pixel' ? time / 3600000 : time * 3600000; + default: + return time; } }; } export const RegionDataSchema = createSchema({ - position: defaultSpec("number", 0), - duration: defaultSpec("number", 0), + position: defaultSpec('number', 0), + duration: defaultSpec('number', 0), keyframes: listSpec(Doc), - fadeIn: defaultSpec("number", 0), - fadeOut: defaultSpec("number", 0), + fadeIn: defaultSpec('number', 0), + fadeOut: defaultSpec('number', 0), functions: listSpec(Doc), - hasData: defaultSpec("boolean", false) + hasData: defaultSpec('boolean', false), }); export type RegionData = makeInterface<[typeof RegionDataSchema]>; export const RegionData = makeInterface(RegionDataSchema); @@ -130,50 +134,63 @@ interface IProps { makeKeyData: (region: RegionData, pos: number, kftype: KeyframeFunc.KeyframeType) => Doc; } - /** - * + * * This class handles the green region stuff * Key facts: - * + * * Structure looks like this - * + * * region as a whole * <------------------------------REGION-------------------------------> - * - * region broken down - * + * + * region broken down + * * <|---------|############ MAIN CONTENT #################|-----------|> .....followed by void......... * (start) (Fade 2) * (fade 1) (finish) - * - * - * As you can see, this is different from After Effect and Premiere Pro, but this is how TAG worked. - * If you want to checkout TAG, it's in the lockers, and the password is the usual lab door password. It's the blue laptop. - * If you want to know the exact location of the computer, message me. - * - * @author Andrew Kim + * + * + * As you can see, this is different from After Effect and Premiere Pro, but this is how TAG worked. + * If you want to checkout TAG, it's in the lockers, and the password is the usual lab door password. It's the blue laptop. + * If you want to know the exact location of the computer, message me. + * + * @author Andrew Kim */ @observer export class Keyframe extends React.Component<IProps> { - @observable private _bar = React.createRef<HTMLDivElement>(); @observable private _mouseToggled = false; @observable private _doubleClickEnabled = false; - @computed private get regiondata() { return RegionData(this.props.RegionData); } - @computed private get regions() { return DocListCast(this.props.node.regions); } - @computed private get keyframes() { return DocListCast(this.regiondata.keyframes); } - @computed private get pixelPosition() { return KeyframeFunc.convertPixelTime(this.regiondata.position, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement); } - @computed private get pixelDuration() { return KeyframeFunc.convertPixelTime(this.regiondata.duration, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement); } - @computed private get pixelFadeIn() { return KeyframeFunc.convertPixelTime(this.regiondata.fadeIn, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement); } - @computed private get pixelFadeOut() { return KeyframeFunc.convertPixelTime(this.regiondata.fadeOut, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement); } + @computed private get regiondata() { + return RegionData(this.props.RegionData); + } + @computed private get regions() { + return DocListCast(this.props.node.regions); + } + @computed private get keyframes() { + return DocListCast(this.regiondata.keyframes); + } + @computed private get pixelPosition() { + return KeyframeFunc.convertPixelTime(this.regiondata.position, 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement); + } + @computed private get pixelDuration() { + return KeyframeFunc.convertPixelTime(this.regiondata.duration, 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement); + } + @computed private get pixelFadeIn() { + return KeyframeFunc.convertPixelTime(this.regiondata.fadeIn, 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement); + } + @computed private get pixelFadeOut() { + return KeyframeFunc.convertPixelTime(this.regiondata.fadeOut, 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement); + } constructor(props: any) { super(props); } componentDidMount() { - setTimeout(() => { //giving it a temporary 1sec delay... + setTimeout(() => { + //giving it a temporary 1sec delay... if (!this.regiondata.keyframes) this.regiondata.keyframes = new List<Doc>(); const start = this.props.makeKeyData(this.regiondata, this.regiondata.position, KeyframeFunc.KeyframeType.end); const fadeIn = this.props.makeKeyData(this.regiondata, this.regiondata.position + this.regiondata.fadeIn, KeyframeFunc.KeyframeType.fade); @@ -202,12 +219,12 @@ export class Keyframe extends React.Component<IProps> { this._doubleClickEnabled = false; }, 200); this._doubleClickEnabled = true; - document.addEventListener("pointermove", this.onBarPointerMove); - document.addEventListener("pointerup", (e: PointerEvent) => { - document.removeEventListener("pointermove", this.onBarPointerMove); + document.addEventListener('pointermove', this.onBarPointerMove); + document.addEventListener('pointerup', (e: PointerEvent) => { + document.removeEventListener('pointermove', this.onBarPointerMove); }); } - } + }; @action onBarPointerMove = (e: PointerEvent) => { @@ -219,46 +236,46 @@ export class Keyframe extends React.Component<IProps> { const left = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.left, this.regiondata, this.regions)!; const right = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.right, this.regiondata, this.regions)!; const prevX = this.regiondata.position; - const futureX = this.regiondata.position + KeyframeFunc.convertPixelTime(e.movementX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement); + const futureX = this.regiondata.position + KeyframeFunc.convertPixelTime(e.movementX, 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement); if (futureX <= 0) { this.regiondata.position = 0; - } else if ((left && left.position + left.duration >= futureX)) { + } else if (left && left.position + left.duration >= futureX) { this.regiondata.position = left.position + left.duration; - } else if ((right && right.position <= futureX + this.regiondata.duration)) { + } else if (right && right.position <= futureX + this.regiondata.duration) { this.regiondata.position = right.position - this.regiondata.duration; } else { this.regiondata.position = futureX; } const movement = this.regiondata.position - prevX; - this.keyframes.forEach(kf => kf.time = NumCast(kf.time) + movement); - } + this.keyframes.forEach(kf => (kf.time = NumCast(kf.time) + movement)); + }; @action onResizeLeft = (e: React.PointerEvent) => { e.preventDefault(); e.stopPropagation(); - document.addEventListener("pointermove", this.onDragResizeLeft); - document.addEventListener("pointerup", () => { - document.removeEventListener("pointermove", this.onDragResizeLeft); + document.addEventListener('pointermove', this.onDragResizeLeft); + document.addEventListener('pointerup', () => { + document.removeEventListener('pointermove', this.onDragResizeLeft); }); - } + }; @action onResizeRight = (e: React.PointerEvent) => { e.preventDefault(); e.stopPropagation(); - document.addEventListener("pointermove", this.onDragResizeRight); - document.addEventListener("pointerup", () => { - document.removeEventListener("pointermove", this.onDragResizeRight); + document.addEventListener('pointermove', this.onDragResizeRight); + document.addEventListener('pointerup', () => { + document.removeEventListener('pointermove', this.onDragResizeRight); }); - } + }; @action onDragResizeLeft = (e: PointerEvent) => { e.preventDefault(); e.stopPropagation(); const bar = this._bar.current!; - const offset = KeyframeFunc.convertPixelTime(Math.round((e.clientX - bar.getBoundingClientRect().left) * this.props.transform.Scale), "mili", "time", this.props.tickSpacing, this.props.tickIncrement); + const offset = KeyframeFunc.convertPixelTime(Math.round((e.clientX - bar.getBoundingClientRect().left) * this.props.transform.Scale), 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement); const leftRegion = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.left, this.regiondata, this.regions); if (leftRegion && this.regiondata.position + offset <= leftRegion.position + leftRegion.duration) { this.regiondata.position = leftRegion.position + leftRegion.duration; @@ -275,90 +292,99 @@ export class Keyframe extends React.Component<IProps> { } this.keyframes[0].time = this.regiondata.position; this.keyframes[1].time = this.regiondata.position + this.regiondata.fadeIn; - } - + }; @action onDragResizeRight = (e: PointerEvent) => { e.preventDefault(); e.stopPropagation(); const bar = this._bar.current!; - const offset = KeyframeFunc.convertPixelTime(Math.round((e.clientX - bar.getBoundingClientRect().right) * this.props.transform.Scale), "mili", "time", this.props.tickSpacing, this.props.tickIncrement); + const offset = KeyframeFunc.convertPixelTime(Math.round((e.clientX - bar.getBoundingClientRect().right) * this.props.transform.Scale), 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement); const rightRegion = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.right, this.regiondata, this.regions); const fadeOutKeyframeTime = NumCast(this.keyframes[this.keyframes.length - 3].time); - if (this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut + offset <= fadeOutKeyframeTime) { //case 1: when third to last keyframe is in the way + if (this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut + offset <= fadeOutKeyframeTime) { + //case 1: when third to last keyframe is in the way this.regiondata.duration = fadeOutKeyframeTime - this.regiondata.position + this.regiondata.fadeOut; - } else if (rightRegion && (this.regiondata.position + this.regiondata.duration + offset >= rightRegion.position)) { + } else if (rightRegion && this.regiondata.position + this.regiondata.duration + offset >= rightRegion.position) { this.regiondata.duration = rightRegion.position - this.regiondata.position; } else { this.regiondata.duration += offset; } this.keyframes[this.keyframes.length - 2].time = this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut; this.keyframes[this.keyframes.length - 1].time = this.regiondata.position + this.regiondata.duration; - } - + }; @action createKeyframe = async (clientX: number) => { this._mouseToggled = true; const bar = this._bar.current!; - const offset = KeyframeFunc.convertPixelTime(Math.round((clientX - bar.getBoundingClientRect().left) * this.props.transform.Scale), "mili", "time", this.props.tickSpacing, this.props.tickIncrement); - if (offset > this.regiondata.fadeIn && offset < this.regiondata.duration - this.regiondata.fadeOut) { //make sure keyframe is not created inbetween fades and ends + const offset = KeyframeFunc.convertPixelTime(Math.round((clientX - bar.getBoundingClientRect().left) * this.props.transform.Scale), 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement); + if (offset > this.regiondata.fadeIn && offset < this.regiondata.duration - this.regiondata.fadeOut) { + //make sure keyframe is not created inbetween fades and ends const position = this.regiondata.position; this.props.makeKeyData(this.regiondata, Math.round(position + offset), KeyframeFunc.KeyframeType.default); this.regiondata.hasData = true; - this.props.changeCurrentBarX(KeyframeFunc.convertPixelTime(Math.round(position + offset), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement)); //first move the keyframe to the correct location and make a copy so the correct file gets coppied - + this.props.changeCurrentBarX(KeyframeFunc.convertPixelTime(Math.round(position + offset), 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement)); //first move the keyframe to the correct location and make a copy so the correct file gets coppied } - } - + }; @action moveKeyframe = async (e: React.MouseEvent, kf: Doc) => { e.preventDefault(); e.stopPropagation(); - this.props.changeCurrentBarX(KeyframeFunc.convertPixelTime(NumCast(kf.time!), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement)); - } + this.props.changeCurrentBarX(KeyframeFunc.convertPixelTime(NumCast(kf.time!), 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement)); + }; /** * custom keyframe context menu items (when clicking on the keyframe circle) */ @action makeKeyframeMenu = (kf: Doc, e: MouseEvent) => { - TimelineMenu.Instance.addItem("button", "Toggle Fade Only", () => { + TimelineMenu.Instance.addItem('button', 'Toggle Fade Only', () => { kf.type = kf.type === KeyframeFunc.KeyframeType.fade ? KeyframeFunc.KeyframeType.default : KeyframeFunc.KeyframeType.fade; }), - TimelineMenu.Instance.addItem("button", "Show Data", action(() => { - const kvp = Docs.Create.KVPDocument(kf, { _width: 300, _height: 300 }); - CollectionDockingView.AddSplit(kvp, "right"); - })), - TimelineMenu.Instance.addItem("button", "Delete", action(() => { - (this.regiondata.keyframes as List<Doc>).splice(this.keyframes.indexOf(kf), 1); - this.forceUpdate(); - })), - TimelineMenu.Instance.addItem("input", "Move", action((val) => { - let cannotMove: boolean = false; - const kfIndex: number = this.keyframes.indexOf(kf); - if (val < 0 || (val < NumCast(this.keyframes[kfIndex - 1].time) || val > NumCast(this.keyframes[kfIndex + 1].time))) { - cannotMove = true; - } - if (!cannotMove) { - this.keyframes[kfIndex].time = parseInt(val, 10); - this.keyframes[1].time = this.regiondata.position + this.regiondata.fadeIn; - } - })); - TimelineMenu.Instance.addMenu("Keyframe"); + TimelineMenu.Instance.addItem( + 'button', + 'Show Data', + action(() => { + const kvp = Docs.Create.KVPDocument(kf, { _width: 300, _height: 300 }); + CollectionDockingView.AddSplit(kvp, OpenWhereMod.right); + }) + ), + TimelineMenu.Instance.addItem( + 'button', + 'Delete', + action(() => { + (this.regiondata.keyframes as List<Doc>).splice(this.keyframes.indexOf(kf), 1); + this.forceUpdate(); + }) + ), + TimelineMenu.Instance.addItem( + 'input', + 'Move', + action(val => { + let cannotMove: boolean = false; + const kfIndex: number = this.keyframes.indexOf(kf); + if (val < 0 || val < NumCast(this.keyframes[kfIndex - 1].time) || val > NumCast(this.keyframes[kfIndex + 1].time)) { + cannotMove = true; + } + if (!cannotMove) { + this.keyframes[kfIndex].time = parseInt(val, 10); + this.keyframes[1].time = this.regiondata.position + this.regiondata.fadeIn; + } + }) + ); + TimelineMenu.Instance.addMenu('Keyframe'); TimelineMenu.Instance.openMenu(e.clientX, e.clientY); - } + }; /** - * context menu for region (anywhere on the green region). + * context menu for region (anywhere on the green region). */ @action makeRegionMenu = (kf: Doc, e: MouseEvent) => { - TimelineMenu.Instance.addItem("button", "Remove Region", () => - Cast(this.props.node.regions, listSpec(Doc))?.splice(this.regions.indexOf(this.props.RegionData), 1)), - TimelineMenu.Instance.addItem("input", `fadeIn: ${this.regiondata.fadeIn}ms`, (val) => { + TimelineMenu.Instance.addItem('button', 'Remove Region', () => Cast(this.props.node.regions, listSpec(Doc))?.splice(this.regions.indexOf(this.props.RegionData), 1)), + TimelineMenu.Instance.addItem('input', `fadeIn: ${this.regiondata.fadeIn}ms`, val => { runInAction(() => { let cannotMove: boolean = false; if (val < 0 || val > NumCast(this.keyframes[2].time) - this.regiondata.position) { @@ -370,7 +396,7 @@ export class Keyframe extends React.Component<IProps> { } }); }), - TimelineMenu.Instance.addItem("input", `fadeOut: ${this.regiondata.fadeOut}ms`, (val) => { + TimelineMenu.Instance.addItem('input', `fadeOut: ${this.regiondata.fadeOut}ms`, val => { runInAction(() => { let cannotMove: boolean = false; if (val < 0 || val > this.regiondata.position + this.regiondata.duration - NumCast(this.keyframes[this.keyframes.length - 3].time)) { @@ -382,34 +408,38 @@ export class Keyframe extends React.Component<IProps> { } }); }), - TimelineMenu.Instance.addItem("input", `position: ${this.regiondata.position}ms`, (val) => { + TimelineMenu.Instance.addItem('input', `position: ${this.regiondata.position}ms`, val => { runInAction(() => { const prevPosition = this.regiondata.position; let cannotMove: boolean = false; - this.regions.map(region => ({ pos: NumCast(region.position), dur: NumCast(region.duration) })).forEach(({ pos, dur }) => { - if (pos !== this.regiondata.position) { - if ((val < 0) || (val > pos && val < pos + dur || (this.regiondata.duration + val > pos && this.regiondata.duration + val < pos + dur))) { - cannotMove = true; + this.regions + .map(region => ({ pos: NumCast(region.position), dur: NumCast(region.duration) })) + .forEach(({ pos, dur }) => { + if (pos !== this.regiondata.position) { + if (val < 0 || (val > pos && val < pos + dur) || (this.regiondata.duration + val > pos && this.regiondata.duration + val < pos + dur)) { + cannotMove = true; + } } - } - }); + }); if (!cannotMove) { this.regiondata.position = parseInt(val, 10); this.updateKeyframes(this.regiondata.position - prevPosition); } }); }), - TimelineMenu.Instance.addItem("input", `duration: ${this.regiondata.duration}ms`, (val) => { + TimelineMenu.Instance.addItem('input', `duration: ${this.regiondata.duration}ms`, val => { runInAction(() => { let cannotMove: boolean = false; - this.regions.map(region => ({ pos: NumCast(region.position), dur: NumCast(region.duration) })).forEach(({ pos, dur }) => { - if (pos !== this.regiondata.position) { - val += this.regiondata.position; - if ((val < 0) || (val > pos && val < pos + dur)) { - cannotMove = true; + this.regions + .map(region => ({ pos: NumCast(region.position), dur: NumCast(region.duration) })) + .forEach(({ pos, dur }) => { + if (pos !== this.regiondata.position) { + val += this.regiondata.position; + if (val < 0 || (val > pos && val < pos + dur)) { + cannotMove = true; + } } - } - }); + }); if (!cannotMove) { this.regiondata.duration = parseInt(val, 10); this.keyframes[this.keyframes.length - 1].time = this.regiondata.position + this.regiondata.duration; @@ -417,9 +447,9 @@ export class Keyframe extends React.Component<IProps> { } }); }), - TimelineMenu.Instance.addMenu("Region"); + TimelineMenu.Instance.addMenu('Region'); TimelineMenu.Instance.openMenu(e.clientX, e.clientY); - } + }; @action updateKeyframes = (incr: number, filter: number[] = []) => { @@ -428,7 +458,7 @@ export class Keyframe extends React.Component<IProps> { kf.time = NumCast(kf.time) + incr; } }); - } + }; /** * hovering effect when hovered (hidden div darkens) @@ -438,9 +468,9 @@ export class Keyframe extends React.Component<IProps> { e.preventDefault(); e.stopPropagation(); const div = ref.current!; - div.style.opacity = "1"; + div.style.opacity = '1'; Doc.BrushDoc(this.props.node); - } + }; /** * hovering effect when hovered out (hidden div becomes invisible) @@ -450,14 +480,12 @@ export class Keyframe extends React.Component<IProps> { e.preventDefault(); e.stopPropagation(); const div = ref.current!; - div.style.opacity = "0"; + div.style.opacity = '0'; Doc.UnBrushDoc(this.props.node); - } - + }; ///////////////////////UI STUFF ///////////////////////// - /** * drawing keyframe. Handles both keyframe with a circle (one that you create by double clicking) and one without circle (fades) * this probably needs biggest change, since everyone expected all keyframes to have a circle (and draggable) @@ -465,32 +493,43 @@ export class Keyframe extends React.Component<IProps> { drawKeyframes = () => { const keyframeDivs: JSX.Element[] = []; return DocListCast(this.regiondata.keyframes).map(kf => { - if (kf.type as KeyframeFunc.KeyframeType !== KeyframeFunc.KeyframeType.end) { - return <> - <div className="keyframe" style={{ left: `${KeyframeFunc.convertPixelTime(NumCast(kf.time), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement) - this.pixelPosition}px` }}> - <div className="divider"></div> - <div className="keyframeCircle keyframe-indicator" - onPointerDown={(e) => { e.preventDefault(); e.stopPropagation(); this.moveKeyframe(e, kf); }} - onContextMenu={(e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - this.makeKeyframeMenu(kf, e.nativeEvent); - }} - onDoubleClick={(e) => { e.preventDefault(); e.stopPropagation(); }}> + if ((kf.type as KeyframeFunc.KeyframeType) !== KeyframeFunc.KeyframeType.end) { + return ( + <> + <div className="keyframe" style={{ left: `${KeyframeFunc.convertPixelTime(NumCast(kf.time), 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement) - this.pixelPosition}px` }}> + <div className="divider"></div> + <div + className="keyframeCircle keyframe-indicator" + onPointerDown={e => { + e.preventDefault(); + e.stopPropagation(); + this.moveKeyframe(e, kf); + }} + onContextMenu={(e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + this.makeKeyframeMenu(kf, e.nativeEvent); + }} + onDoubleClick={e => { + e.preventDefault(); + e.stopPropagation(); + }}></div> </div> - </div> - <div className="keyframe-information" /> - </>; + <div className="keyframe-information" /> + </> + ); } else { - return <div className="keyframe" style={{ left: `${KeyframeFunc.convertPixelTime(NumCast(kf.time), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement) - this.pixelPosition}px` }}> - <div className="divider" /> - </div>; + return ( + <div className="keyframe" style={{ left: `${KeyframeFunc.convertPixelTime(NumCast(kf.time), 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement) - this.pixelPosition}px` }}> + <div className="divider" /> + </div> + ); } }); - } + }; /** - * drawing the hidden divs that partition different intervals within a region. + * drawing the hidden divs that partition different intervals within a region. */ @action drawKeyframeDividers = () => { @@ -500,26 +539,36 @@ export class Keyframe extends React.Component<IProps> { if (index !== this.keyframes.length - 1) { const right = this.keyframes[index + 1]; const bodyRef = React.createRef<HTMLDivElement>(); - const kfPos = KeyframeFunc.convertPixelTime(NumCast(kf.time), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement); - const rightPos = KeyframeFunc.convertPixelTime(NumCast(right.time), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement); + const kfPos = KeyframeFunc.convertPixelTime(NumCast(kf.time), 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement); + const rightPos = KeyframeFunc.convertPixelTime(NumCast(right.time), 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement); keyframeDividers.push( - <div ref={bodyRef} className="body-container" style={{ left: `${kfPos - this.pixelPosition}px`, width: `${rightPos - kfPos}px` }} - onPointerOver={(e) => { e.preventDefault(); e.stopPropagation(); this.onContainerOver(e, bodyRef); }} - onPointerOut={(e) => { e.preventDefault(); e.stopPropagation(); this.onContainerOut(e, bodyRef); }} - onContextMenu={(e) => { + <div + ref={bodyRef} + className="body-container" + style={{ left: `${kfPos - this.pixelPosition}px`, width: `${rightPos - kfPos}px` }} + onPointerOver={e => { + e.preventDefault(); + e.stopPropagation(); + this.onContainerOver(e, bodyRef); + }} + onPointerOut={e => { + e.preventDefault(); + e.stopPropagation(); + this.onContainerOut(e, bodyRef); + }} + onContextMenu={e => { e.preventDefault(); e.stopPropagation(); if (index !== 0 || index !== this.keyframes.length - 2) { this._mouseToggled = true; } this.makeRegionMenu(kf, e.nativeEvent); - }}> - </div> + }}></div> ); } }); return keyframeDividers; - } + }; /** * rendering that green region @@ -527,13 +576,18 @@ export class Keyframe extends React.Component<IProps> { //154, 206, 223 render() { return ( - <div className="bar" ref={this._bar} style={{ - transform: `translate(${this.pixelPosition}px)`, - width: `${this.pixelDuration}px`, - background: `linear-gradient(90deg, rgba(154, 206, 223, 0) 0%, rgba(154, 206, 223, 1) ${this.pixelFadeIn / this.pixelDuration * 100}%, rgba(154, 206, 223, 1) ${(this.pixelDuration - this.pixelFadeOut) / this.pixelDuration * 100}%, rgba(154, 206, 223, 0) 100% )` - }} + <div + className="bar" + ref={this._bar} + style={{ + transform: `translate(${this.pixelPosition}px)`, + width: `${this.pixelDuration}px`, + background: `linear-gradient(90deg, rgba(154, 206, 223, 0) 0%, rgba(154, 206, 223, 1) ${(this.pixelFadeIn / this.pixelDuration) * 100}%, rgba(154, 206, 223, 1) ${ + ((this.pixelDuration - this.pixelFadeOut) / this.pixelDuration) * 100 + }%, rgba(154, 206, 223, 0) 100% )`, + }} onPointerDown={this.onBarPointerDown}> - <div className="leftResize keyframe-indicator" onPointerDown={this.onResizeLeft} ></div> + <div className="leftResize keyframe-indicator" onPointerDown={this.onResizeLeft}></div> {/* <div className="keyframe-information"></div> */} <div className="rightResize keyframe-indicator" onPointerDown={this.onResizeRight}></div> {/* <div className="keyframe-information"></div> */} @@ -542,4 +596,4 @@ export class Keyframe extends React.Component<IProps> { </div> ); } -}
\ No newline at end of file +} diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss index ac3541f2c..78e44dfa2 100644 --- a/src/client/views/collections/CollectionDockingView.scss +++ b/src/client/views/collections/CollectionDockingView.scss @@ -89,6 +89,10 @@ position: relative; } +.lm_stack { + position: relative; +} + .lm_drag_tab { padding: 0; width: 15px !important; diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index e9b41de25..ffc004df6 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -1,6 +1,6 @@ import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; -import * as ReactDOM from 'react-dom'; +import * as ReactDOM from 'react-dom/client'; import * as GoldenLayout from '../../../client/goldenLayout'; import { Doc, DocListCast, Opt } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; @@ -25,6 +25,7 @@ import { CollectionFreeFormView } from './collectionFreeForm'; import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; import { TabDocView } from './TabDocView'; import React = require('react'); +import { OpenWhere, OpenWhereMod } from '../nodes/DocumentView'; const _global = (window /* browser */ || global) /* node */ as any; @observer @@ -142,7 +143,7 @@ export class CollectionDockingView extends CollectionSubView() { @undoBatch @action - public static ReplaceTab(document: Doc, panelName: string, stack: any, addToSplit?: boolean): boolean { + public static ReplaceTab(document: Doc, panelName: OpenWhereMod, stack: any, addToSplit?: boolean): boolean { const instance = CollectionDockingView.Instance; if (!instance) return false; const newConfig = CollectionDockingView.makeDocumentConfig(document, panelName); @@ -164,7 +165,7 @@ export class CollectionDockingView extends CollectionSubView() { } @undoBatch - public static ToggleSplit(doc: Doc, location: string, stack?: any, panelName?: string) { + public static ToggleSplit(doc: Doc, location: OpenWhereMod, stack?: any, panelName?: string) { return CollectionDockingView.Instance && Array.from(CollectionDockingView.Instance.tabMap.keys()).findIndex(tab => tab.DashDoc === doc) !== -1 ? CollectionDockingView.CloseSplit(doc) : CollectionDockingView.AddSplit(doc, location, stack, panelName); @@ -175,7 +176,7 @@ export class CollectionDockingView extends CollectionSubView() { // @undoBatch @action - public static AddSplit(document: Doc, pullSide: string, stack?: any, panelName?: string) { + public static AddSplit(document: Doc, pullSide: OpenWhereMod, stack?: any, panelName?: string) { if (document?._viewType === CollectionViewType.Docking) return DashboardView.openDashboard(document); if (!CollectionDockingView.Instance) return false; const tab = Array.from(CollectionDockingView.Instance.tabMap).find(tab => tab.DashDoc === document); @@ -190,7 +191,7 @@ export class CollectionDockingView extends CollectionSubView() { if (!pullSide && stack) { stack.addChild(docContentConfig, undefined); - stack.setActiveContentItem(stack.contentItems[stack.contentItems.length - 1]); + setTimeout(() => stack.setActiveContentItem(stack.contentItems[stack.contentItems.length - 1])); } else { const newContentItem = () => { const newItem = glayRoot.layoutManager.createContentItem({ type: 'stack', content: [docContentConfig] }, instance._goldenLayout); @@ -208,14 +209,15 @@ export class CollectionDockingView extends CollectionSubView() { // if row switch (pullSide) { default: - case 'right': + case OpenWhereMod.none: + case OpenWhereMod.right: glayRoot.contentItems[0].addChild(newContentItem()); break; - case 'left': + case OpenWhereMod.left: glayRoot.contentItems[0].addChild(newContentItem(), 0); break; - case 'top': - case 'bottom': + case OpenWhereMod.top: + case OpenWhereMod.bottom: // if not going in a row layout, must add already existing content into column const rowlayout = glayRoot.contentItems[0]; const newColumn = rowlayout.layoutManager.createContentItem({ type: 'column' }, instance._goldenLayout); @@ -318,7 +320,7 @@ export class CollectionDockingView extends CollectionSubView() { } }; - componentDidMount: () => void = () => { + componentDidMount: () => void = async () => { if (this._containerRef.current) { this._lightboxReactionDisposer = reaction( () => LightboxView.LightboxDoc, @@ -335,8 +337,11 @@ export class CollectionDockingView extends CollectionSubView() { this._ignoreStateChange = ''; } ); - setTimeout(this.setupGoldenLayout); - //window.addEventListener('resize', this.onResize); // bcz: would rather add this event to the parent node, but resize events only come from Window + reaction( + () => this.props.PanelWidth(), + width => !this._goldenLayout && width > 20 && setTimeout(() => this.setupGoldenLayout()), // need to wait for the collectiondockingview-container to have it's width/height since golden layout reads that to configure its windows + { fireImmediately: true } + ); } }; @@ -384,6 +389,7 @@ export class CollectionDockingView extends CollectionSubView() { const className = typeof htmlTarget.className === 'string' ? htmlTarget.className : ''; if (!className.includes('lm_close') && !className.includes('lm_maximise')) { this._flush = UndoManager.StartBatch('golden layout edit'); + DocServer.UPDATE_SERVER_CACHE(); } } } @@ -449,7 +455,7 @@ export class CollectionDockingView extends CollectionSubView() { .map(id => DocServer.GetCachedRefField(id)) .filter(f => f) .map(f => f as Doc); - const changesMade = this.props.Document.dockcingConfig !== json; + const changesMade = this.props.Document.dockingConfig !== json; if (changesMade && !this._flush) { UndoManager.RunInBatch(() => { this.props.Document.dockingConfig = json; @@ -460,7 +466,7 @@ export class CollectionDockingView extends CollectionSubView() { }; tabDestroyed = (tab: any) => { - if (![DocumentType.KVP, DocumentType.PRES].includes(tab.DashDoc?.type)) { + if (tab.DashDoc && ![DocumentType.KVP, DocumentType.PRES].includes(tab.DashDoc?.type)) { Doc.AddDocToList(Doc.MyHeaderBar, 'data', tab.DashDoc); Doc.AddDocToList(Doc.MyRecentlyClosed, 'data', tab.DashDoc, undefined, true, true); } @@ -470,8 +476,8 @@ export class CollectionDockingView extends CollectionSubView() { Doc.RemoveDocFromList(dview, fieldKey, tab.DashDoc); this.tabMap.delete(tab); tab._disposers && Object.values(tab._disposers).forEach((disposer: any) => disposer?.()); - tab.reactComponents?.forEach((ele: any) => ReactDOM.unmountComponentAtNode(ele)); - setTimeout(this.stateChanged); + //tab.reactComponents?.forEach((ele: any) => ReactDOM.unmountComponentAtNode(ele)); + this.stateChanged(); } }; tabCreated = (tab: any) => { @@ -493,7 +499,7 @@ export class CollectionDockingView extends CollectionSubView() { title: `Untitled Tab ${NumCast(dashboard['pane-count'])}`, }); this.props.Document.isShared && inheritParentAcls(this.props.Document, docToAdd); - CollectionDockingView.AddSplit(docToAdd, '', stack); + CollectionDockingView.AddSplit(docToAdd, OpenWhereMod.none, stack); } }); @@ -504,7 +510,12 @@ export class CollectionDockingView extends CollectionSubView() { action(() => { //if (confirm('really close this?')) { if ((!stack.parent.isRoot && !stack.parent.parent.isRoot) || stack.parent.contentItems.length > 1) { + const batch = UndoManager.StartBatch('close stack'); stack.remove(); + setTimeout(() => { + this.stateChanged(); + batch.end(); + }); } else { alert('cant delete the last stack'); } @@ -531,7 +542,7 @@ export class CollectionDockingView extends CollectionSubView() { title: `Untitled Tab ${NumCast(dashboard['pane-count'])}`, }); this.props.Document.isShared && inheritParentAcls(this.props.Document, docToAdd); - CollectionDockingView.AddSplit(docToAdd, '', stack); + CollectionDockingView.AddSplit(docToAdd, OpenWhereMod.none, stack); } }) ); @@ -560,14 +571,14 @@ export class CollectionDockingView extends CollectionSubView() { ScriptingGlobals.add( function openInLightbox(doc: any) { - LightboxView.AddDocTab(doc, 'lightbox'); + LightboxView.AddDocTab(doc, OpenWhere.lightbox); }, 'opens up document in a lightbox', '(doc: any)' ); ScriptingGlobals.add( function openOnRight(doc: any) { - return CollectionDockingView.AddSplit(doc, 'right'); + return CollectionDockingView.AddSplit(doc, OpenWhereMod.right); }, 'opens up document in tab on right side of the screen', '(doc: any)' @@ -580,5 +591,5 @@ ScriptingGlobals.add( '(doc: any)' ); ScriptingGlobals.add(function useRightSplit(doc: any, shiftKey?: boolean) { - CollectionDockingView.ReplaceTab(doc, 'right', undefined, shiftKey); + CollectionDockingView.ReplaceTab(doc, OpenWhereMod.right, undefined, shiftKey); }); diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx index db81f28f6..e2594b6ae 100644 --- a/src/client/views/collections/CollectionMenu.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -31,7 +31,7 @@ import { Colors } from '../global/globalEnums'; import { ActiveFillColor, ActiveInkColor, SetActiveArrowEnd, SetActiveArrowStart, SetActiveBezierApprox, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth } from '../InkingStroke'; import { LightboxView } from '../LightboxView'; import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView'; -import { DocumentView } from '../nodes/DocumentView'; +import { DocumentView, OpenWhereMod } from '../nodes/DocumentView'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; import { RichTextMenu } from '../nodes/formattedText/RichTextMenu'; import { DefaultStyleProvider } from '../StyleProvider'; @@ -569,7 +569,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu startRecording = () => { const doc = Docs.Create.ScreenshotDocument({ title: 'screen recording', _fitWidth: true, _width: 400, _height: 200, mediaState: 'pendingRecording' }); //Doc.AddDocToList(Doc.MyOverlayDocs, undefined, doc); - CollectionDockingView.AddSplit(doc, 'right'); + CollectionDockingView.AddSplit(doc, OpenWhereMod.right); }; @computed @@ -733,11 +733,12 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView doc._currentFrame = 0; CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0); } - CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0); + CollectionFreeFormDocumentView.updateKeyframe(undefined, childDocs, currentFrame || 0); doc._currentFrame = newFrame === undefined ? 0 : Math.max(0, newFrame); } } + _keyTimer: NodeJS.Timeout | undefined; @undoBatch @action nextKeyframe = (): void => { @@ -746,7 +747,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView this.document._currentFrame = 0; CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0); } - CollectionFreeFormDocumentView.updateKeyframe(this.childDocs, currentFrame || 0); + this._keyTimer = CollectionFreeFormDocumentView.updateKeyframe(this._keyTimer, 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)); }; @@ -758,7 +759,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView this.document._currentFrame = 0; CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0); } - CollectionFreeFormDocumentView.gotoKeyframe(this.childDocs.slice()); + this._keyTimer = CollectionFreeFormDocumentView.gotoKeyframe(this._keyTimer, this.childDocs.slice()); this.document._currentFrame = Math.max(0, (currentFrame || 0) - 1); }; diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx index b0f64ed60..26c73438c 100644 --- a/src/client/views/collections/CollectionNoteTakingView.tsx +++ b/src/client/views/collections/CollectionNoteTakingView.tsx @@ -19,7 +19,7 @@ import { undoBatch } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { LightboxView } from '../LightboxView'; -import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment } from '../nodes/DocumentView'; +import { DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere, ViewAdjustment } from '../nodes/DocumentView'; import { FieldViewProps } from '../nodes/FieldView'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; import { StyleProp } from '../StyleProvider'; @@ -180,16 +180,8 @@ export class CollectionNoteTakingView extends CollectionSubView() { return () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); } - addDocTab = (doc: Doc, where: string) => { - if (where === 'inPlace' && this.layoutDoc.isInPlaceContainer) { - this.dataDoc[this.props.fieldKey] = new List<Doc>([doc]); - return true; - } - return this.props.addDocTab(doc, where); - }; - scrollToBottom = () => { - smoothScroll(500, this._mainCont!, this._mainCont!.scrollHeight); + smoothScroll(500, this._mainCont!, this._mainCont!.scrollHeight, 'ease'); }; // let's dive in and get the actual document we want to drag/move around @@ -201,13 +193,12 @@ export class CollectionNoteTakingView extends CollectionSubView() { const top = found.getBoundingClientRect().top; const localTop = this.props.ScreenToLocalTransform().transformPoint(0, top); if (Math.floor(localTop[1]) !== 0) { - smoothScroll((focusSpeed = NumCast(doc.focusSpeed, 500)), this._mainCont!, localTop[1] + this._mainCont!.scrollTop); + smoothScroll((focusSpeed = options.zoomTime ?? 500), this._mainCont!, localTop[1] + this._mainCont!.scrollTop, options.easeFunc); } } const endFocus = async (moved: boolean) => (options?.afterFocus ? options?.afterFocus(moved) : ViewAdjustment.doNothing); this.props.focus(this.rootDoc, { - willZoom: options?.willZoom, - scale: options?.scale, + ...options, afterFocus: (didFocus: boolean) => new Promise<ViewAdjustment>(res => setTimeout(async () => res(await endFocus(didFocus)), focusSpeed)), }); }; @@ -274,7 +265,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { removeDocument={this.props.removeDocument} contentPointerEvents={StrCast(this.layoutDoc.contentPointerEvents)} whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged} - addDocTab={this.addDocTab} + addDocTab={this.props.addDocTab} bringToFront={returnFalse} scriptContext={this.props.scriptContext} pinToPres={this.props.pinToPres} diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx index 4489601db..ba90ed8cd 100644 --- a/src/client/views/collections/CollectionPileView.tsx +++ b/src/client/views/collections/CollectionPileView.tsx @@ -1,33 +1,37 @@ -import { action, computed, IReactionDisposer, reaction } from "mobx"; -import { observer } from "mobx-react"; -import { Doc, HeightSym, WidthSym } from "../../../fields/Doc"; -import { NumCast, StrCast } from "../../../fields/Types"; -import { emptyFunction, returnFalse, returnTrue, setupMoveUpEvents } from "../../../Utils"; -import { DocUtils } from "../../documents/Documents"; -import { SelectionManager } from "../../util/SelectionManager"; -import { SnappingManager } from "../../util/SnappingManager"; -import { undoBatch, UndoManager } from "../../util/UndoManager"; -import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView"; -import "./CollectionPileView.scss"; -import { CollectionSubView } from "./CollectionSubView"; -import React = require("react"); -import { ScriptField } from "../../../fields/ScriptField"; +import { action, computed, IReactionDisposer, reaction } from 'mobx'; +import { observer } from 'mobx-react'; +import { Doc, HeightSym, WidthSym } from '../../../fields/Doc'; +import { NumCast, StrCast } from '../../../fields/Types'; +import { emptyFunction, returnFalse, returnTrue, setupMoveUpEvents } from '../../../Utils'; +import { DocUtils } from '../../documents/Documents'; +import { SelectionManager } from '../../util/SelectionManager'; +import { SnappingManager } from '../../util/SnappingManager'; +import { undoBatch, UndoManager } from '../../util/UndoManager'; +import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; +import './CollectionPileView.scss'; +import { CollectionSubView } from './CollectionSubView'; +import React = require('react'); +import { ScriptField } from '../../../fields/ScriptField'; +import { OpenWhere } from '../nodes/DocumentView'; +import { computePassLayout, computeStarburstLayout } from './collectionFreeForm'; @observer export class CollectionPileView extends CollectionSubView() { - _originalChrome: any = ""; + _originalChrome: any = ''; _disposers: { [name: string]: IReactionDisposer } = {}; componentDidMount() { - if (this.layoutEngine() !== "pass" && this.layoutEngine() !== "starburst") { - this.Document._pileLayoutEngine = "pass"; + if (this.layoutEngine() !== computePassLayout.name && this.layoutEngine() !== computeStarburstLayout.name) { + this.Document._pileLayoutEngine = computePassLayout.name; } this._originalChrome = this.layoutDoc._chromeHidden; this.layoutDoc._chromeHidden = true; - // pileups are designed to go away when they are empty. - this._disposers.selected = reaction(() => this.childDocs.length, - (num) => !num && this.props.ContainingCollectionView?.removeDocument(this.props.Document)); + // pileups are designed to go away when they are empty. + this._disposers.selected = reaction( + () => this.childDocs.length, + num => !num && this.props.ContainingCollectionView?.removeDocument(this.props.Document) + ); } componentWillUnmount() { this.layoutDoc._chromeHidden = this._originalChrome; @@ -38,37 +42,41 @@ export class CollectionPileView extends CollectionSubView() { @undoBatch addPileDoc = (doc: Doc | Doc[]) => { - (doc instanceof Doc ? [doc] : doc).map((d) => DocUtils.iconify(d)); + (doc instanceof Doc ? [doc] : doc).map(d => DocUtils.iconify(d)); return this.props.addDocument?.(doc) || false; - } + }; @undoBatch removePileDoc = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => { - (doc instanceof Doc ? [doc] : doc).map(undoBatch((d) => Doc.deiconifyView(d))); + (doc instanceof Doc ? [doc] : doc).map(undoBatch(d => Doc.deiconifyView(d))); return this.props.moveDocument?.(doc, targetCollection, addDoc) || false; - } + }; toggleIcon = () => { - return ScriptField.MakeScript("documentView.iconify()", { documentView: "any" }); - } + return ScriptField.MakeScript('documentView.iconify()', { documentView: 'any' }); + }; // returns the contents of the pileup in a CollectionFreeFormView @computed get contents() { - const isStarburst = this.layoutEngine() === "starburst"; - return <div className="collectionPileView-innards" - style={{ pointerEvents: isStarburst || SnappingManager.GetIsDragging() ? undefined : "none" }} > - <CollectionFreeFormView {...this.props} - layoutEngine={this.layoutEngine} - childDocumentsActive={isStarburst ? returnTrue : undefined} - addDocument={this.addPileDoc} - childClickScript={this.toggleIcon()} - moveDocument={this.removePileDoc} /> - </div>; + const isStarburst = this.layoutEngine() === computeStarburstLayout.name; + return ( + <div className="collectionPileView-innards" style={{ pointerEvents: isStarburst || SnappingManager.GetIsDragging() ? undefined : 'none' }}> + <CollectionFreeFormView + {...this.props} + layoutEngine={this.layoutEngine} + childDocumentsActive={isStarburst ? returnTrue : undefined} + addDocument={this.addPileDoc} + childCanEmbedOnDrag={true} + childClickScript={this.toggleIcon()} + moveDocument={this.removePileDoc} + /> + </div> + ); } // toggles the pileup between starburst to compact toggleStarburst = action(() => { - if (this.layoutEngine() === 'starburst') { + if (this.layoutEngine() === computeStarburstLayout.name) { const defaultSize = 110; this.rootDoc.x = NumCast(this.rootDoc.x) + this.layoutDoc[WidthSym]() / 2 - NumCast(this.layoutDoc._starburstPileWidth, defaultSize) / 2; this.rootDoc.y = NumCast(this.rootDoc.y) + this.layoutDoc[HeightSym]() / 2 - NumCast(this.layoutDoc._starburstPileHeight, defaultSize) / 2; @@ -77,12 +85,12 @@ export class CollectionPileView extends CollectionSubView() { DocUtils.pileup(this.childDocs, undefined, undefined, NumCast(this.layoutDoc._width) / 2, false); this.layoutDoc._panX = 0; this.layoutDoc._panY = -10; - this.props.Document._pileLayoutEngine = 'pass'; + this.props.Document._pileLayoutEngine = computePassLayout.name; } else { const defaultSize = 25; !this.layoutDoc._starburstRadius && (this.layoutDoc._starburstRadius = 250); !this.layoutDoc._starburstDocScale && (this.layoutDoc._starburstDocScale = 2.5); - if (this.layoutEngine() === 'pass') { + if (this.layoutEngine() === computePassLayout.name) { this.rootDoc.x = NumCast(this.rootDoc.x) + this.layoutDoc[WidthSym]() / 2 - defaultSize / 2; this.rootDoc.y = NumCast(this.rootDoc.y) + this.layoutDoc[HeightSym]() / 2 - defaultSize / 2; this.layoutDoc._starburstPileWidth = this.layoutDoc[WidthSym](); @@ -90,7 +98,7 @@ export class CollectionPileView extends CollectionSubView() { } this.layoutDoc._panX = this.layoutDoc._panY = 0; this.layoutDoc._width = this.layoutDoc._height = defaultSize; - this.props.Document._pileLayoutEngine = 'starburst'; + this.props.Document._pileLayoutEngine = computeStarburstLayout.name; } }); @@ -99,27 +107,35 @@ export class CollectionPileView extends CollectionSubView() { pointerDown = (e: React.PointerEvent) => { let dist = 0; SnappingManager.SetIsDragging(true); - 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]); - if (dist > 100) { - if (!this._undoBatch) { - this._undoBatch = UndoManager.StartBatch("layout pile"); + 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]); + if (dist > 100) { + if (!this._undoBatch) { + this._undoBatch = UndoManager.StartBatch('layout pile'); + } + const doc = this.childDocs[0]; + doc.x = e.clientX; + doc.y = e.clientY; + this.props.addDocTab(doc, OpenWhere.inParentFromScreen) && (this.props.removeDocument?.(doc) || false); + dist = 0; } - const doc = this.childDocs[0]; - doc.x = e.clientX; - doc.y = e.clientY; - this.props.addDocTab(doc, "inParent") && (this.props.removeDocument?.(doc) || false); - dist = 0; } - } - return false; - }, () => { - this._undoBatch?.end(); - this._undoBatch = undefined; - SnappingManager.SetIsDragging(false); - }, emptyFunction, e.shiftKey && this.layoutEngine() === "pass", this.layoutEngine() === "pass" && e.shiftKey); // this sets _doubleTap - } + return false; + }, + () => { + this._undoBatch?.end(); + this._undoBatch = undefined; + SnappingManager.SetIsDragging(false); + }, + emptyFunction, + e.shiftKey && this.layoutEngine() === computePassLayout.name, + this.layoutEngine() === computePassLayout.name && e.shiftKey + ); // this sets _doubleTap + }; // onClick for toggling the pileup view @undoBatch @@ -130,12 +146,13 @@ export class CollectionPileView extends CollectionSubView() { this.toggleStarburst(); e.stopPropagation(); } - } + }; render() { - return <div className={`collectionPileView`} onClick={this.onClick} onPointerDown={this.pointerDown} - style={{ width: this.props.PanelWidth(), height: "100%" }}> - {this.contents} - </div>; + return ( + <div className={`collectionPileView`} onClick={this.onClick} onPointerDown={this.pointerDown} style={{ width: this.props.PanelWidth(), height: '100%' }}> + {this.contents} + </div> + ); } } diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx index 7bf798656..e9bf03208 100644 --- a/src/client/views/collections/CollectionStackedTimeline.tsx +++ b/src/client/views/collections/CollectionStackedTimeline.tsx @@ -8,6 +8,7 @@ import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; import { ComputedField, ScriptField } from '../../../fields/ScriptField'; import { Cast, NumCast } from '../../../fields/Types'; +import { ImageField } from '../../../fields/URLField'; import { emptyFunction, formatTime, OmitKeys, returnFalse, returnOne, returnTrue, setupMoveUpEvents, smoothScrollHorizontal, StopEvent } from '../../../Utils'; import { Docs } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; @@ -23,13 +24,10 @@ import { AudioWaveform } from '../AudioWaveform'; import { CollectionSubView } from '../collections/CollectionSubView'; import { Colors } from '../global/globalEnums'; import { LightboxView } from '../LightboxView'; -import { DocFocusFunc, DocFocusOptions, DocumentView, DocumentViewProps, DocumentViewSharedProps } from '../nodes/DocumentView'; +import { DocFocusFunc, DocFocusOptions, DocumentView, DocumentViewProps } from '../nodes/DocumentView'; import { LabelBox } from '../nodes/LabelBox'; -import './CollectionStackedTimeline.scss'; import { VideoBox } from '../nodes/VideoBox'; -import { ImageField } from '../../../fields/URLField'; -import { StyleProp } from '../StyleProvider'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import './CollectionStackedTimeline.scss'; export type CollectionStackedTimelineProps = { Play: () => void; @@ -610,7 +608,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack height={height} toTimeline={this.toTimeline} layoutDoc={this.layoutDoc} - // isDocumentActive={this.props.childDocumentsActive ? this.props.isDocumentActive : this.isContentActive} + isDocumentActive={this.isContentActive} currentTimecode={this.currentTimecode} _timeline={this._timeline} stackedTimeline={this} @@ -702,7 +700,7 @@ interface StackedTimelineAnchorProps { endTag: string; renderDepth: number; layoutDoc: Doc; - isDocumentActive?: () => boolean; + isDocumentActive?: () => boolean | undefined; ScreenToLocalTransform: () => Transform; _timeline: HTMLDivElement | null; focus: DocFocusFunc; @@ -748,7 +746,7 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps> time < NumCast(this.props.mark[this.props.endTag]) && this._lastTimecode < NumCast(this.props.mark[this.props.startTag]) - 1e-5 ) { - LinkFollower.FollowLink(undefined, this.props.mark, this.props as any as DocumentViewProps, false, true); + LinkFollower.FollowLink(undefined, this.props.mark, this.props as any as DocumentViewProps, false); } this._lastTimecode = time; } @@ -806,25 +804,6 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps> return [resetTitle]; }; - innerStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any => { - if (property === StyleProp.Decorations && doc && NumCast(doc.timecodeToHide) - NumCast(doc.timecodeToShow) < 0.0002) { - return ( - <div className="styleProvider-lock"> - <FontAwesomeIcon - icon={'camera'} - style={{ color: 'red' }} - onClick={e => { - LinkFollower.FollowLink(undefined, doc, props as DocumentViewSharedProps, e.altKey); - e.stopPropagation(); - }} - size="lg" - /> - </div> - ); - } - return this.props.styleProvider?.(doc, props, property); - }; - // renders anchor LabelBox renderInner = computedFn(function (this: StackedTimelineAnchor, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), screenXf: () => Transform, width: () => number, height: () => number) { const anchor = observable({ view: undefined as any }); @@ -841,7 +820,7 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps> ref={action((r: DocumentView | null) => (anchor.view = r))} Document={mark} DataDoc={undefined} - styleProvider={this.innerStyleProvider} + styleProvider={this.props.styleProvider} renderDepth={this.props.renderDepth + 1} LayoutTemplate={undefined} LayoutTemplateString={LabelBox.LayoutStringWithTitle('data', this.computeTitle())} diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 77b47ed82..acf59b5da 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -3,12 +3,12 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { CursorProperty } from 'csstype'; import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; -import { DataSym, Doc, 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 { listSpec } from '../../../fields/Schema'; import { SchemaHeaderField } from '../../../fields/SchemaHeaderField'; -import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; +import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, returnZero, setupMoveUpEvents, smoothScroll, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; @@ -22,7 +22,7 @@ import { ContextMenuProps } from '../ContextMenuItem'; import { EditableView } from '../EditableView'; import { LightboxView } from '../LightboxView'; import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView'; -import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment } from '../nodes/DocumentView'; +import { DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere, ViewAdjustment } from '../nodes/DocumentView'; import { FieldViewProps } from '../nodes/FieldView'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; import { StyleProp } from '../StyleProvider'; @@ -33,6 +33,7 @@ import { CollectionSubView } from './CollectionSubView'; const _global = (window /* browser */ || global) /* node */ as any; export type collectionStackingViewProps = { + sortFunc?: (a: Doc, b: Doc) => number; chromeHidden?: boolean; // view type is stacking viewType?: CollectionViewType; @@ -73,7 +74,9 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection } // filteredChildren is what you want to work with. It's the list of things that you're currently displaying @computed get filteredChildren() { - return this.childLayoutPairs.filter(pair => pair.layout instanceof Doc && !pair.layout.hidden).map(pair => pair.layout); + const children = this.childLayoutPairs.filter(pair => pair.layout instanceof Doc && !pair.layout.hidden).map(pair => pair.layout); + if (this.props.sortFunc) children.sort(this.props.sortFunc); + return children; } // how much margin we give the header @computed get headerMargin() { @@ -238,16 +241,8 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection return () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); } - addDocTab = (doc: Doc, where: string) => { - if (where === 'inPlace' && this.layoutDoc.isInPlaceContainer) { - this.dataDoc[this.props.fieldKey] = new List<Doc>([doc]); - return true; - } - return this.props.addDocTab(doc, where); - }; - scrollToBottom = () => { - smoothScroll(500, this._mainCont!, this._mainCont!.scrollHeight); + smoothScroll(500, this._mainCont!, this._mainCont!.scrollHeight, 'ease'); }; // let's dive in and get the actual document we want to drag/move around @@ -260,13 +255,12 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection const top = found.getBoundingClientRect().top; const localTop = this.props.ScreenToLocalTransform().transformPoint(0, top); if (Math.floor(localTop[1]) !== 0) { - smoothScroll((focusSpeed = NumCast(doc.focusSpeed, 500)), this._mainCont!, localTop[1] + this._mainCont!.scrollTop); + smoothScroll((focusSpeed = options.zoomTime ?? 500), this._mainCont!, localTop[1] + this._mainCont!.scrollTop, options.easeFunc); } } const endFocus = async (moved: boolean) => options?.afterFocus?.(moved) ?? ViewAdjustment.doNothing; this.props.focus(this.rootDoc, { - willZoom: options?.willZoom, - scale: options?.scale, + ...options, afterFocus: (didFocus: boolean) => new Promise<ViewAdjustment>(res => setTimeout(async () => res(await endFocus(didFocus)), focusSpeed)), }); }; @@ -305,6 +299,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection isChildContentActive = () => this.props.isDocumentActive?.() && (this.props.childDocumentsActive?.() || BoolCast(this.rootDoc.childDocumentsActive)) ? true : this.props.childDocumentsActive?.() === false || this.rootDoc.childDocumentsActive === false ? false : undefined; + isChildButtonContentActive = () => (this.props.childDocumentsActive?.() === false || this.rootDoc.childDocumentsActive === false ? false : undefined); // this is what renders the document that you see on the screen // called in Children: this actually adds a document to our children list getDisplayDoc(doc: Doc, width: () => number) { @@ -326,7 +321,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection styleProvider={this.styleProvider} docViewPath={this.props.docViewPath} fitWidth={this.props.childFitWidth} - isContentActive={this.isChildContentActive} + isContentActive={doc.isLinkButton ? this.isChildButtonContentActive : this.isChildContentActive} onKey={this.onKeyDown} isDocumentActive={this.isContentActive} LayoutTemplate={this.props.childLayoutTemplate} @@ -357,7 +352,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection removeDocument={this.props.removeDocument} contentPointerEvents={StrCast(this.layoutDoc.contentPointerEvents)} whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged} - addDocTab={this.addDocTab} + addDocTab={this.props.addDocTab} bringToFront={returnFalse} scriptContext={this.props.scriptContext} pinToPres={this.props.pinToPres} diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 7bc273d7d..d2074219a 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -111,7 +111,7 @@ export function CollectionSubView<X>(moreProps?: X) { rawdocs = rootDoc && !this.props.isAnnotationOverlay ? [Doc.GetProto(rootDoc)] : []; } - const docs = rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(Doc.GetProto(d)) !== AclPrivate).map(d => d as Doc); + const docs = rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(Doc.GetProto(d)) !== AclPrivate && (this.props.ignoreUnrendered || !d.unrendered)).map(d => d as Doc); const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField); const childDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs; @@ -172,8 +172,6 @@ export function CollectionSubView<X>(moreProps?: X) { // The following conditional detects a recurring bug we've seen on the server if (proto[Id] === Docs.Prototypes.get(DocumentType.COL)[Id]) { alert('COLLECTION PROTO CURSOR ISSUE DETECTED! Check console for more info...'); - console.log(doc); - console.log(proto); throw new Error(`AHA! You were trying to set a cursor on a collection's proto, which is the original collection proto! Look at the two previously printed lines for document values!`); } let cursors = Cast(proto.cursors, listSpec(CursorField)); diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx index 3dd9d2d84..a1466bcd0 100644 --- a/src/client/views/collections/CollectionTimeView.tsx +++ b/src/client/views/collections/CollectionTimeView.tsx @@ -1,32 +1,32 @@ -import { toUpper } from "lodash"; -import { action, computed, observable, runInAction } from "mobx"; -import { observer } from "mobx-react"; -import { Doc, Opt, StrListCast } from "../../../fields/Doc"; -import { List } from "../../../fields/List"; -import { ObjectField } from "../../../fields/ObjectField"; -import { RichTextField } from "../../../fields/RichTextField"; -import { listSpec } from "../../../fields/Schema"; -import { ComputedField, ScriptField } from "../../../fields/ScriptField"; -import { Cast, NumCast, StrCast } from "../../../fields/Types"; -import { emptyFunction, returnEmptyString, returnFalse, returnTrue, setupMoveUpEvents } from "../../../Utils"; -import { Docs } from "../../documents/Documents"; -import { DocumentType } from "../../documents/DocumentTypes"; -import { DocumentManager } from "../../util/DocumentManager"; -import { ScriptingGlobals } from "../../util/ScriptingGlobals"; -import { ContextMenu } from "../ContextMenu"; -import { ContextMenuProps } from "../ContextMenuItem"; -import { EditableView } from "../EditableView"; -import { ViewSpecPrefix } from "../nodes/DocumentView"; -import { ViewDefBounds } from "./collectionFreeForm/CollectionFreeFormLayoutEngines"; -import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView"; -import { CollectionSubView } from "./CollectionSubView"; -import "./CollectionTimeView.scss"; -import React = require("react"); +import { toUpper } from 'lodash'; +import { action, computed, observable, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; +import { Doc, Opt, StrListCast } from '../../../fields/Doc'; +import { List } from '../../../fields/List'; +import { ObjectField } from '../../../fields/ObjectField'; +import { RichTextField } from '../../../fields/RichTextField'; +import { listSpec } from '../../../fields/Schema'; +import { ComputedField, ScriptField } from '../../../fields/ScriptField'; +import { Cast, NumCast, StrCast } from '../../../fields/Types'; +import { emptyFunction, returnEmptyString, returnFalse, returnTrue, setupMoveUpEvents } from '../../../Utils'; +import { Docs } from '../../documents/Documents'; +import { DocumentType } from '../../documents/DocumentTypes'; +import { DocumentManager } from '../../util/DocumentManager'; +import { ScriptingGlobals } from '../../util/ScriptingGlobals'; +import { ContextMenu } from '../ContextMenu'; +import { ContextMenuProps } from '../ContextMenuItem'; +import { EditableView } from '../EditableView'; +import { ViewSpecPrefix } from '../nodes/DocumentView'; +import { computePivotLayout, computeTimelineLayout, ViewDefBounds } from './collectionFreeForm/CollectionFreeFormLayoutEngines'; +import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; +import { CollectionSubView } from './CollectionSubView'; +import './CollectionTimeView.scss'; +import React = require('react'); @observer export class CollectionTimeView extends CollectionSubView() { _changing = false; - @observable _layoutEngine = "pivot"; + @observable _layoutEngine = computePivotLayout.name; @observable _collapsed: boolean = false; @observable _childClickedScript: Opt<ScriptField>; @observable _viewDefDivClick: Opt<ScriptField>; @@ -35,7 +35,7 @@ export class CollectionTimeView extends CollectionSubView() { getAnchor = () => { const anchor = Docs.Create.HTMLAnchorDocument([], { title: ComputedField.MakeFunction(`"${this.pivotField}"])`) as any, - annotationOn: this.rootDoc + annotationOn: this.rootDoc, }); // save view spec information for anchor @@ -43,81 +43,103 @@ export class CollectionTimeView extends CollectionSubView() { proto.pivotField = this.pivotField; proto.docFilters = ObjectField.MakeCopy(this.layoutDoc._docFilters as ObjectField) || new List<string>([]); proto.docRangeFilters = ObjectField.MakeCopy(this.layoutDoc._docRangeFilters as ObjectField) || new List<string>([]); - proto[ViewSpecPrefix + "_viewType"] = this.layoutDoc._viewType; + proto[ViewSpecPrefix + '_viewType'] = this.layoutDoc._viewType; // store anchor in annotations list of document (not technically needed since these anchors are never drawn) - if (Cast(this.dataDoc[this.props.fieldKey + "-annotations"], listSpec(Doc), null) !== undefined) { - Cast(this.dataDoc[this.props.fieldKey + "-annotations"], listSpec(Doc), []).push(anchor); + if (Cast(this.dataDoc[this.props.fieldKey + '-annotations'], listSpec(Doc), null) !== undefined) { + Cast(this.dataDoc[this.props.fieldKey + '-annotations'], listSpec(Doc), []).push(anchor); } else { - this.dataDoc[this.props.fieldKey + "-annotations"] = new List<Doc>([anchor]); + this.dataDoc[this.props.fieldKey + '-annotations'] = new List<Doc>([anchor]); } return anchor; - } + }; async componentDidMount() { this.props.setContentView?.(this); //const detailView = (await DocCastAsync(this.props.Document.childClickedOpenTemplateView)) || DocUtils.findTemplate("detailView", StrCast(this.rootDoc.type), ""); - ///const childText = "const alias = getAlias(self); switchView(alias, detailView); alias.dropAction='alias'; alias.removeDropProperties=new List<string>(['dropAction']); useRightSplit(alias, shiftKey); "; + ///const childText = "const alias = getAlias(self); switchView(alias, detailView); alias.dropAction='alias'; useRightSplit(alias, shiftKey); "; runInAction(() => { - this._childClickedScript = ScriptField.MakeScript("openInLightbox(self)", { this: Doc.name }); - this._viewDefDivClick = ScriptField.MakeScript("pivotColumnClick(this,payload)", { payload: "any" }); + this._childClickedScript = ScriptField.MakeScript('openInLightbox(self)', { this: Doc.name }); + this._viewDefDivClick = ScriptField.MakeScript('pivotColumnClick(this,payload)', { payload: 'any' }); }); } - get pivotField() { return this._focusPivotField || StrCast(this.layoutDoc._pivotField); } + get pivotField() { + return this._focusPivotField || StrCast(this.layoutDoc._pivotField); + } @action setViewSpec = (anchor: Doc, preview: boolean) => { - if (preview) { // if in preview, then override document's fields with view spec + if (preview) { + // if in preview, then override document's fields with view spec this._focusFilters = StrListCast(Doc.GetProto(anchor).docFilters); this._focusRangeFilters = StrListCast(Doc.GetProto(anchor).docRangeFilters); this._focusPivotField = StrCast(anchor.pivotField); - } else if (anchor.pivotField !== undefined) { // otherwise set document's fields based on anchor view spec + } else if (anchor.pivotField !== undefined) { + // otherwise set document's fields based on anchor view spec this.layoutDoc._prevFilterIndex = 1; this.layoutDoc._pivotField = StrCast(anchor.pivotField); this.layoutDoc._docFilters = new List<string>(StrListCast(anchor.docFilters)); this.layoutDoc._docRangeFilters = new List<string>(StrListCast(anchor.docRangeFilters)); } return 0; - } + }; layoutEngine = () => this._layoutEngine; - toggleVisibility = action(() => this._collapsed = !this._collapsed); + toggleVisibility = action(() => (this._collapsed = !this._collapsed)); onMinDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => { - const minReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMinReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMin"], 0)); - const maxReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMaxReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMax"], 10)); - this.props.Document[this.props.fieldKey + "-timelineMinReq"] = minReq + (maxReq - minReq) * delta[0] / this.props.PanelWidth(); - this.props.Document[this.props.fieldKey + "-timelineSpan"] = undefined; - return false; - }), returnFalse, emptyFunction); - } + setupMoveUpEvents( + this, + e, + action((e: PointerEvent, down: number[], delta: number[]) => { + const minReq = NumCast(this.props.Document[this.props.fieldKey + '-timelineMinReq'], NumCast(this.props.Document[this.props.fieldKey + '-timelineMin'], 0)); + const maxReq = NumCast(this.props.Document[this.props.fieldKey + '-timelineMaxReq'], NumCast(this.props.Document[this.props.fieldKey + '-timelineMax'], 10)); + this.props.Document[this.props.fieldKey + '-timelineMinReq'] = minReq + ((maxReq - minReq) * delta[0]) / this.props.PanelWidth(); + this.props.Document[this.props.fieldKey + '-timelineSpan'] = undefined; + return false; + }), + returnFalse, + emptyFunction + ); + }; onMaxDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => { - const minReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMinReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMin"], 0)); - const maxReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMaxReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMax"], 10)); - this.props.Document[this.props.fieldKey + "-timelineMaxReq"] = maxReq + (maxReq - minReq) * delta[0] / this.props.PanelWidth(); - return false; - }), returnFalse, emptyFunction); - } + setupMoveUpEvents( + this, + e, + action((e: PointerEvent, down: number[], delta: number[]) => { + const minReq = NumCast(this.props.Document[this.props.fieldKey + '-timelineMinReq'], NumCast(this.props.Document[this.props.fieldKey + '-timelineMin'], 0)); + const maxReq = NumCast(this.props.Document[this.props.fieldKey + '-timelineMaxReq'], NumCast(this.props.Document[this.props.fieldKey + '-timelineMax'], 10)); + this.props.Document[this.props.fieldKey + '-timelineMaxReq'] = maxReq + ((maxReq - minReq) * delta[0]) / this.props.PanelWidth(); + return false; + }), + returnFalse, + emptyFunction + ); + }; onMidDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => { - const minReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMinReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMin"], 0)); - const maxReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMaxReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMax"], 10)); - this.props.Document[this.props.fieldKey + "-timelineMinReq"] = minReq - (maxReq - minReq) * delta[0] / this.props.PanelWidth(); - this.props.Document[this.props.fieldKey + "-timelineMaxReq"] = maxReq - (maxReq - minReq) * delta[0] / this.props.PanelWidth(); - return false; - }), returnFalse, emptyFunction); - } + setupMoveUpEvents( + this, + e, + action((e: PointerEvent, down: number[], delta: number[]) => { + const minReq = NumCast(this.props.Document[this.props.fieldKey + '-timelineMinReq'], NumCast(this.props.Document[this.props.fieldKey + '-timelineMin'], 0)); + const maxReq = NumCast(this.props.Document[this.props.fieldKey + '-timelineMaxReq'], NumCast(this.props.Document[this.props.fieldKey + '-timelineMax'], 10)); + this.props.Document[this.props.fieldKey + '-timelineMinReq'] = minReq - ((maxReq - minReq) * delta[0]) / this.props.PanelWidth(); + this.props.Document[this.props.fieldKey + '-timelineMaxReq'] = maxReq - ((maxReq - minReq) * delta[0]) / this.props.PanelWidth(); + return false; + }), + returnFalse, + emptyFunction + ); + }; goTo = (prevFilterIndex: number) => { - this.layoutDoc._pivotField = this.layoutDoc["_prevPivotFields" + prevFilterIndex]; - this.layoutDoc._docFilters = ObjectField.MakeCopy(this.layoutDoc["_prevDocFilter" + prevFilterIndex] as ObjectField); - this.layoutDoc._docRangeFilters = ObjectField.MakeCopy(this.layoutDoc["_prevDocRangeFilters" + prevFilterIndex] as ObjectField); + this.layoutDoc._pivotField = this.layoutDoc['_prevPivotFields' + prevFilterIndex]; + this.layoutDoc._docFilters = ObjectField.MakeCopy(this.layoutDoc['_prevDocFilter' + prevFilterIndex] as ObjectField); + this.layoutDoc._docRangeFilters = ObjectField.MakeCopy(this.layoutDoc['_prevDocRangeFilters' + prevFilterIndex] as ObjectField); this.layoutDoc._prevFilterIndex = prevFilterIndex; - } + }; @action contentsDown = (e: React.MouseEvent) => { @@ -127,37 +149,58 @@ export class CollectionTimeView extends CollectionSubView() { } else { this.layoutDoc._docFilters = new List([]); } - } + }; dontScaleFilter = (doc: Doc) => doc.type === DocumentType.RTF; @computed get contents() { - return <div className="collectionTimeView-innards" key="timeline" style={{ pointerEvents: this.props.isContentActive() ? undefined : "none" }} - onClick={this.contentsDown}> - <CollectionFreeFormView {...this.props} - engineProps={{ pivotField: this.pivotField, docFilters: this.childDocFilters, docRangeFilters: this.childDocRangeFilters }} - fitContentsToBox={returnTrue} - childClickScript={this._childClickedScript} - viewDefDivClick={this._viewDefDivClick} - //dontScaleFilter={this.dontScaleFilter} - layoutEngine={this.layoutEngine} /> - </div>; + return ( + <div className="collectionTimeView-innards" key="timeline" style={{ pointerEvents: this.props.isContentActive() ? undefined : 'none' }} onClick={this.contentsDown}> + <CollectionFreeFormView + {...this.props} + engineProps={{ pivotField: this.pivotField, docFilters: this.childDocFilters, docRangeFilters: this.childDocRangeFilters }} + fitContentsToBox={returnTrue} + childClickScript={this._childClickedScript} + viewDefDivClick={this._viewDefDivClick} + //dontScaleFilter={this.dontScaleFilter} + layoutEngine={this.layoutEngine} + /> + </div> + ); } public static SyncTimelineToPresentation(doc: Doc) { const fieldKey = Doc.LayoutFieldKey(doc); - doc[fieldKey + "-timelineCur"] = ComputedField.MakeFunction("(activePresentationItem()[this._pivotField || 'year'] || 0)"); + doc[fieldKey + '-timelineCur'] = ComputedField.MakeFunction("(activePresentationItem()[this._pivotField || 'year'] || 0)"); } specificMenu = (e: React.MouseEvent) => { const layoutItems: ContextMenuProps[] = []; const doc = this.layoutDoc; - layoutItems.push({ description: "Force Timeline", event: () => { doc._forceRenderEngine = "timeline"; }, icon: "compress-arrows-alt" }); - layoutItems.push({ description: "Force Pivot", event: () => { doc._forceRenderEngine = "pivot"; }, icon: "compress-arrows-alt" }); - layoutItems.push({ description: "Auto Time/Pivot layout", event: () => { doc._forceRenderEngine = undefined; }, icon: "compress-arrows-alt" }); - layoutItems.push({ description: "Sync with presentation", event: () => CollectionTimeView.SyncTimelineToPresentation(doc), icon: "compress-arrows-alt" }); + layoutItems.push({ + description: 'Force Timeline', + event: () => { + doc._forceRenderEngine = computeTimelineLayout.name; + }, + icon: 'compress-arrows-alt', + }); + layoutItems.push({ + description: 'Force Pivot', + event: () => { + doc._forceRenderEngine = computePivotLayout.name; + }, + icon: 'compress-arrows-alt', + }); + layoutItems.push({ + description: 'Auto Time/Pivot layout', + event: () => { + doc._forceRenderEngine = undefined; + }, + icon: 'compress-arrows-alt', + }); + layoutItems.push({ description: 'Sync with presentation', event: () => CollectionTimeView.SyncTimelineToPresentation(doc), icon: 'compress-arrows-alt' }); - ContextMenu.Instance.addItem({ description: "Options...", subitems: layoutItems, icon: "eye" }); - } + ContextMenu.Instance.addItem({ description: 'Options...', subitems: layoutItems, icon: 'eye' }); + }; @computed get _allFacets() { const facets = new Set<string>(); this.childDocs.forEach(child => Object.keys(Doc.GetProto(child)).forEach(key => facets.add(key))); @@ -169,37 +212,40 @@ export class CollectionTimeView extends CollectionSubView() { const docItems: ContextMenuProps[] = []; const keySet: Set<string> = new Set(); - this.childLayoutPairs.map(pair => this._allFacets.filter(fieldKey => - pair.layout[fieldKey] instanceof RichTextField || - typeof (pair.layout[fieldKey]) === "number" || - typeof (pair.layout[fieldKey]) === "boolean" || - typeof (pair.layout[fieldKey]) === "string").filter(fieldKey => fieldKey[0] !== "_" && (fieldKey[0] !== "#" || fieldKey === "#") && (fieldKey === "tags" || fieldKey[0] === toUpper(fieldKey)[0])).map(fieldKey => keySet.add(fieldKey))); - Array.from(keySet).map(fieldKey => - docItems.push({ description: ":" + fieldKey, event: () => this.layoutDoc._pivotField = fieldKey, icon: "compress-arrows-alt" })); - docItems.push({ description: ":default", event: () => this.layoutDoc._pivotField = undefined, icon: "compress-arrows-alt" }); - ContextMenu.Instance.addItem({ description: "Pivot Fields ...", subitems: docItems, icon: "eye" }); + this.childLayoutPairs.map(pair => + this._allFacets + .filter(fieldKey => pair.layout[fieldKey] instanceof RichTextField || typeof pair.layout[fieldKey] === 'number' || typeof pair.layout[fieldKey] === 'boolean' || typeof pair.layout[fieldKey] === 'string') + .filter(fieldKey => fieldKey[0] !== '_' && (fieldKey[0] !== '#' || fieldKey === '#') && (fieldKey === 'tags' || fieldKey[0] === toUpper(fieldKey)[0])) + .map(fieldKey => keySet.add(fieldKey)) + ); + Array.from(keySet).map(fieldKey => docItems.push({ description: ':' + fieldKey, event: () => (this.layoutDoc._pivotField = fieldKey), icon: 'compress-arrows-alt' })); + docItems.push({ description: ':default', event: () => (this.layoutDoc._pivotField = undefined), icon: 'compress-arrows-alt' }); + ContextMenu.Instance.addItem({ description: 'Pivot Fields ...', subitems: docItems, icon: 'eye' }); const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(x, y); - ContextMenu.Instance.displayMenu(x, y, ":"); - } + ContextMenu.Instance.displayMenu(x, y, ':'); + }; @computed get pivotKeyUI() { - return <div className={"pivotKeyEntry"}> - <EditableView - GetValue={returnEmptyString} - SetValue={(value: any) => { - if (value?.length) { - this.layoutDoc._pivotField = value; - return true; - } - return false; - }} - toggle={this.toggleVisibility} - background={"#f1efeb"} // this.props.headingObject ? this.props.headingObject.color : "#f1efeb"; - contents={":" + StrCast(this.layoutDoc._pivotField)} - showMenuOnLoad={true} - display={"inline"} - menuCallback={this.menuCallback} /> - </div>; + return ( + <div className={'pivotKeyEntry'}> + <EditableView + GetValue={returnEmptyString} + SetValue={(value: any) => { + if (value?.length) { + this.layoutDoc._pivotField = value; + return true; + } + return false; + }} + toggle={this.toggleVisibility} + background={'#f1efeb'} // this.props.headingObject ? this.props.headingObject.color : "#f1efeb"; + contents={':' + StrCast(this.layoutDoc._pivotField)} + showMenuOnLoad={true} + display={'inline'} + menuCallback={this.menuCallback} + /> + </div> + ); } render() { @@ -211,55 +257,62 @@ export class CollectionTimeView extends CollectionSubView() { } }); const forceLayout = StrCast(this.layoutDoc._forceRenderEngine); - const doTimeline = forceLayout ? (forceLayout === "timeline") : nonNumbers / this.childDocs.length < 0.1 && this.props.PanelWidth() / this.props.PanelHeight() > 6; - if (doTimeline !== (this._layoutEngine === "timeline")) { + const doTimeline = forceLayout ? forceLayout === computeTimelineLayout.name : nonNumbers / this.childDocs.length < 0.1 && this.props.PanelWidth() / this.props.PanelHeight() > 6; + if (doTimeline !== (this._layoutEngine === computeTimelineLayout.name)) { if (!this._changing) { this._changing = true; - setTimeout(action(() => { - this._layoutEngine = doTimeline ? "timeline" : "pivot"; - this._changing = false; - }), 0); + setTimeout( + action(() => { + this._layoutEngine = doTimeline ? computeTimelineLayout.name : computePivotLayout.name; + this._changing = false; + }), + 0 + ); } } - return <div className={"collectionTimeView" + (doTimeline ? "" : "-pivot")} onContextMenu={this.specificMenu} - style={{ width: this.props.PanelWidth(), height: "100%" }}> - {this.pivotKeyUI} - {this.contents} - {!this.props.isSelected() || !doTimeline ? (null) : <> - <div className="collectionTimeView-thumb-min collectionTimeView-thumb" key="min" onPointerDown={this.onMinDown} /> - <div className="collectionTimeView-thumb-max collectionTimeView-thumb" key="mid" onPointerDown={this.onMaxDown} /> - <div className="collectionTimeView-thumb-mid collectionTimeView-thumb" key="max" onPointerDown={this.onMidDown} /> - </>} - </div>; + return ( + <div className={'collectionTimeView' + (doTimeline ? '' : '-pivot')} onContextMenu={this.specificMenu} style={{ width: this.props.PanelWidth(), height: '100%' }}> + {this.pivotKeyUI} + {this.contents} + {!this.props.isSelected() || !doTimeline ? null : ( + <> + <div className="collectionTimeView-thumb-min collectionTimeView-thumb" key="min" onPointerDown={this.onMinDown} /> + <div className="collectionTimeView-thumb-max collectionTimeView-thumb" key="mid" onPointerDown={this.onMaxDown} /> + <div className="collectionTimeView-thumb-mid collectionTimeView-thumb" key="max" onPointerDown={this.onMidDown} /> + </> + )} + </div> + ); } } ScriptingGlobals.add(function pivotColumnClick(pivotDoc: Doc, bounds: ViewDefBounds) { - const pivotField = StrCast(pivotDoc._pivotField) || "author"; + const pivotField = StrCast(pivotDoc._pivotField) || 'author'; let prevFilterIndex = NumCast(pivotDoc._prevFilterIndex); const originalFilter = StrListCast(ObjectField.MakeCopy(pivotDoc._docFilters as ObjectField)); - pivotDoc["_prevDocFilter" + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._docFilters as ObjectField); - pivotDoc["_prevDocRangeFilters" + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._docRangeFilters as ObjectField); - pivotDoc["_prevPivotFields" + prevFilterIndex] = pivotField; + pivotDoc['_prevDocFilter' + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._docFilters as ObjectField); + pivotDoc['_prevDocRangeFilters' + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._docRangeFilters as ObjectField); + pivotDoc['_prevPivotFields' + prevFilterIndex] = pivotField; pivotDoc._prevFilterIndex = ++prevFilterIndex; pivotDoc._docFilters = new List(); - setTimeout(action(() => { - const filterVals = (bounds.payload as string[]); - filterVals.map(filterVal => Doc.setDocFilter(pivotDoc, pivotField, filterVal, "check")); - const pivotView = DocumentManager.Instance.getDocumentView(pivotDoc); - if (pivotDoc && pivotView?.ComponentView instanceof CollectionTimeView && filterVals.length === 1) { - if (pivotView?.ComponentView.childDocs.length && pivotView.ComponentView.childDocs[0][filterVals[0]]) { - pivotDoc._pivotField = filterVals[0]; + setTimeout( + action(() => { + const filterVals = bounds.payload as string[]; + filterVals.map(filterVal => Doc.setDocFilter(pivotDoc, pivotField, filterVal, 'check')); + const pivotView = DocumentManager.Instance.getDocumentView(pivotDoc); + if (pivotDoc && pivotView?.ComponentView instanceof CollectionTimeView && filterVals.length === 1) { + if (pivotView?.ComponentView.childDocs.length && pivotView.ComponentView.childDocs[0][filterVals[0]]) { + pivotDoc._pivotField = filterVals[0]; + } } - } - const newFilters = StrListCast(pivotDoc._docFilters); - if (newFilters.length && originalFilter.length && - newFilters.lastElement() === originalFilter.lastElement()) { - pivotDoc._prevFilterIndex = --prevFilterIndex; - pivotDoc["_prevDocFilter" + prevFilterIndex] = undefined; - pivotDoc["_prevDocRangeFilters" + prevFilterIndex] = undefined; - pivotDoc["_prevPivotFields" + prevFilterIndex] = undefined; - } - })); -});
\ No newline at end of file + const newFilters = StrListCast(pivotDoc._docFilters); + if (newFilters.length && originalFilter.length && newFilters.lastElement() === originalFilter.lastElement()) { + pivotDoc._prevFilterIndex = --prevFilterIndex; + pivotDoc['_prevDocFilter' + prevFilterIndex] = undefined; + pivotDoc['_prevDocRangeFilters' + prevFilterIndex] = undefined; + pivotDoc['_prevPivotFields' + prevFilterIndex] = undefined; + } + }) + ); +}); diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index 3785b7d61..273b08247 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -67,7 +67,8 @@ font-style: italic; font-size: 8pt; margin-left: 3px; - display: none; + opacity: 0; + pointer-events: none; } .collectionTreeView-contents { diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 0ff89c5a7..1a265af4a 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -115,7 +115,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree const titleHeight = !this._titleRef ? this.marginTop() : Number(getComputedStyle(this._titleRef).height.replace('px', '')); const bodyHeight = Array.from(this.refList).reduce((p, r) => p + Number(getComputedStyle(r).height.replace('px', '')), this.marginBot()) + 6; this.layoutDoc._autoHeightMargins = bodyHeight; - this.props.setHeight?.(bodyHeight + titleHeight); + !this.props.dontRegisterView && this.props.setHeight?.(bodyHeight + titleHeight); } }; unobserveHeight = (ref: any) => { diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 9f63a11aa..a28b1ca19 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -16,6 +16,7 @@ import { InteractionUtils } from '../../util/InteractionUtils'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent'; +import { OpenWhere, OpenWhereMod } from '../nodes/DocumentView'; import { FieldView, FieldViewProps } from '../nodes/FieldView'; import { CollectionCarousel3DView } from './CollectionCarousel3DView'; import { CollectionCarouselView } from './CollectionCarouselView'; @@ -42,6 +43,7 @@ interface CollectionViewProps_ extends FieldViewProps { layoutEngine?: () => string; setPreviewCursor?: (func: (x: number, y: number, drag: boolean, hide: boolean) => void) => void; setBrushViewer?: (func?: (view: { width: number; height: number; panX: number; panY: number }) => void) => void; + ignoreUnrendered?: boolean; // property overrides for child documents childDocuments?: Doc[]; // used to override the documents shown by the sub collection to an explicit list (see LinkBox) @@ -54,6 +56,7 @@ interface CollectionViewProps_ extends FieldViewProps { childHideDecorationTitle?: () => boolean; childHideResizeHandles?: () => boolean; childLayoutTemplate?: () => Doc | undefined; // specify a layout Doc template to use for children of the collection + childCanEmbedOnDrag?: boolean; childXPadding?: number; childYPadding?: number; childLayoutString?: string; @@ -173,7 +176,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab vtype => { const newRendition = Doc.MakeAlias(this.rootDoc); newRendition._viewType = vtype; - this.props.addDocTab(newRendition, 'add:right'); + this.props.addDocTab(newRendition, OpenWhere.addRight); return newRendition; }, false @@ -183,17 +186,17 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab const optionItems = options && 'subitems' in options ? options.subitems : []; !Doc.noviceMode ? optionItems.splice(0, 0, { description: `${this.rootDoc.forceActive ? 'Select' : 'Force'} Contents Active`, event: () => (this.rootDoc.forceActive = !this.rootDoc.forceActive), icon: 'project-diagram' }) : null; if (this.rootDoc.childLayout instanceof Doc) { - optionItems.push({ description: 'View Child Layout', event: () => this.props.addDocTab(this.rootDoc.childLayout as Doc, 'add:right'), icon: 'project-diagram' }); + optionItems.push({ description: 'View Child Layout', event: () => this.props.addDocTab(this.rootDoc.childLayout as Doc, OpenWhere.addRight), icon: 'project-diagram' }); } if (this.rootDoc.childClickedOpenTemplateView instanceof Doc) { - optionItems.push({ description: 'View Child Detailed Layout', event: () => this.props.addDocTab(this.rootDoc.childClickedOpenTemplateView as Doc, 'add:right'), icon: 'project-diagram' }); + optionItems.push({ description: 'View Child Detailed Layout', event: () => this.props.addDocTab(this.rootDoc.childClickedOpenTemplateView as Doc, OpenWhere.addRight), icon: 'project-diagram' }); } !Doc.noviceMode && optionItems.push({ description: `${this.rootDoc.isInPlaceContainer ? 'Unset' : 'Set'} inPlace Container`, event: () => (this.rootDoc.isInPlaceContainer = !this.rootDoc.isInPlaceContainer), icon: 'project-diagram' }); if (!Doc.noviceMode && false) { optionItems.push({ description: 'Create Branch', - event: async () => this.props.addDocTab(await BranchCreate(this.rootDoc), 'add:right'), + event: async () => this.props.addDocTab(await BranchCreate(this.rootDoc), OpenWhere.addRight), icon: 'project-diagram', }); optionItems.push({ @@ -224,7 +227,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab event: (obj: any) => { const alias = Doc.MakeAlias(this.rootDoc); DocUtils.makeCustomViewClicked(alias, undefined, func.key); - this.props.addDocTab(alias, 'add:right'); + this.props.addDocTab(alias, OpenWhere.addRight); }, }) ); diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index cde5132a3..5c05b5c13 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -4,14 +4,14 @@ import { Tooltip } from '@material-ui/core'; import { clamp } from 'lodash'; import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; -import * as ReactDOM from 'react-dom'; +import * as ReactDOM from 'react-dom/client'; import { DataSym, Doc, HeightSym, Opt, WidthSym } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { FieldId } from '../../../fields/RefField'; import { listSpec } from '../../../fields/Schema'; import { ScriptField } from '../../../fields/ScriptField'; -import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types'; +import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../fields/Types'; import { emptyFunction, lightOrDark, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick, Utils } from '../../../Utils'; import { DocServer } from '../../DocServer'; import { DocUtils } from '../../documents/Documents'; @@ -26,7 +26,7 @@ import { DashboardView } from '../DashboardView'; import { Colors, Shadows } from '../global/globalEnums'; import { LightboxView } from '../LightboxView'; import { MainView } from '../MainView'; -import { DocFocusOptions, DocumentView, DocumentViewProps } from '../nodes/DocumentView'; +import { DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere, OpenWhereMod } from '../nodes/DocumentView'; import { DashFieldView } from '../nodes/formattedText/DashFieldView'; import { PinProps, PresBox, PresMovement } from '../nodes/trails'; import { DefaultStyleProvider, StyleProp } from '../StyleProvider'; @@ -55,6 +55,11 @@ export class TabDocView extends React.Component<TabDocViewProps> { @computed get layoutDoc() { return this._document && Doc.Layout(this._document); } + @computed get tabBorderColor() { + const highlight = DefaultStyleProvider(this._document, undefined, StyleProp.Highlighting); + if (highlight?.highlightIndex >= Doc.DocBrushStatus.highlighted) return highlight.highlightColor; + return 'transparent'; + } @computed get tabColor() { let tabColor = StrCast(this._document?._backgroundColor, StrCast(this._document?.backgroundColor, DefaultStyleProvider(this._document, undefined, StyleProp.BackgroundColor))); if (tabColor === 'transparent') return 'black'; @@ -143,21 +148,19 @@ export class TabDocView extends React.Component<TabDocViewProps> { const docIcon = <FontAwesomeIcon onPointerDown={dragBtnDown} icon={iconType} />; const closeIcon = <FontAwesomeIcon icon={'eye'} />; - ReactDOM.render(docIcon, iconWrap); - ReactDOM.render(closeIcon, closeWrap); + ReactDOM.createRoot(iconWrap).render(docIcon); + ReactDOM.createRoot(closeWrap).render(closeIcon); tab.reactComponents = [iconWrap, closeWrap]; tab.element[0].prepend(iconWrap); - tab._disposers.layerDisposer = reaction( - () => ({ layer: tab.DashDoc.activeLayer, color: this.tabColor }), - ({ layer, color }) => { - // console.log("TabDocView: " + this.tabColor); - // console.log("lightOrDark: " + lightOrDark(this.tabColor)); + tab._disposers.color = reaction( + () => ({ color: this.tabColor, borderColor: this.tabBorderColor }), + coloring => { const textColor = lightOrDark(this.tabColor); //not working with StyleProp.Color titleEle.style.color = textColor; - titleEle.style.backgroundColor = 'transparent'; + titleEle.style.backgroundColor = coloring.borderColor; iconWrap.style.color = textColor; closeWrap.style.color = textColor; - tab.element[0].style.background = !layer ? color : 'dimgrey'; + tab.element[0].style.background = coloring.color; }, { fireImmediately: true } ); @@ -193,11 +196,12 @@ export class TabDocView extends React.Component<TabDocViewProps> { () => SelectionManager.Views().some(v => v.topMost && v.props.Document === doc), action(selected => { if (selected) this._activated = true; - const toggle = tab.element[0].children[1].children[0] as HTMLInputElement; + const toggle = tab.element[0].children[2].children[0] as HTMLInputElement; selected && tab.contentItem !== tab.header.parent.getActiveContentItem() && UndoManager.RunInBatch(() => tab.header.parent.setActiveContentItem(tab.contentItem), 'tab switch'); - // toggle.style.fontWeight = selected ? "bold" : ""; + toggle.style.fontWeight = selected ? 'bold' : ''; // toggle.style.textTransform = selected ? "uppercase" : ""; - }) + }), + { fireImmediately: true } ); // highlight the tab when the tab document is brushed in any part of the UI @@ -226,7 +230,7 @@ export class TabDocView extends React.Component<TabDocViewProps> { * Adds a document to the presentation view **/ @action - public static PinDoc(docs: Doc | Doc[], pinProps?: PinProps) { + public static PinDoc(docs: Doc | Doc[], pinProps: PinProps) { const docList = docs instanceof Doc ? [docs] : docs; const batch = UndoManager.StartBatch('pinning doc'); @@ -243,11 +247,13 @@ export class TabDocView extends React.Component<TabDocViewProps> { alert('Cannot pin presentation document to itself'); return; } - const pinDoc = Doc.MakeAlias(doc); - pinDoc.presentationTargetDoc = doc; + const anchorDoc = DocumentManager.Instance.getDocumentView(doc)?.ComponentView?.getAnchor?.(); + const pinDoc = Doc.MakeAlias(anchorDoc ?? doc); + pinDoc.presentationTargetDoc = anchorDoc ?? doc; pinDoc.title = doc.title + ' - Slide'; pinDoc.data = new List<Doc>(); // the children of the alias' layout are the presentation slide children. the alias' data field might be children of a collection, PDF data, etc -- in any case we don't want the tree view to "see" this data pinDoc.presMovement = doc.type === DocumentType.SCRIPTING || pinProps?.pinDocLayout ? PresMovement.None : PresMovement.Zoom; + pinDoc.presDuration = 2000; pinDoc.groupWithUp = false; pinDoc.context = curPres; // these should potentially all be props passed down by the CollectionTreeView to the TreeView elements. That way the PresBox could configure all of its children at render time @@ -268,7 +274,7 @@ export class TabDocView extends React.Component<TabDocViewProps> { pinDoc.presStartTime = NumCast(doc.clipStart); pinDoc.presEndTime = NumCast(doc.clipEnd, duration); } - PresBox.pinDocView(pinDoc, pinProps, doc); + PresBox.pinDocView(pinDoc, pinProps.pinDocContent ? { ...pinProps, pinData: PresBox.pinDataTypes(doc) } : pinProps, doc); pinDoc.onClick = ScriptField.MakeFunction('navigateToDoc(self.presentationTargetDoc, self)'); Doc.AddDocToList(curPres, 'data', pinDoc, presSelected); //save position @@ -277,6 +283,11 @@ export class TabDocView extends React.Component<TabDocViewProps> { pinDoc.title = doc.title + ' (move)'; pinDoc.presMovement = PresMovement.Pan; } + if (pinProps?.currentFrame !== undefined) { + pinDoc.presCurrentFrame = pinProps?.currentFrame; + pinDoc.title = doc.title + ' (move)'; + pinDoc.presMovement = PresMovement.Pan; + } if (pinDoc.isInkMask) { pinDoc.presHideAfter = true; pinDoc.presHideBefore = true; @@ -293,8 +304,8 @@ export class TabDocView extends React.Component<TabDocViewProps> { ) { const docs = Cast(Doc.MyOverlayDocs.data, listSpec(Doc), []); if (docs.includes(curPres)) docs.splice(docs.indexOf(curPres), 1); - CollectionDockingView.AddSplit(curPres, 'right'); - setTimeout(() => DocumentManager.Instance.jumpToDocument(docList.lastElement(), false, undefined, []), 100); // keeps the pinned doc in view since the sidebar shifts things + CollectionDockingView.AddSplit(curPres, OpenWhereMod.right); + setTimeout(() => DocumentManager.Instance.jumpToDocument(docList.lastElement(), { willPan: true }, undefined, []), 100); // keeps the pinned doc in view since the sidebar shifts things } setTimeout(batch.end, 500); // need to wait until dockingview (goldenlayout) updates all its structurs } @@ -340,31 +351,21 @@ export class TabDocView extends React.Component<TabDocViewProps> { // "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) => { + addDocTab = (doc: Doc, location: OpenWhere) => { SelectionManager.DeselectAll(); - const locationFields = doc._viewType === CollectionViewType.Docking ? ['dashboard'] : location.split(':'); - const locationParams = locationFields.length > 1 ? locationFields[1] : ''; - switch (locationFields[0]) { - case 'dashboard': - return DashboardView.openDashboard(doc); - case 'close': - return CollectionDockingView.CloseSplit(doc, locationParams); - case 'fullScreen': - return CollectionDockingView.OpenFullScreen(doc); - case 'replace': - return CollectionDockingView.ReplaceTab(doc, locationParams, this.stack); - // case "lightbox": { - // // TabDocView.PinDoc(doc, { hidePresBox: true }); - // return LightboxView.AddDocTab(doc, location, undefined, this.addDocTab); - // } - case 'lightbox': - return LightboxView.AddDocTab(doc, location, undefined, this.addDocTab); - case 'toggle': - return CollectionDockingView.ToggleSplit(doc, locationParams, this.stack); - case 'inPlace': - case 'add': - default: - return CollectionDockingView.AddSplit(doc, locationParams, this.stack); + const whereFields = doc._viewType === CollectionViewType.Docking ? [OpenWhere.dashboard] : location.split(':'); + const whereMods: OpenWhereMod = whereFields.length > 1 ? (whereFields[1] as OpenWhereMod) : OpenWhereMod.none; + if (doc.dockingConfig) return DashboardView.openDashboard(doc); + // prettier-ignore + switch (whereFields[0]) { + case OpenWhere.inPlace: // fall through to lightbox + case OpenWhere.lightbox: return LightboxView.AddDocTab(doc, location, undefined, this.addDocTab); + case OpenWhere.dashboard: return DashboardView.openDashboard(doc); + case OpenWhere.fullScreen: return CollectionDockingView.OpenFullScreen(doc); + case OpenWhere.close: return CollectionDockingView.CloseSplit(doc, whereMods); + case OpenWhere.replace: return CollectionDockingView.ReplaceTab(doc, whereMods, this.stack); + case OpenWhere.toggle: return CollectionDockingView.ToggleSplit(doc, whereMods, this.stack); + case OpenWhere.add:default:return CollectionDockingView.AddSplit(doc, whereMods, this.stack); } }; remDocTab = (doc: Doc | Doc[]) => { @@ -382,17 +383,10 @@ export class TabDocView extends React.Component<TabDocViewProps> { @action focusFunc = (doc: Doc, options: DocFocusOptions) => { const shrinkwrap = options?.originalTarget === this._document && this.view?.ComponentView?.shrinkWrap; - if (options?.willZoom !== false && shrinkwrap && this._document) { - const focusSpeed = NumCast(this._document.focusSpeed, 500); + if (options?.willPanZoom !== false && shrinkwrap && this._document) { + const focusSpeed = options.zoomTime ?? 500; shrinkwrap(); - this._document._viewTransition = `transform ${focusSpeed}ms`; - setTimeout( - action(() => { - this._document!._viewTransition = undefined; - options?.afterFocus?.(false); - }), - focusSpeed - ); + this._view?.setViewTransition('transform', focusSpeed, () => options?.afterFocus?.(false)); } else { options?.afterFocus?.(false); } @@ -501,7 +495,7 @@ interface TabMinimapViewProps { document: Doc; hideMinimap: () => boolean; tabView: () => DocumentView | undefined; - addDocTab: (doc: Doc, where: string) => boolean; + addDocTab: (doc: Doc, where: OpenWhere) => boolean; PanelWidth: () => number; PanelHeight: () => number; background: () => string; diff --git a/src/client/views/collections/TreeView.scss b/src/client/views/collections/TreeView.scss index 83fee013a..7eab03e1d 100644 --- a/src/client/views/collections/TreeView.scss +++ b/src/client/views/collections/TreeView.scss @@ -173,7 +173,8 @@ .treeView-header:hover { .collectionTreeView-keyHeader { - display: inherit; + opacity: unset; + pointer-events: unset; } .treeView-openRight { diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index 1e97eee37..bd326f917 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -22,7 +22,7 @@ import { Transform } from '../../util/Transform'; import { undoBatch, UndoManager } from '../../util/UndoManager'; import { EditableView } from '../EditableView'; import { TREE_BULLET_WIDTH } from '../global/globalCssVariables.scss'; -import { DocumentView, DocumentViewInternal, DocumentViewProps, StyleProviderFunc } from '../nodes/DocumentView'; +import { DocumentView, DocumentViewInternal, DocumentViewProps, OpenWhere, StyleProviderFunc } from '../nodes/DocumentView'; import { FieldViewProps } from '../nodes/FieldView'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; import { RichTextMenu } from '../nodes/formattedText/RichTextMenu'; @@ -44,7 +44,7 @@ export interface TreeViewProps { containerCollection: Doc; renderDepth: number; dropAction: dropActionType; - addDocTab: (doc: Doc, where: string) => boolean; + addDocTab: (doc: Doc, where: OpenWhere) => boolean; panelWidth: () => number; panelHeight: () => number; addDocument: (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => boolean; @@ -55,7 +55,7 @@ export interface TreeViewProps { indentDocument?: (editTitle: boolean) => void; outdentDocument?: (editTitle: boolean) => void; ScreenToLocalTransform: () => Transform; - contextMenuItems: { script: ScriptField; filter: ScriptField; icon: string; label: string }[]; + contextMenuItems?: { script: ScriptField; filter: ScriptField; icon: string; label: string }[]; dontRegisterView?: boolean; styleProvider?: StyleProviderFunc | undefined; treeViewHideHeaderFields: () => boolean; @@ -236,7 +236,7 @@ export class TreeView extends React.Component<TreeViewProps> { const bestAlias = docView.props.Document.author === Doc.CurrentUserEmail && !Doc.IsPrototype(docView.props.Document) ? docView.props.Document : DocListCast(this.props.document.aliases).find(doc => !doc.context && doc.author === Doc.CurrentUserEmail); const nextBestAlias = DocListCast(this.props.document.aliases).find(doc => doc.author === Doc.CurrentUserEmail); - this.props.addDocTab(bestAlias ?? nextBestAlias ?? Doc.MakeAlias(this.props.document), 'lightbox'); + this.props.addDocTab(bestAlias ?? nextBestAlias ?? Doc.MakeAlias(this.props.document), OpenWhere.lightbox); } }; @@ -302,7 +302,7 @@ export class TreeView extends React.Component<TreeViewProps> { 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 * 0.75) || (!before && this.treeViewOpen && this.childDocList.length); + const inside = pt[0] > Math.min(rect.left + 75, rect.left + rect.width * 0.75) || (!before && this.treeViewOpen && this.childDocs?.length); this._header.current!.className = 'treeView-header'; if (!this.props.treeView.outlineMode || DragManager.DocDragData?.treeViewDoc === this.props.treeView.rootDoc) { if (inside) this._header.current!.className += ' treeView-header-inside'; @@ -362,7 +362,7 @@ export class TreeView extends React.Component<TreeViewProps> { if (!this._header.current) return; const rect = this._header.current.getBoundingClientRect(); const before = pt[1] < rect.top + rect.height / 2; - const inside = this.props.treeView.fileSysMode && !this.doc.isFolder ? false : pt[0] > Math.min(rect.left + 75, rect.left + rect.width * 0.75) || (!before && this.treeViewOpen && this.childDocList.length); + const inside = this.props.treeView.fileSysMode && !this.doc.isFolder ? false : pt[0] > Math.min(rect.left + 75, rect.left + rect.width * 0.75) || (!before && this.treeViewOpen && this.childDocs?.length ? true : false); if (de.complete.linkDragData) { const sourceDoc = de.complete.linkDragData.linkSourceGetAnchor(); const destDoc = this.doc; @@ -401,7 +401,6 @@ export class TreeView extends React.Component<TreeViewProps> { getTransform = () => this.refTransform(this._tref.current); embeddedPanelWidth = () => this.props.panelWidth() / (this.props.treeView.props.NativeDimScaling?.() || 1); embeddedPanelHeight = () => { - console.log(this.props.treeView.rootDoc.title); const layoutDoc = (temp => temp && Doc.expandTemplateLayout(temp, this.props.document, ''))(this.props.treeView.props.childLayoutTemplate?.()) || this.layoutDoc; return Math.min( layoutDoc[HeightSym](), @@ -417,7 +416,6 @@ export class TreeView extends React.Component<TreeViewProps> { })() ); }; - @computed get expandedField() { const ids: { [key: string]: string } = {}; const rows: JSX.Element[] = []; @@ -429,6 +427,8 @@ export class TreeView extends React.Component<TreeViewProps> { const contents = doc[key]; let contentElement: (JSX.Element | null)[] | JSX.Element = []; + let leftOffset = observable({ width: 0 }); + const expandedWidth = () => this.props.panelWidth() - leftOffset.width; 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) => { @@ -453,7 +453,7 @@ export class TreeView extends React.Component<TreeViewProps> { this.titleStyleProvider, this.props.ScreenToLocalTransform, this.props.isContentActive, - this.props.panelWidth, + expandedWidth, this.props.renderDepth, this.props.treeViewHideHeaderFields, [...this.props.renderedIds, doc[Id]], @@ -484,15 +484,21 @@ export class TreeView extends React.Component<TreeViewProps> { ); } rows.push( - <div style={{ display: 'flex' }} key={key}> - <span style={{ fontWeight: 'bold' }}>{key + ':'}</span> - + <div style={{ display: 'flex', overflow: 'auto' }} key={key}> + <span + ref={action((r: any) => { + if (r) leftOffset.width = r.getBoundingClientRect().width; + })} + style={{ fontWeight: 'bold' }}> + {key + ':'} + + </span> {contentElement} </div> ); } rows.push( - <div style={{ display: 'flex' }} key={'newKeyValue'}> + <div style={{ display: 'flex', overflow: 'auto' }} key={'newKeyValue'}> <EditableView key="editableView" contents={'+key:value'} @@ -509,7 +515,7 @@ export class TreeView extends React.Component<TreeViewProps> { @computed get renderContent() { TraceMobx(); const expandKey = this.treeViewExpandedView; - const sortings = this.props.styleProvider?.(this.doc, this.props.treeView.props, StyleProp.TreeViewSortings) as { [key: string]: { color: string; label: string } }; + const sortings = (this.props.styleProvider?.(this.doc, this.props.treeView.props, StyleProp.TreeViewSortings) as { [key: string]: { color: string; label: string } }) ?? {}; if (['links', 'annotations', 'aliases', this.fieldKey].includes(expandKey)) { const sorting = StrCast(this.doc.treeViewSortCriterion, TreeSort.None); const sortKeys = Object.keys(sortings); @@ -693,6 +699,7 @@ export class TreeView extends React.Component<TreeViewProps> { this.treeViewOpen = true; }; + @observable headerEleWidth = 0; @computed get headerElements() { return this.props.treeViewHideHeaderFields() || this.doc.treeViewHideHeaderFields ? null : ( <> @@ -729,7 +736,7 @@ export class TreeView extends React.Component<TreeViewProps> { const openAlias = { script: ScriptField.MakeFunction(`openOnRight(getAlias(self))`)!, icon: 'copy', label: 'Open Alias' }; const focusDoc = { script: ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!, icon: 'eye', label: 'Focus or Open' }; return [ - ...this.props.contextMenuItems.filter(mi => (!mi.filter ? true : mi.filter.script.run({ doc: this.doc })?.result)), + ...(this.props.contextMenuItems ?? []).filter(mi => (!mi.filter ? true : mi.filter.script.run({ doc: this.doc })?.result)), ...(this.doc.isFolder ? folderOp : Doc.IsSystem(this.doc) @@ -823,7 +830,7 @@ export class TreeView extends React.Component<TreeViewProps> { } return false; }; - titleWidth = () => Math.max(20, Math.min(this.props.treeView.truncateTitleWidth(), this.props.panelWidth())) / (this.props.treeView.props.NativeDimScaling?.() || 1) - 3 * treeBulletWidth(); + titleWidth = () => Math.max(20, Math.min(this.props.treeView.truncateTitleWidth(), this.props.panelWidth())) / (this.props.treeView.props.NativeDimScaling?.() || 1) - this.headerEleWidth - treeBulletWidth(); return18 = () => 18; /** @@ -831,7 +838,6 @@ export class TreeView extends React.Component<TreeViewProps> { */ @computed get renderTitle() { - // TraceMobx(); const view = this._editTitle ? ( <EditableView @@ -868,7 +874,7 @@ export class TreeView extends React.Component<TreeViewProps> { } })} Document={this.doc} - fitWidth={(doc: Doc) => true} + fitWidth={returnTrue} DataDoc={undefined} scriptContext={this} hideDecorationTitle={this.props.treeView.outlineMode} @@ -925,7 +931,7 @@ export class TreeView extends React.Component<TreeViewProps> { }}> {view} </div> - <div className="treeView-rightButtons"> + <div className="treeView-rightButtons" ref={action((r: any) => r && (this.headerEleWidth = r.getBoundingClientRect().width))}> {buttons} {/* hide and lock buttons */} {this.headerElements} </div> @@ -1035,7 +1041,7 @@ export class TreeView extends React.Component<TreeViewProps> { const pt = [de.clientX, de.clientY]; const rect = this._header.current!.getBoundingClientRect(); const before = pt[1] < rect.top + rect.height / 2; - const inside = this.props.treeView.fileSysMode && !this.doc.isFolder ? false : pt[0] > Math.min(rect.left + 75, rect.left + rect.width * 0.75) || (!before && this.treeViewOpen && this.childDocList.length); + const inside = this.props.treeView.fileSysMode && !this.doc.isFolder ? false : pt[0] > Math.min(rect.left + 75, rect.left + rect.width * 0.75) || (!before && this.treeViewOpen && this.childDocs?.length ? true : false); const docs = this.props.treeView.onTreeDrop(de, (docs: Doc[]) => this.dropDocuments(docs, before, inside, 'copy', undefined, false)); }; @@ -1103,7 +1109,7 @@ export class TreeView extends React.Component<TreeViewProps> { remove: undefined | ((doc: Doc | Doc[]) => boolean), move: DragManager.MoveFunction, dropAction: dropActionType, - addDocTab: (doc: Doc, where: string) => boolean, + addDocTab: (doc: Doc, where: OpenWhere) => boolean, styleProvider: undefined | StyleProviderFunc, screenToLocalXf: () => Transform, isContentActive: (outsideReaction?: boolean) => boolean, diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx index 89cc22d07..7dd9cdb8b 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx @@ -46,7 +46,7 @@ export interface PoolData { export interface ViewDefResult { ele: JSX.Element; bounds?: ViewDefBounds; - inkMask?: number; //sort elements into either the mask layer (which has a mixedBlendMode appropriate for transparent masks), or the regular documents layer; -1 = no mask, 0 = mask layer but stroke is transprent (hidden), >0 = mask layer and not hidden + inkMask?: number; //sort elements into either the mask layer (which has a mixedBlendMode appropriate for transparent masks), or the regular documents layer; -1 = no mask, 0 = mask layer but stroke is transparent (hidden, as in during a presentation when you want to smoothly animate it into being a mask), >0 = mask layer and not hidden } function toLabel(target: FieldResult<Field>) { if (typeof target === 'number' || Number(target)) { @@ -82,7 +82,7 @@ interface PivotColumn { filters: string[]; } -export function computerPassLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) { +export function computePassLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) { const docMap = new Map<string, PoolData>(); childPairs.forEach(({ layout, data }, i) => { docMap.set(layout[Id], { @@ -97,7 +97,7 @@ export function computerPassLayout(poolData: Map<string, PoolData>, pivotDoc: Do return normalizeResults(panelDim, 12, docMap, poolData, viewDefsToJSX, [], 0, []); } -export function computerStarburstLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) { +export function computeStarburstLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) { const mustFit = pivotDoc[WidthSym]() !== panelDim[0]; // if a panel size is set that's not the same as the pivot doc's size, then assume this is in a panel for a content fitting view (like a grid) in which case everything must be scaled to stay within the panel const docMap = new Map<string, PoolData>(); const docSize = mustFit ? panelDim[0] * 0.33 : 75; // assume an icon sized at 75 diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss index 858719a08..b44acfce8 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss @@ -5,15 +5,8 @@ transition: opacity 0.5s ease-in; fill: transparent; } -.collectionfreeformlinkview-linkCircle { - stroke: rgb(0,0,0); - opacity: 0.5; - pointer-events: all; - cursor: pointer; -} .collectionfreeformlinkview-linkText { - stroke: rgb(0,0,0); - opacity: 0.5; + stroke: rgb(0, 0, 0); pointer-events: all; cursor: move; } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index bf9de6760..8919b1c01 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -3,7 +3,7 @@ import { observer } from 'mobx-react'; import { Doc, Field } from '../../../../fields/Doc'; import { Id } from '../../../../fields/FieldSymbols'; import { List } from '../../../../fields/List'; -import { Cast, NumCast } from '../../../../fields/Types'; +import { Cast, NumCast, StrCast } from '../../../../fields/Types'; import { emptyFunction, setupMoveUpEvents, Utils } from '../../../../Utils'; import { LinkManager } from '../../../util/LinkManager'; import { SelectionManager } from '../../../util/SelectionManager'; @@ -44,7 +44,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo this._start = Date.now(); this._timeout && clearTimeout(this._timeout); this._timeout = setTimeout(this.timeout, 25); - setTimeout(this.placeAnchors); + setTimeout(this.placeAnchors, 10); // when docs are dragged, their transforms will update before a render has been performed. placeanchors needs to come after a render to find things in the dom. a 0 timeout will still come before the render }), { fireImmediately: true } ); @@ -117,12 +117,16 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo return false; }, emptyFunction, - () => { + action(() => { + SelectionManager.DeselectAll(); + SelectionManager.SelectSchemaViewDoc(this.props.LinkDocs[0], true); + LinkManager.currentLink = this.props.LinkDocs[0]; + this.toggleProperties(); // OverlayView.Instance.addElement( // <LinkEditor sourceDoc={this.props.A.props.Document} linkDoc={this.props.LinkDocs[0]} // showLinks={action(() => { })} // />, { x: 300, y: 300 }); - } + }) ); }; @@ -163,15 +167,16 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo @action toggleProperties = () => { - if (SettingsManager.propertiesWidth > 0) { - SettingsManager.propertiesWidth = 0; - } else { + if ((SettingsManager.propertiesWidth ?? 0) < 100) { SettingsManager.propertiesWidth = 250; } }; + @action onClickLine = () => { + SelectionManager.DeselectAll(); SelectionManager.SelectSchemaViewDoc(this.props.LinkDocs[0], true); + LinkManager.currentLink = this.props.LinkDocs[0]; this.toggleProperties(); }; @@ -199,15 +204,17 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo const bclipped = bleft !== b.left || btop !== b.top; if (aclipped && bclipped) return undefined; const clipped = aclipped || bclipped; + const pt1inside = NumCast(LinkDocs[0].anchor1_x) % 100 !== 0 && NumCast(LinkDocs[0].anchor1_y) % 100 !== 0; + const pt2inside = NumCast(LinkDocs[0].anchor2_x) % 100 !== 0 && NumCast(LinkDocs[0].anchor2_y) % 100 !== 0; const pt1 = [aleft + a.width / 2, atop + a.height / 2]; const pt2 = [bleft + b.width / 2, btop + b.width / 2]; - const pt1vec = [(bDocBounds.left + bDocBounds.right) / 2 - pt1[0], (bDocBounds.top + bDocBounds.bottom) / 2 - pt1[1]]; - const pt2vec = [(aDocBounds.left + aDocBounds.right) / 2 - pt2[0], (aDocBounds.top + aDocBounds.bottom) / 2 - pt2[1]]; + const pt2vec = pt2inside ? [-0.7071, 0.7071] : [(bDocBounds.left + bDocBounds.right) / 2 - pt2[0], (bDocBounds.top + bDocBounds.bottom) / 2 - pt2[1]]; + const pt1vec = pt1inside ? [-0.7071, 0.7071] : [(aDocBounds.left + aDocBounds.right) / 2 - pt1[0], (aDocBounds.top + aDocBounds.bottom) / 2 - pt1[1]]; 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; - const pt1norm = clipped ? [0, 0] : [(pt1vec[0] / pt1len) * ptlen, (pt1vec[1] / pt1len) * ptlen]; - const pt2norm = clipped ? [0, 0] : [(pt2vec[0] / pt2len) * ptlen, (pt2vec[1] / pt2len) * ptlen]; + const pt1norm = clipped ? [0, 0] : [-(pt1vec[0] / pt1len) * ptlen, -(pt1vec[1] / pt1len) * ptlen]; + const pt2norm = clipped ? [0, 0] : [-(pt2vec[0] / pt2len) * ptlen, -(pt2vec[1] / pt2len) * ptlen]; const pt1normlen = Math.sqrt(pt1norm[0] * pt1norm[0] + pt1norm[1] * pt1norm[1]) || 1; const pt2normlen = Math.sqrt(pt2norm[0] * pt2norm[0] + pt2norm[1] * pt2norm[1]) || 1; const pt1normalized = [pt1norm[0] / pt1normlen, pt1norm[1] / pt1normlen]; @@ -217,51 +224,82 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo const textX = (Math.min(pt1[0], pt2[0]) + Math.max(pt1[0], pt2[0])) / 2 + NumCast(LinkDocs[0].linkOffsetX); const textY = (pt1[1] + pt2[1]) / 2 + NumCast(LinkDocs[0].linkOffsetY); - return { a, b, pt1norm, pt2norm, aActive, bActive, textX, textY, pt1: [pt1[0] + pt1normalized[0] * 13, pt1[1] + pt1normalized[1] * 13], pt2: [pt2[0] + pt2normalized[0] * 13, pt2[1] + pt2normalized[1] * 13] }; + return { + a, + b, + pt1norm, + pt2norm, + aActive, + bActive, + textX, + textY, + pt1: [pt1[0] + pt1normalized[0] * 13, pt1[1] + pt1normalized[1] * 13], + pt2: [pt2[0] + pt2normalized[0] * 13, pt2[1] + pt2normalized[1] * 13], + }; } render() { if (!this.renderData) return null; + const link = this.props.LinkDocs[0]; const { a, b, pt1norm, pt2norm, aActive, bActive, textX, textY, pt1, pt2 } = this.renderData; - LinkManager.currentLink = this.props.LinkDocs[0]; - const linkRelationship = Field.toString(LinkManager.currentLink?.linkRelationship as any as Field); //get string representing relationship + const linkRelationship = Field.toString(link?.linkRelationship as any as Field); //get string representing relationship const linkRelationshipList = Doc.UserDoc().linkRelationshipList as List<string>; const linkColorList = Doc.UserDoc().linkColorList as List<string>; const linkRelationshipSizes = Doc.UserDoc().linkRelationshipSizes as List<number>; const currRelationshipIndex = linkRelationshipList.indexOf(linkRelationship); + const linkDescription = Field.toString(link.description as any as Field); const linkSize = currRelationshipIndex === -1 || currRelationshipIndex >= linkRelationshipSizes.length ? -1 : linkRelationshipSizes[currRelationshipIndex]; //access stroke color using index of the relationship in the color list (default black) - const stroke = currRelationshipIndex === -1 || currRelationshipIndex >= linkColorList.length ? 'black' : linkColorList[currRelationshipIndex]; + const stroke = currRelationshipIndex === -1 || currRelationshipIndex >= linkColorList.length ? StrCast(link._backgroundColor, 'black') : linkColorList[currRelationshipIndex]; // const hexStroke = this.rgbToHex(stroke) //calculate stroke width/thickness based on the relative importance of the relationshipship (i.e. how many links the relationship has) //thickness varies linearly from 3px to 12px for increasing link count const strokeWidth = linkSize === -1 ? '3px' : Math.floor(2 + 10 * (linkSize / Math.max(...linkRelationshipSizes))) + 'px'; - if (this.props.LinkDocs[0].displayArrow === undefined) { - this.props.LinkDocs[0].displayArrow = false; + if (link.linkDisplayArrow === undefined) { + link.linkDisplayArrow = false; } - return this.props.LinkDocs[0].opacity === 0 || !a.width || !b.width || (!this.props.LinkDocs[0].linkDisplay && !aActive && !bActive) ? null : ( + return link.opacity === 0 || !a.width || !b.width || (!link.linkDisplay && !aActive && !bActive) ? null : ( <> <defs> - <marker id="arrowhead" markerWidth="4" markerHeight="3" refX="0" refY="1.5" orient="auto"> - <polygon points="0 0, 3 1.5, 0 3" fill={Colors.DARK_GRAY} /> + <marker id={`${link[Id] + 'arrowhead'}`} markerWidth="4" markerHeight="3" refX="0" refY="1.5" orient="auto"> + <polygon points="0 0, 3 1.5, 0 3" fill={stroke} /> </marker> + <filter id="outline"> + <feMorphology in="SourceAlpha" result="expanded" operator="dilate" radius="1" /> + <feFlood floodColor={`${Colors.DARK_GRAY}`} /> + <feComposite in2="expanded" operator="in" /> + <feComposite in="SourceGraphic" /> + </filter> + <filter x="0" y="0" width="1" height="1" id={`${link[Id] + 'background'}`}> + <feFlood floodColor={`${StrCast(link._backgroundColor, 'white')}`} result="bg" /> + <feMerge> + <feMergeNode in="bg" /> + <feMergeNode in="SourceGraphic" /> + </feMerge> + </filter> </defs> <path + filter={LinkManager.currentLink === link ? 'url(#outline)' : ''} + fill="pink" + stroke="antiquewhite" + strokeWidth="4" className="collectionfreeformlinkview-linkLine" - style={{ pointerEvents: 'all', opacity: this._opacity, stroke: SelectionManager.SelectedSchemaDoc() === this.props.LinkDocs[0] ? Colors.MEDIUM_BLUE : stroke, strokeWidth }} + style={{ pointerEvents: 'all', opacity: this._opacity, stroke, strokeWidth }} onClick={this.onClickLine} 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]}`} - markerEnd={this.props.LinkDocs[0].displayArrow ? 'url(#arrowhead)' : ''} + markerEnd={link.linkDisplayArrow ? `url(#${link[Id] + 'arrowhead'})` : ''} /> - {textX === undefined ? null : ( - <text className="collectionfreeformlinkview-linkText" x={textX} y={textY} onPointerDown={this.pointerDown}> - {Field.toString(this.props.LinkDocs[0].description as any as Field)} + {textX === undefined || !linkDescription ? null : ( + <text filter={`url(#${link[Id] + 'background'})`} className="collectionfreeformlinkview-linkText" x={textX} y={textY} onPointerDown={this.pointerDown}> + <tspan> </tspan> + <tspan dy="2">{linkDescription}</tspan> + <tspan dy="2"> </tspan> </text> )} </> diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx index b8344dc0c..420e6a318 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx @@ -2,13 +2,13 @@ import { computed } from 'mobx'; import { observer } from 'mobx-react'; import { Id } from '../../../../fields/FieldSymbols'; import { DocumentManager } from '../../../util/DocumentManager'; +import { LightboxView } from '../../LightboxView'; import './CollectionFreeFormLinksView.scss'; import { CollectionFreeFormLinkView } from './CollectionFreeFormLinkView'; import React = require('react'); -import { LightboxView } from '../../LightboxView'; @observer -export class CollectionFreeFormLinksView extends React.Component<React.PropsWithChildren<{}>> { +export class CollectionFreeFormLinksView extends React.Component { @computed get uniqueConnections() { return Array.from(new Set(DocumentManager.Instance.LinkedDocumentViews)) .filter(c => !LightboxView.LightboxDoc || (LightboxView.IsLightboxDocView(c.a.docViewPath) && LightboxView.IsLightboxDocView(c.b.docViewPath))) @@ -19,7 +19,6 @@ export class CollectionFreeFormLinksView extends React.Component<React.PropsWith return ( <div className="collectionfreeformlinksview-container"> <svg className="collectionfreeformlinksview-svgCanvas">{this.uniqueConnections}</svg> - {this.props.children} </div> ); } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index d80fcdfc3..7a7ae3f40 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -96,37 +96,6 @@ border-color: #69a5db; } -.progressivizeButton { - position: absolute; - display: grid; - grid-template-columns: auto 20px auto; - transform: translate(-105%, 0); - align-items: center; - border: black solid 1px; - border-radius: 3px; - justify-content: center; - width: 40; - z-index: 30000; - height: 20; - overflow: hidden; - background-color: #d5dce2; - transition: all 1s; - - .progressivizeButton-prev:hover { - color: #5a9edd; - } - - .progressivizeButton-frame { - justify-self: center; - text-align: center; - width: 15px; - } - - .progressivizeButton-next:hover { - color: #5a9edd; - } -} - .resizable { background: rgba(0, 0, 0, 0.2); width: 100px; @@ -178,25 +147,6 @@ } } -.progressivizeMove-frame { - width: 20px; - border-radius: 2px; - z-index: 100000; - color: white; - text-align: center; - background-color: #5a9edd; - transform: translate(-110%, 110%); -} - -.progressivizeButton:hover { - box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.5); - - .progressivizeButton-frame { - background-color: #5a9edd; - color: white; - } -} - .collectionFreeform-customText { position: absolute; text-align: center; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 2f246e74f..dc0eb69f3 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,4 +1,5 @@ import { Bezier } from 'bezier-js'; +import { Colors } from 'browndash-components'; import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { computedFn } from 'mobx-utils'; @@ -37,7 +38,7 @@ import { GestureOverlay } from '../../GestureOverlay'; import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, InkingStroke, SetActiveInkColor, SetActiveInkWidth } from '../../InkingStroke'; import { LightboxView } from '../../LightboxView'; import { CollectionFreeFormDocumentView } from '../../nodes/CollectionFreeFormDocumentView'; -import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment, ViewSpecPrefix } from '../../nodes/DocumentView'; +import { DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere, ViewAdjustment, ViewSpecPrefix } from '../../nodes/DocumentView'; import { FieldViewProps } from '../../nodes/FieldView'; import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox'; import { PresBox } from '../../nodes/trails/PresBox'; @@ -47,7 +48,7 @@ import { StyleProp } from '../../StyleProvider'; import { CollectionSubView } from '../CollectionSubView'; import { TreeViewType } from '../CollectionTreeView'; import { TabDocView } from '../TabDocView'; -import { computePivotLayout, computerPassLayout, computerStarburstLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult } from './CollectionFreeFormLayoutEngines'; +import { computePassLayout, computePivotLayout, computeStarburstLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult } from './CollectionFreeFormLayoutEngines'; import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCursors'; import './CollectionFreeFormView.scss'; import { MarqueeView } from './MarqueeView'; @@ -71,6 +72,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return 'CollectionFreeFormView(' + this.props.Document.title?.toString() + ')'; } // this makes mobx trace() statements more descriptive + @observable + public static ShowPresPaths = false; + private _lastNudge: any; private _lastX: number = 0; private _lastY: number = 0; @@ -105,7 +109,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } @observable.shallow _layoutElements: ViewDefResult[] = []; // shallow because some layout items (eg pivot labels) are just generated 'divs' and can't be frozen as observables - @observable _viewTransition: number = 0; // sets the pan/zoom transform ease time- used by nudge(), focus() etc to smoothly zoom/pan. set to 0 to use document's transition time or default of 0 + @observable _panZoomTransition: number = 0; // sets the pan/zoom transform ease time- used by nudge(), focus() etc to smoothly zoom/pan. set to 0 to use document's transition time or default of 0 @observable _hLines: number[] | undefined; @observable _vLines: number[] | undefined; @observable _firstRender = true; // this turns off rendering of the collection's content so that there's instant feedback when a tab is switched of what content will be shown. @@ -118,6 +122,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @observable ChildDrag: DocumentView | undefined; // child document view being dragged. needed to update drop areas of groups when a group item is dragged. @observable _brushedView = { width: 0, height: 0, panX: 0, panY: 0, opacity: 0 }; // highlighted region of freeform canvas used by presentations to indicate a region + constructor(props: any) { + super(props); + this.props.setContentView?.(this); + } + @computed get views() { const viewsMask = this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z && ele.inkMask !== -1 && ele.inkMask !== undefined).map(ele => ele.ele); const renderableEles = this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z && (ele.inkMask === -1 || ele.inkMask === undefined)).map(ele => ele.ele); @@ -173,6 +182,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return this.getContainerTransform().translate(-this.cachedCenteringShiftX, -this.cachedCenteringShiftY).transform(this.cachedGetLocalTransform); } + _keyTimer: NodeJS.Timeout | undefined; changeKeyFrame = (back = false) => { const currentFrame = Cast(this.Document._currentFrame, 'number', null); if (currentFrame === undefined) { @@ -180,14 +190,17 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0); } if (back) { - CollectionFreeFormDocumentView.gotoKeyframe(this.childDocs.slice()); + this._keyTimer = CollectionFreeFormDocumentView.gotoKeyframe(this._keyTimer, this.childDocs.slice()); this.Document._currentFrame = Math.max(0, (currentFrame || 0) - 1); } else { - CollectionFreeFormDocumentView.updateKeyframe(this.childDocs, currentFrame || 0); + this._keyTimer = CollectionFreeFormDocumentView.updateKeyframe(this._keyTimer, 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)); } }; + @observable _keyframeEditing = false; + @action setKeyFrameEditing = (set: boolean) => (this._keyframeEditing = set); + getKeyFrameEditing = () => this._keyframeEditing; onBrowseClickHandler = () => this.props.onBrowseClick?.() || ScriptCast(this.layoutDoc.onBrowseClick); onChildClickHandler = () => this.props.childClickScript || ScriptCast(this.Document.onChildClick); onChildDoubleClickHandler = () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); @@ -709,6 +722,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @action pan = (e: PointerEvent | React.Touch | { clientX: number; clientY: number }): void => { + this.props.DocumentView?.().clearViewTransition(); const [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); this.setPan(NumCast(this.Document._panX) - dx, NumCast(this.Document._panY) - dy, 0, true); this._lastX = e.clientX; @@ -792,9 +806,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection p2: { x: currPointInkSpace.X, y: currPointInkSpace.Y }, }); const intersects = Array.from(new Set(rawIntersects as (number | string)[])); // convert to more manageable union array type - if (intersects.length) { - console.log(); - } // return tuples of the inkingStroke intersected, and the t value of the intersection intersections.push(...intersects.map(t => ({ inkView, t: +t + Math.floor(i / 4) }))); // convert string t's to numbers and add start of curve segment to convert from local t value to t value along complete curve } @@ -1008,7 +1019,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection if (this.layoutDoc._Transform || DocListCast(Doc.MyOverlayDocs?.data).includes(this.props.Document) || this.props.Document.treeViewOutlineMode === TreeViewType.outline) return; e.stopPropagation(); e.preventDefault(); - switch (Doc.UserDoc().freeformScrollMode) { + switch (!e.ctrlKey ? Doc.UserDoc().freeformScrollMode : freeformScrollMode.Pan) { case freeformScrollMode.Pan: // if shift is selected then zoom if (e.ctrlKey) { @@ -1021,8 +1032,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } else if (!e.ctrlKey && this.props.Document.scrollHeight !== undefined) { // things that can scroll vertically should do that instead of zooming } else if (this.props.isContentActive(true) && !this.Document._isGroup) { - const dx = e.deltaX; - const dy = e.deltaY; + const dx = -e.deltaX; + const dy = -e.deltaY; if (e.shiftKey) { !this.props.isAnnotationOverlayScrollable && this.scrollPan({ deltaX: dy, deltaY: 0 }); } else { @@ -1076,7 +1087,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } } if (!this.layoutDoc._lockedTransform || LightboxView.LightboxDoc || DocListCast(Doc.MyOverlayDocs?.data).includes(this.Document)) { - this._viewTransition = panTime; + this._panZoomTransition = panTime; const scale = this.getLocalTransform().inverse().Scale; const minScale = NumCast(this.rootDoc._viewScaleMin, 1); const minPanX = NumCast(this.rootDoc._panXMin, 0); @@ -1095,7 +1106,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this.setPan(NumCast(this.layoutDoc._panX) + ((this.props.PanelWidth() / 2) * x) / this.zoomScaling(), NumCast(this.layoutDoc._panY) + ((this.props.PanelHeight() / 2) * -y) / this.zoomScaling(), nudgeTime, true); this._lastNudge && clearTimeout(this._lastNudge); this._lastNudge = setTimeout( - action(() => (this._viewTransition = 0)), + action(() => (this._panZoomTransition = 0)), nudgeTime ); return true; @@ -1124,7 +1135,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @action zoomSmoothlyAboutPt(docpt: number[], scale: number, transitionTime = 500) { if (this.Document._isGroup) return; - this._viewTransition = transitionTime; + this._panZoomTransition = transitionTime; const screenXY = this.getTransform().inverse().transformPoint(docpt[0], docpt[1]); this.layoutDoc[this.scaleFieldKey] = scale; const newScreenXY = this.getTransform().inverse().transformPoint(docpt[0], docpt[1]); @@ -1132,9 +1143,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const newpan = this.getTransform().transformDirection(scrDelta.x, scrDelta.y); this.layoutDoc._panX = NumCast(this.layoutDoc._panX) - newpan[0]; this.layoutDoc._panY = NumCast(this.layoutDoc._panY) - newpan[1]; - return new Promise<number>(res => setTimeout(() => res(runInAction(() => (this._viewTransition = 0))), this._viewTransition)); // set transition to be smooth, then reset + return new Promise<number>(res => setTimeout(() => res(runInAction(() => (this._panZoomTransition = 0))), this._panZoomTransition)); // set transition to be smooth, then reset } + _focusCount = 0; focusDocument = (doc: Doc, options: DocFocusOptions) => { const state = HistoryUtil.getState(); @@ -1154,14 +1166,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection // if (SelectionManager.Views().length !== 1 || SelectionManager.Views()[0].Document !== doc) { // SelectionManager.DeselectAll(); // } - if (this.props.Document.scrollHeight || this.props.Document.scrollTop !== undefined || this.props.Document.currentTimecode !== undefined) { + if (this.props.Document.scrollHeight || this.props.Document.scrollTop !== undefined || this.props.Document.currentTimecode !== undefined || doc.type === DocumentType.MARKER) { this.props.focus(doc, options); } else { const xfToCollection = options?.docTransform ?? Transform.Identity(); - const savedState = { panX: NumCast(this.Document._panX), panY: NumCast(this.Document._panY), scale: options?.willZoom ? this.Document[this.scaleFieldKey] : undefined }; + const savedState = { panX: NumCast(this.Document._panX), panY: NumCast(this.Document._panY), scale: options?.willPanZoom ? this.Document[this.scaleFieldKey] : undefined }; const newState = HistoryUtil.getState(); const cantTransform = (this.rootDoc._isGroup || this.layoutDoc._lockedTransform) && !LightboxView.LightboxDoc; - const { panX, panY, scale } = cantTransform ? savedState : this.calculatePanIntoView(doc, xfToCollection, options?.willZoom ? options?.scale || 0.75 : undefined); + const { panX, panY, scale } = cantTransform || (!options.willPan && !options.willPanZoom) ? savedState : this.calculatePanIntoView(doc, xfToCollection, options?.willPanZoom ? options?.zoomScale || 0.75 : undefined); if (!cantTransform) { // only pan and zoom to focus on a document if the document is not an annotation in an annotation overlay collection newState.initializers![this.Document[Id]] = { panX, panY }; @@ -1169,27 +1181,28 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } // focus on the document in the collection const didMove = !cantTransform && !doc.z && (panX !== savedState.panX || panY !== savedState.panY || scale !== savedState.scale); - const focusSpeed = options?.instant ? 0 : didMove ? NumCast(doc.focusSpeed, 500) : 0; + const focusSpeed = options?.instant ? 0 : didMove ? options.zoomTime ?? 500 : 0; // glr: freeform transform speed can be set by adjusting presTransition field - needs a way of knowing when presentation is not active... if (didMove) { - scale && (this.Document[this.scaleFieldKey] = scale); + options.zoomScale && scale && (this.Document[this.scaleFieldKey] = scale); this.setPan(panX, panY, focusSpeed, 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 } + const focusCount = ++this._focusCount; const startTime = Date.now(); // focus on this collection within its parent view. the parent view after focusing determines whether to reset the view change within the collection const endFocus = async (moved: boolean) => { doc.hidden && Doc.UnHighlightDoc(doc); const resetView = options?.afterFocus ? await options?.afterFocus(moved) : ViewAdjustment.doNothing; if (resetView) { - const restoreState = (!LightboxView.LightboxDoc || LightboxView.LightboxDoc === this.props.Document) && savedState; + const restoreState = savedState; if (typeof restoreState !== 'boolean') { this.Document._panX = restoreState.panX; this.Document._panY = restoreState.panY; this.Document[this.scaleFieldKey] = restoreState.scale; } } - runInAction(() => (this._viewTransition = 0)); + this._focusCount === focusCount && didMove && runInAction(() => (this._panZoomTransition = 0)); return resetView; }; const xf = !cantTransform @@ -1229,6 +1242,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const cx = NumCast(this.layoutDoc._panX) + (this.props.isAnnotationOverlay ? pw / 2 : 0); const cy = NumCast(this.layoutDoc._panY) + (this.props.isAnnotationOverlay ? ph / 2 : 0); const screen = { left: cx - pw / 2, right: cx + pw / 2, top: cy - ph / 2, bot: cy + ph / 2 }; + const maxYShift = Math.max(0, screen.bot - screen.top - (bounds.bot - bounds.top)); + const phborder = bounds.top < screen.top || bounds.bot > screen.bot ? Math.min(ph / 10, maxYShift / 2) : 0; if (screen.right - screen.left < bounds.right - bounds.left || screen.bot - screen.top < bounds.bot - bounds.top) { return { panX: (bounds.left + bounds.right) / 2, @@ -1238,7 +1253,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } return { panX: (this.props.isAnnotationOverlay ? NumCast(this.layoutDoc._panX) : cx) + Math.min(0, bounds.left - pw / 10 - screen.left) + Math.max(0, bounds.right + pw / 10 - screen.right), - panY: (this.props.isAnnotationOverlay ? NumCast(this.layoutDoc._panY) : cy) + Math.min(0, bounds.top - ph / 10 - screen.top) + Math.max(0, bounds.bot + ph / 10 - screen.bot), + panY: (this.props.isAnnotationOverlay ? NumCast(this.layoutDoc._panY) : cy) + Math.min(0, bounds.top - phborder - screen.top) + Math.max(0, bounds.bot + phborder - screen.bot), }; }; @@ -1267,7 +1282,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; pointerEvents = () => { const engine = this.props.layoutEngine?.() || StrCast(this.props.Document._layoutEngine); - const pointerEvents = this.props.isContentActive() === false ? 'none' : this.props.childPointerEvents ?? (this.props.viewDefDivClick || (engine === 'pass' && !this.props.isSelected(true)) ? 'none' : this.props.pointerEvents?.()); + const pointerEvents = + this.props.isContentActive() === false ? 'none' : this.props.childPointerEvents ?? (this.props.viewDefDivClick || (engine === computePassLayout.name && !this.props.isSelected(true)) ? 'none' : this.props.pointerEvents?.()); return pointerEvents; }; getChildDocView(entry: PoolData) { @@ -1309,6 +1325,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged} docViewPath={this.props.docViewPath} styleProvider={this.getClusterColor} + canEmbedOnDrag={this.props.childCanEmbedOnDrag} dataProvider={this.childDataProvider} sizeProvider={this.childSizeProvider} dropAction={StrCast(this.props.Document.childDropAction) as dropActionType} @@ -1322,18 +1339,26 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection /> ); } - addDocTab = action((doc: Doc, where: string) => { - if (where === 'inParent') { - (doc instanceof Doc ? [doc] : doc).forEach(doc => { - const pt = this.getTransform().transformPoint(NumCast(doc.x), NumCast(doc.y)); - doc.x = pt[0]; - doc.y = pt[1]; - }); - return this.props.addDocument?.(doc) || false; - } - if (where === 'inPlace' && this.layoutDoc.isInPlaceContainer) { - this.dataDoc[this.props.fieldKey] = doc instanceof Doc ? doc : new List<Doc>(doc as any as Doc[]); - return true; + addDocTab = action((doc: Doc, where: OpenWhere) => { + switch (where) { + case OpenWhere.inParent: + return this.props.addDocument?.(doc) || false; + case OpenWhere.inParentFromScreen: + return ( + this.props.addDocument?.( + (doc instanceof Doc ? [doc] : doc).map(doc => { + const pt = this.getTransform().transformPoint(NumCast(doc.x), NumCast(doc.y)); + doc.x = pt[0]; + doc.y = pt[1]; + return doc; + }) + ) || false + ); + case OpenWhere.inPlace: + if (this.layoutDoc.isInPlaceContainer) { + this.dataDoc[this.props.fieldKey] = new List<Doc>(doc instanceof Doc ? [doc] : doc); + return true; + } } return this.props.addDocTab(doc, where); }); @@ -1351,7 +1376,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection z: Cast(z, 'number'), color: Cast(color, 'string') ? StrCast(color) : this.props.styleProvider?.(childDoc, this.props, StyleProp.Color), backgroundColor: Cast(backgroundColor, 'string') ? StrCast(backgroundColor) : this.props.styleProvider?.(childDoc, this.props, StyleProp.BackgroundColor), - opacity: Cast(opacity, 'number') ?? this.props.styleProvider?.(childDoc, this.props, StyleProp.Opacity), + opacity: this._keyframeEditing ? 1 : Cast(opacity, 'number') ?? this.props.styleProvider?.(childDoc, this.props, StyleProp.Opacity), zIndex: Cast(zIndex, 'number'), width: Cast(childDocLayout._width, 'number'), height: Cast(childDocLayout._height, 'number'), @@ -1451,10 +1476,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const newPool = new Map<string, PoolData>(); // prettier-ignore switch (this.layoutEngine) { - case 'pass': return { newPool, computedElementData: this.doEngineLayout(newPool, computerPassLayout) }; - case 'timeline': return { newPool, computedElementData: this.doEngineLayout(newPool, computeTimelineLayout) }; - case 'pivot': return { newPool, computedElementData: this.doEngineLayout(newPool, computePivotLayout) }; - case 'starburst': return { newPool, computedElementData: this.doEngineLayout(newPool, computerStarburstLayout) }; + case computePassLayout.name : return { newPool, computedElementData: this.doEngineLayout(newPool, computePassLayout) }; + case computeTimelineLayout.name: return { newPool, computedElementData: this.doEngineLayout(newPool, computeTimelineLayout) }; + case computePivotLayout.name: return { newPool, computedElementData: this.doEngineLayout(newPool, computePivotLayout) }; + case computeStarburstLayout.name: return { newPool, computedElementData: this.doEngineLayout(newPool, computeStarburstLayout) }; } return { newPool, computedElementData: this.doFreeformLayout(newPool) }; } @@ -1494,7 +1519,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection elements.push({ ele: this.getChildDocView(entry[1]), bounds: this.childDataProvider(entry[1].pair.layout, entry[1].replica), - inkMask: BoolCast(entry[1].pair.layout.isInkMask) ? NumCast(entry[1].pair.layout.opacity) : -1, + inkMask: BoolCast(entry[1].pair.layout.isInkMask) ? NumCast(entry[1].pair.layout.opacity, 1) : -1, }) ); @@ -1524,11 +1549,28 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return 0; }; + @action + scrollFocus = (anchor: Doc, options: DocFocusOptions) => { + const focusSpeed = options.instant ? 0 : options.zoomTime ?? 500; + return PresBox.restoreTargetDocView( + this.props.DocumentView?.(), // + { pinDocLayout: BoolCast(anchor.presPinDocLayout) }, + anchor, + focusSpeed, + { + pannable: anchor.presPinData ? true : false, + } + ) + ? focusSpeed + : undefined; + }; // sets viewing information for a componentview, typically when following a link. 'preview' tells the view to use the values without writing to the document + getAnchor = () => { if (this.props.Document.annotationOn) { return this.rootDoc; } - const anchor = Docs.Create.TextanchorDocument({ title: 'ViewSpec - ' + StrCast(this.layoutDoc._viewType), annotationOn: this.rootDoc }); + const anchor = Docs.Create.TextanchorDocument({ title: 'ViewSpec - ' + StrCast(this.layoutDoc._viewType), presTransition: 500, annotationOn: this.rootDoc }); + PresBox.pinDocView(anchor, { pinData: { pannable: true } }, this.rootDoc); const proto = Doc.GetProto(anchor); proto[ViewSpecPrefix + '_viewType'] = this.layoutDoc._viewType; proto.docFilters = ObjectField.MakeCopy(this.layoutDoc.docFilters as ObjectField) || new List<string>([]); @@ -1543,7 +1585,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @action componentDidMount() { super.componentDidMount?.(); - this.props.setContentView?.(this); this.props.setBrushViewer?.(this.brushView); setTimeout( action(() => { @@ -1566,12 +1607,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection r: cbounds.r - p[0] + c[0], b: cbounds.b - p[1] + c[1], }; - this.layoutDoc._width = pbounds.r - pbounds.x; - this.layoutDoc._height = pbounds.b - pbounds.y; - this.layoutDoc._panX = (cbounds.r + cbounds.x) / 2; - this.layoutDoc._panY = (cbounds.b + cbounds.y) / 2; - this.layoutDoc.x = pbounds.x; - this.layoutDoc.y = pbounds.y; + if (Number.isFinite(pbounds.r - pbounds.x) && Number.isFinite(pbounds.b - pbounds.y)) { + this.layoutDoc._width = pbounds.r - pbounds.x; + this.layoutDoc._height = pbounds.b - pbounds.y; + this.layoutDoc._panX = (cbounds.r + cbounds.x) / 2; + this.layoutDoc._panY = (cbounds.b + cbounds.y) / 2; + this.layoutDoc.x = pbounds.x; + this.layoutDoc.y = pbounds.y; + } } }, { fireImmediately: true } @@ -1701,7 +1744,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection doc.x = scr?.[0]; doc.y = scr?.[1]; }); - this.props.addDocTab(childDocs as any as Doc, 'inParent'); + this.props.addDocTab(childDocs as any as Doc, OpenWhere.inParentFromScreen); this.props.ContainingCollectionView?.removeDocument(this.props.Document); }; @@ -1839,6 +1882,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection DragManager.SetSnapLines(horizLines, vertLines); }; + incrementalRendering = () => this.childDocs.filter(doc => !this._renderCutoffData.get(doc[Id])).length !== 0; + incrementalRender = action(() => { if (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath())) { const unrendered = this.childDocs.filter(doc => !this._renderCutoffData.get(doc[Id])); @@ -1864,6 +1909,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection ); } + showPresPaths = () => (CollectionFreeFormView.ShowPresPaths ? PresBox.Instance.getPaths(this.rootDoc) : null); + @computed get marqueeView() { TraceMobx(); return ( @@ -1910,10 +1957,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection isAnnotationOverlayScrollable={this.props.isAnnotationOverlayScrollable} transform={this.contentTransform} zoomScaling={this.zoomScaling} - presPaths={BoolCast(this.Document.presPathView)} - progressivize={BoolCast(this.Document.editProgressivize)} + presPaths={this.showPresPaths} presPinView={BoolCast(this.Document.presPinView)} - transition={this._viewTransition ? `transform ${this._viewTransition}ms` : Cast(this.layoutDoc._viewTransition, 'string', null)} + transition={this._panZoomTransition ? `transform ${this._panZoomTransition}ms` : Cast(this.layoutDoc._viewTransition, 'string', null)} viewDefDivClick={this.props.viewDefDivClick}> {this.children} </CollectionFreeFormViewPannableContents> @@ -1985,19 +2031,17 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection {this._firstRender ? this.placeholder : this.marqueeView} {this.props.noOverlay ? null : <CollectionFreeFormOverlayView elements={this.elementFunc} />} - { - // uncomment to show snap lines - <div className="snapLines" style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', pointerEvents: 'none' }}> - <svg style={{ width: '100%', height: '100%' }}> - {this._hLines?.map(l => ( - <line x1="0" y1={l} x2="1000" y2={l} stroke="black" /> - ))} - {this._vLines?.map(l => ( - <line y1="0" x1={l} y2="1000" x2={l} stroke="black" /> - ))} - </svg> - </div> - } + {/* // uncomment to show snap lines */} + <div className="snapLines" style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', pointerEvents: 'none' }}> + <svg style={{ width: '100%', height: '100%' }}> + {this._hLines?.map(l => ( + <line x1="0" y1={l} x2="1000" y2={l} stroke="black" /> + ))} + {this._vLines?.map(l => ( + <line y1="0" x1={l} y2="1000" x2={l} stroke="black" /> + ))} + </svg> + </div> {this.props.Document._isGroup && SnappingManager.GetIsDragging() && this.ChildDrag ? ( <div @@ -2039,8 +2083,7 @@ interface CollectionFreeFormViewPannableContentsProps { viewDefDivClick?: ScriptField; children: () => JSX.Element[]; transition?: string; - presPaths?: boolean; - progressivize?: boolean; + presPaths: () => JSX.Element | null; presPinView?: boolean; isAnnotationOverlay: boolean | undefined; isAnnotationOverlayScrollable: boolean | undefined; @@ -2093,42 +2136,11 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF return true; }; - // scale: NumCast(targetDoc._viewScale), - @computed get zoomProgressivizeContainer() { - const activeItem = PresBox.Instance.activeItem; - // const targetDoc = PresBox.Instance.targetDoc; - if (activeItem && activeItem.presPinView && activeItem.id) { - const left = NumCast(activeItem.presPinViewX); - const top = NumCast(activeItem.presPinViewY); - const width = 100; - const height = 100; - return !this.props.presPinView ? null : ( - <div key="resizable" className="resizable" onPointerDown={this.onPointerDown} style={{ width, height, top, left, position: 'absolute' }}> - <div className="resizers" key={'resizer' + activeItem.id}> - <div className="resizer top-left" onPointerDown={this.onPointerDown} /> - <div className="resizer top-right" onPointerDown={this.onPointerDown} /> - <div className="resizer bottom-left" onPointerDown={this.onPointerDown} /> - <div className="resizer bottom-right" onPointerDown={this.onPointerDown} /> - </div> - </div> - ); - } - } - - @computed get zoomProgressivize() { - return PresBox.Instance?.activeItem?.presPinView && PresBox.Instance.layoutDoc.presStatus === 'edit' ? this.zoomProgressivizeContainer : null; - } - - @computed get progressivize() { - return PresBox.Instance && this.props.progressivize ? PresBox.Instance.progressivizeChildDocs : null; - } - @computed get presPaths() { - const presPaths = 'presPaths' + (this.props.presPaths ? '' : '-hidden'); - return !PresBox.Instance || !this.props.presPaths ? null : ( + return !this.props.presPaths() ? null : ( <> - <div key="presorder">{PresBox.Instance.order}</div> - <svg key="svg" className={presPaths}> + <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" /> @@ -2140,7 +2152,7 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF <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} + {this.props.presPaths()} </svg> </> ); @@ -2180,8 +2192,6 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF /> )} {this.presPaths} - {this.progressivize} - {this.zoomProgressivize} </div> ); } @@ -2250,7 +2260,7 @@ class CollectionFreeFormBackgroundGrid extends React.Component<CollectionFreeFor export function CollectionBrowseClick(dv: DocumentView, clientX: number, clientY: number) { SelectionManager.DeselectAll(); dv.props.focus(dv.props.Document, { - willZoom: true, + willPanZoom: true, afterFocus: async didMove => { if (!didMove) { const selfFfview = dv.ComponentView instanceof CollectionFreeFormView ? dv.ComponentView : undefined; @@ -2270,6 +2280,14 @@ ScriptingGlobals.add(function nextKeyFrame(readOnly: boolean) { ScriptingGlobals.add(function prevKeyFrame(readOnly: boolean) { !readOnly && (SelectionManager.Views()[0].ComponentView as CollectionFreeFormView)?.changeKeyFrame(true); }); +ScriptingGlobals.add(function curKeyFrame(readOnly: boolean) { + const selView = SelectionManager.Views(); + if (readOnly) return selView[0].ComponentView?.getKeyFrameEditing?.() ? Colors.MEDIUM_BLUE : 'transparent'; + runInAction(() => selView[0].ComponentView?.setKeyFrameEditing?.(!selView[0].ComponentView?.getKeyFrameEditing?.())); +}); ScriptingGlobals.add(function pinWithView(readOnly: boolean, pinDocContent: boolean) { - !readOnly && SelectionManager.Views().forEach(view => TabDocView.PinDoc(view.rootDoc, { pinDocContent, pinViewport: MarqueeView.CurViewBounds(view.rootDoc, view.props.PanelWidth(), view.props.PanelHeight()) })); + !readOnly && + SelectionManager.Views().forEach(view => + TabDocView.PinDoc(view.rootDoc, { currentFrame: Cast(view.rootDoc.currentFrame, 'number', null), pinDocContent, pinViewport: MarqueeView.CurViewBounds(view.rootDoc, view.props.PanelWidth(), view.props.PanelHeight()) }) + ); }); diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index a020b67cd..dcbadc5f3 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -17,8 +17,8 @@ import { SelectionManager } from '../../../util/SelectionManager'; import { Transform } from '../../../util/Transform'; import { undoBatch, UndoManager } from '../../../util/UndoManager'; import { ContextMenu } from '../../ContextMenu'; +import { OpenWhere } from '../../nodes/DocumentView'; import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox'; -import { PresBox } from '../../nodes/trails/PresBox'; import { VideoBox } from '../../nodes/VideoBox'; import { pasteImageBitmap } from '../../nodes/WebBoxRenderer'; import { PreviewCursor } from '../../PreviewCursor'; @@ -120,7 +120,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque const cm = ContextMenu.Instance; const [x, y] = this.Transform.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: 400, x, y, _height: 512, _nativeWidth: 850, title: 'bing', useCors: true }), 'add:right')); + cm.setDefaultItem('?', (str: string) => this.props.addDocTab(Docs.Create.WebDocument(`https://bing.com/search?q=${str}`, { _width: 400, x, y, _height: 512, _nativeWidth: 850, title: 'bing', useCors: true }), OpenWhere.addRight)); cm.displayMenu(this._downX, this._downY, undefined, true); e.stopPropagation(); @@ -243,7 +243,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque if (!(e.nativeEvent as any).marqueeHit) { (e.nativeEvent as any).marqueeHit = true; // allow marquee if right click OR alt+left click OR in adding presentation slide & left key drag mode - if (e.button === 2 || (e.button === 0 && e.altKey) || (PresBox.startMarquee && e.button === 0)) { + if (e.button === 2 || (e.button === 0 && e.altKey)) { // if (e.altKey || (MarqueeView.DragMarquee && this.props.active(true))) { this.setPreviewCursor(e.clientX, e.clientY, true, false); // (!e.altKey) && e.stopPropagation(); // bcz: removed so that you can alt-click on button in a collection to switch link following behaviors. @@ -272,9 +272,6 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this.cleanupInteractions(true); // stop listening for events if another lower-level handle (e.g. another Marquee) has stopPropagated this } e.altKey && e.preventDefault(); - if (PresBox.startMarquee) { - e.stopPropagation(); - } }; @action @@ -295,11 +292,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque document.removeEventListener('pointerdown', hideMarquee, true); document.removeEventListener('wheel', hideMarquee, true); }; - if (PresBox.startMarquee) { - this.pinWithView(); - PresBox.startMarquee = false; - } - if (!this._commandExecuted && Math.abs(this.Bounds.height * this.Bounds.width) > 100 && !PresBox.startMarquee) { + if (!this._commandExecuted && Math.abs(this.Bounds.height * this.Bounds.width) > 100) { MarqueeOptionsMenu.Instance.createCollection = this.collection; MarqueeOptionsMenu.Instance.delete = this.delete; MarqueeOptionsMenu.Instance.summarize = this.summary; @@ -393,7 +386,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque : ((doc: Doc) => { Doc.GetProto(doc).data = new List<Doc>(selected); Doc.GetProto(doc).title = makeGroup ? 'grouping' : 'nested freeform'; - !this.props.isAnnotationOverlay && Doc.AddDocToList(Doc.MyFileOrphans, undefined, Doc.GetProto(doc)); + !this.props.isAnnotationOverlay && Doc.AddFileOrphan(Doc.GetProto(doc)); doc._panX = doc._panY = 0; return doc; })(Doc.MakeCopy(Doc.UserDoc().emptyCollection as Doc, true)); @@ -540,7 +533,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque d.y = NumCast(d.y) - this.Bounds.top; return d; }); - const summary = Docs.Create.TextDocument('', { backgroundColor: '#e2ad32', x: this.Bounds.left, y: this.Bounds.top, isPushpin: true, _width: 200, _height: 200, _fitContentsToBox: true, _showSidebar: true, title: 'overview' }); + const summary = Docs.Create.TextDocument('', { backgroundColor: '#e2ad32', x: this.Bounds.left, y: this.Bounds.top, followLinkToggle: true, _width: 200, _height: 200, _fitContentsToBox: true, _showSidebar: true, title: 'overview' }); const portal = Docs.Create.FreeformDocument(selected, { x: this.Bounds.left + 200, y: this.Bounds.top, isGroup: true, backgroundColor: 'transparent' }); DocUtils.MakeLink({ doc: summary }, { doc: portal }, 'summary of:summarized by', ''); @@ -685,7 +678,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque className="marqueeView" style={{ overflow: StrCast(this.props.Document._overflow), - cursor: [InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool) || this._visible || PresBox.startMarquee ? 'crosshair' : 'pointer', + cursor: [InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool) || this._visible ? 'crosshair' : 'pointer', }} onDragOver={e => e.preventDefault()} onScroll={e => (e.currentTarget.scrollTop = e.currentTarget.scrollLeft = 0)} diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.scss b/src/client/views/collections/collectionLinear/CollectionLinearView.scss index e8df192cf..521bcda1e 100644 --- a/src/client/views/collections/collectionLinear/CollectionLinearView.scss +++ b/src/client/views/collections/collectionLinear/CollectionLinearView.scss @@ -1,6 +1,10 @@ -@import "../../global/globalCssVariables"; -@import "../../_nodeModuleOverrides"; +@import '../../global/globalCssVariables'; +@import '../../_nodeModuleOverrides'; +.collectionLinearView-label { + color: black; + background-color: $light-gray; +} .collectionLinearView-outer { overflow: visible; height: 100%; diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx index 92f6bbb64..a2330c6b2 100644 --- a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx +++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx @@ -1,15 +1,15 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Tooltip } from '@material-ui/core'; +import { StylesProvider, 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, Opt, WidthSym } from '../../../../fields/Doc'; import { Id } from '../../../../fields/FieldSymbols'; -import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; -import { emptyFunction, returnEmptyDoclist, returnTrue, Utils } from '../../../../Utils'; +import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; +import { emptyFunction, returnEmptyDoclist, returnTrue, StopEvent, Utils } from '../../../../Utils'; import { CollectionViewType } from '../../../documents/DocumentTypes'; import { DocumentManager } from '../../../util/DocumentManager'; -import { DragManager } from '../../../util/DragManager'; +import { DragManager, dropActionType } from '../../../util/DragManager'; import { Transform } from '../../../util/Transform'; import { Colors, Shadows } from '../../global/globalEnums'; import { DocumentLinksButton } from '../../nodes/DocumentLinksButton'; @@ -119,7 +119,49 @@ export class CollectionLinearView extends CollectionSubView() { e.preventDefault(); }; + getLinkUI = () => { + return !DocumentLinksButton.StartLink ? null : ( + <span className="bottomPopup-background" style={{ pointerEvents: 'all' }} onPointerDown={e => e.stopPropagation()}> + <span className="bottomPopup-text"> + Creating link from:{' '} + <b> + {(DocumentLinksButton.AnnotationId ? 'Annotation in ' : ' ') + + (StrCast(DocumentLinksButton.StartLink.title).length < 51 ? DocumentLinksButton.StartLink.title : StrCast(DocumentLinksButton.StartLink.title).slice(0, 50) + '...')} + </b> + </span> + + <Tooltip title={<div className="dash-tooltip">{'Toggle description pop-up'} </div>} placement="top"> + <span className="bottomPopup-descriptions" onClick={this.changeDescriptionSetting}> + Labels: {LinkDescriptionPopup.showDescriptions ? LinkDescriptionPopup.showDescriptions : 'ON'} + </span> + </Tooltip> + + <Tooltip title={<div className="dash-tooltip">Exit linking mode</div>} placement="top"> + <span className="bottomPopup-exit" onClick={this.exitLongLinks}> + Stop + </span> + </Tooltip> + </span> + ); + }; + getCurrentlyPlayingUI = () => { + return !CollectionStackedTimeline.CurrentlyPlaying?.length ? null : ( + <span className="bottomPopup-background"> + <span className="bottomPopup-text"> + Currently playing: + {CollectionStackedTimeline.CurrentlyPlaying.map((clip, i) => ( + <span className="audio-title" onPointerDown={() => DocumentManager.Instance.jumpToDocument(clip, { willPanZoom: true }, undefined, [])}> + {clip.title + (i === CollectionStackedTimeline.CurrentlyPlaying.length - 1 ? '' : ',')} + </span> + ))} + </span> + </span> + ); + }; getDisplayDoc = (doc: Doc, preview: boolean = false) => { + if (doc.icon === 'linkui') return this.getLinkUI(); + if (doc.icon === 'currentlyplayingui') return this.getCurrentlyPlayingUI(); + const nested = doc._viewType === CollectionViewType.Linear; const hidden = doc.hidden === true; @@ -148,6 +190,7 @@ export class CollectionLinearView extends CollectionSubView() { moveDocument={this.props.moveDocument} addDocTab={this.props.addDocTab} pinToPres={emptyFunction} + dropAction={StrCast(this.layoutDoc.childDropAction) as dropActionType} rootSelected={this.props.isSelected} removeDocument={this.props.removeDocument} ScreenToLocalTransform={docXf} @@ -172,40 +215,32 @@ export class CollectionLinearView extends CollectionSubView() { }; render() { - const guid = Utils.GenerateGuid(); // Generate a unique ID to use as the label - const flexDir: any = StrCast(this.Document.flexDirection); // Specify direction of linear view content - const flexGap: number = NumCast(this.Document.flexGap); // Specify the gap between linear view content - const expandable: boolean = BoolCast(this.props.Document.linearViewExpandable); // Specify whether it is expandable or not - const floating: boolean = BoolCast(this.props.Document.linearViewFloating); // Specify whether it is expandable or not + const flexDir = StrCast(this.Document.flexDirection); // Specify direction of linear view content + const flexGap = NumCast(this.Document.flexGap); // Specify the gap between linear view content + const isExpanded = BoolCast(this.layoutDoc.linearViewIsExpanded); - const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); - const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); - const icon: string = StrCast(this.props.Document.icon); // Menu opener toggle const menuOpener = ( <label - htmlFor={`${guid}`} - style={{ - boxShadow: floating ? Shadows.STANDARD_SHADOW : undefined, - color: BoolCast(this.layoutDoc.linearViewIsExpanded) ? undefined : Colors.BLACK, - backgroundColor: backgroundColor === color ? 'black' : BoolCast(this.layoutDoc.linearViewIsExpanded) ? undefined : Colors.LIGHT_GRAY, - }} - onPointerDown={e => e.stopPropagation()}> - <div className="collectionLinearView-menuOpener">{BoolCast(this.layoutDoc.linearViewIsExpanded) ? icon ? icon : <FontAwesomeIcon icon={'minus'} /> : icon ? icon : <FontAwesomeIcon icon={'plus'} />}</div> + className={`collectionlinearView-label${isExpanded ? '-expanded' : ''}`} + htmlFor={this.Document[Id] + '-input'} + style={{ boxShadow: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BoxShadow) }} + onPointerDown={StopEvent}> + <div className="collectionLinearView-menuOpener">{Cast(this.props.Document.icon, 'string', null) ?? <FontAwesomeIcon icon={isExpanded ? 'minus' : 'plus'} />}</div> </label> ); return ( - <div className={`collectionLinearView-outer ${this.layoutDoc.linearViewSubMenu}`} style={{ backgroundColor: BoolCast(this.layoutDoc.linearViewIsExpanded) ? undefined : 'transparent' }}> + <div className={`collectionLinearView-outer ${this.layoutDoc.linearViewSubMenu}`} style={{ backgroundColor: this.layoutDoc.linearViewIsExpanded ? undefined : 'transparent' }}> <div className="collectionLinearView" ref={this.createDashEventsTarget} onContextMenu={this.myContextMenu}> - {!expandable ? null : ( - <Tooltip title={<div className="dash-tooltip">{BoolCast(this.props.Document.linearViewIsExpanded) ? 'Close' : 'Open'}</div>} placement="top"> + {!this.props.Document.linearViewExpandable ? null : ( + <Tooltip title={<div className="dash-tooltip">{isExpanded ? 'Close' : 'Open'}</div>} placement="top"> {menuOpener} </Tooltip> )} <input - id={`${guid}`} + id={this.Document[Id] + '-input'} type="checkbox" - checked={BoolCast(this.props.Document.linearViewIsExpanded)} + checked={isExpanded} ref={this.addMenuToggle} onChange={action(e => { ScriptCast(this.Document.onClick)?.script.run({ @@ -216,7 +251,7 @@ export class CollectionLinearView extends CollectionSubView() { thisContainer: this.props.ContainingCollectionDoc, documentView: this.props.docViewPath().lastElement(), }); - this.props.Document.linearViewIsExpanded = this.addMenuToggle.current!.checked; + this.layoutDoc.linearViewIsExpanded = this.addMenuToggle.current!.checked; })} /> @@ -224,58 +259,11 @@ export class CollectionLinearView extends CollectionSubView() { className="collectionLinearView-content" style={{ height: this.dimension(), - flexDirection: flexDir, + flexDirection: flexDir as any, gap: flexGap, }}> {this.childLayoutPairs.map(pair => this.getDisplayDoc(pair.layout))} </div> - {!DocumentLinksButton.StartLink || this.layoutDoc !== Doc.MyDockedBtns ? null : ( - <span className="bottomPopup-background" style={{ pointerEvents: 'all' }} onPointerDown={e => e.stopPropagation()}> - <span className="bottomPopup-text"> - Creating link from:{' '} - <b> - {(DocumentLinksButton.AnnotationId ? 'Annotation in ' : ' ') + - (StrCast(DocumentLinksButton.StartLink.title).length < 51 ? DocumentLinksButton.StartLink.title : StrCast(DocumentLinksButton.StartLink.title).slice(0, 50) + '...')} - </b> - </span> - - <Tooltip - title={ - <> - <div className="dash-tooltip">{'Toggle description pop-up'} </div> - </> - } - placement="top"> - <span className="bottomPopup-descriptions" onClick={this.changeDescriptionSetting}> - Labels: {LinkDescriptionPopup.showDescriptions ? LinkDescriptionPopup.showDescriptions : 'ON'} - </span> - </Tooltip> - - <Tooltip - title={ - <> - <div className="dash-tooltip">Exit linking mode</div> - </> - } - placement="top"> - <span className="bottomPopup-exit" onClick={this.exitLongLinks}> - Stop - </span> - </Tooltip> - </span> - )} - {!CollectionStackedTimeline.CurrentlyPlaying || !CollectionStackedTimeline.CurrentlyPlaying.length || this.layoutDoc !== Doc.MyDockedBtns ? null : ( - <span className="bottomPopup-background"> - <span className="bottomPopup-text"> - Currently playing: - {CollectionStackedTimeline.CurrentlyPlaying.map((clip, i) => ( - <span className="audio-title" onPointerDown={() => DocumentManager.Instance.jumpToDocument(clip, true, undefined, [])}> - {clip.title + (i === CollectionStackedTimeline.CurrentlyPlaying.length - 1 ? '' : ',')} - </span> - ))} - </span> - </span> - )} </div> </div> ); diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx index 465dbfe6d..b88da5191 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx @@ -9,7 +9,7 @@ import { DragManager, dropActionType } from '../../../util/DragManager'; import { SnappingManager } from '../../../util/SnappingManager'; import { Transform } from '../../../util/Transform'; import { undoBatch } from '../../../util/UndoManager'; -import { DocumentView } from '../../nodes/DocumentView'; +import { DocFocusOptions, DocumentView } from '../../nodes/DocumentView'; import { CollectionSubView } from '../CollectionSubView'; import './CollectionMulticolumnView.scss'; import ResizeBar from './MulticolumnResizer'; @@ -234,16 +234,9 @@ export class CollectionMulticolumnView extends CollectionSubView() { onChildClickHandler = () => ScriptCast(this.Document.onChildClick); onChildDoubleClickHandler = () => ScriptCast(this.Document.onChildDoubleClick); - addDocTab = (doc: Doc, where: string) => { - if (where === 'inPlace' && this.layoutDoc.isInPlaceContainer) { - this.dataDoc[this.props.fieldKey] = new List<Doc>([doc]); - return true; - } - return this.props.addDocTab(doc, where); - }; - isContentActive = () => this.props.isSelected() || this.props.isContentActive(); - isChildContentActive = () => - ((this.props.childDocumentsActive?.() || this.Document._childDocumentsActive) && this.props.isDocumentActive?.() && SnappingManager.GetIsDragging()) || this.props.isSelected() || this.props.isAnyChildContentActive() ? true : false; + focusDocument = (doc: Doc, options: DocFocusOptions) => this.props.focus(this.rootDoc, options); + isContentActive = () => this.props.isSelected() || this.props.isContentActive() || this.props.isAnyChildContentActive(); + isChildContentActive = () => (((this.props.childDocumentsActive?.() || this.Document._childDocumentsActive) && this.props.isDocumentActive?.() && SnappingManager.GetIsDragging()) || this.isContentActive() ? true : false); getDisplayDoc = (layout: Doc, dxf: () => Transform, width: () => number, height: () => number) => { return ( <DocumentView @@ -267,7 +260,7 @@ export class CollectionMulticolumnView extends CollectionSubView() { hideResizeHandles={this.props.childHideResizeHandles?.()} hideDecorationTitle={this.props.childHideDecorationTitle?.()} fitContentsToBox={this.props.fitContentsToBox} - focus={this.props.focus} + focus={this.focusDocument} docFilters={this.childDocFilters} docRangeFilters={this.childDocRangeFilters} searchFilterDocs={this.searchFilterDocs} @@ -278,7 +271,7 @@ export class CollectionMulticolumnView extends CollectionSubView() { moveDocument={this.props.moveDocument} removeDocument={this.props.removeDocument} whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged} - addDocTab={this.addDocTab} + addDocTab={this.props.addDocTab} pinToPres={this.props.pinToPres} bringToFront={returnFalse} /> diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx index f8de4e5de..407deaabd 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx @@ -9,7 +9,7 @@ import { DragManager, dropActionType } from '../../../util/DragManager'; import { SnappingManager } from '../../../util/SnappingManager'; import { Transform } from '../../../util/Transform'; import { undoBatch } from '../../../util/UndoManager'; -import { DocumentView } from '../../nodes/DocumentView'; +import { DocFocusOptions, DocumentView } from '../../nodes/DocumentView'; import { CollectionSubView } from '../CollectionSubView'; import './CollectionMultirowView.scss'; import HeightLabel from './MultirowHeightLabel'; @@ -234,16 +234,9 @@ export class CollectionMultirowView extends CollectionSubView() { onChildClickHandler = () => ScriptCast(this.Document.onChildClick); onChildDoubleClickHandler = () => ScriptCast(this.Document.onChildDoubleClick); - addDocTab = (doc: Doc, where: string) => { - if (where === 'inPlace' && this.layoutDoc.isInPlaceContainer) { - this.dataDoc[this.props.fieldKey] = new List<Doc>([doc]); - return true; - } - return this.props.addDocTab(doc, where); - }; - isContentActive = () => this.props.isSelected() || this.props.isContentActive(); - isChildContentActive = () => - ((this.props.childDocumentsActive?.() || this.Document._childDocumentsActive) && this.props.isDocumentActive?.() && SnappingManager.GetIsDragging()) || this.props.isSelected() || this.props.isAnyChildContentActive() ? true : false; + focusDocument = (doc: Doc, options: DocFocusOptions) => this.props.focus(this.rootDoc, options); + isContentActive = () => this.props.isSelected() || this.props.isContentActive() || this.props.isAnyChildContentActive(); + isChildContentActive = () => (((this.props.childDocumentsActive?.() || this.Document._childDocumentsActive) && this.props.isDocumentActive?.() && SnappingManager.GetIsDragging()) || this.isContentActive() ? true : false); getDisplayDoc = (layout: Doc, dxf: () => Transform, width: () => number, height: () => number) => { return ( <DocumentView @@ -277,7 +270,7 @@ export class CollectionMultirowView extends CollectionSubView() { moveDocument={this.props.moveDocument} removeDocument={this.props.removeDocument} whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged} - addDocTab={this.addDocTab} + addDocTab={this.props.addDocTab} pinToPres={this.props.pinToPres} bringToFront={returnFalse} /> diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx index adcd9e1e3..97a6c5c18 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx @@ -1,35 +1,37 @@ -import React = require("react"); -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, observable } from "mobx"; -import { observer } from "mobx-react"; -import { extname } from "path"; -import DatePicker from "react-datepicker"; -import { CellInfo } from "react-table"; -import { DateField } from "../../../../fields/DateField"; -import { Doc, DocListCast, Field, Opt } from "../../../../fields/Doc"; -import { Id } from "../../../../fields/FieldSymbols"; -import { List } from "../../../../fields/List"; -import { SchemaHeaderField } from "../../../../fields/SchemaHeaderField"; -import { ComputedField } from "../../../../fields/ScriptField"; -import { BoolCast, Cast, DateCast, FieldValue, StrCast } from "../../../../fields/Types"; -import { ImageField } from "../../../../fields/URLField"; -import { emptyFunction, Utils } 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 React = require('react'); +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, computed, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import { extname } from 'path'; +import DatePicker from 'react-datepicker'; +import { CellInfo } from 'react-table'; +import { DateField } from '../../../../fields/DateField'; +import { Doc, DocListCast, Field, Opt } from '../../../../fields/Doc'; +import { Id } from '../../../../fields/FieldSymbols'; +import { List } from '../../../../fields/List'; +import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField'; +import { ComputedField } from '../../../../fields/ScriptField'; +import { BoolCast, Cast, DateCast, FieldValue, StrCast } from '../../../../fields/Types'; +import { ImageField } from '../../../../fields/URLField'; +import { emptyFunction, Utils } 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 '../../../views/DocumentDecorations.scss'; -import { EditableView } from "../../EditableView"; +import { EditableView } from '../../EditableView'; import { MAX_ROW_HEIGHT } from '../../global/globalCssVariables.scss'; -import { DocumentIconContainer } from "../../nodes/DocumentIcon"; -import { OverlayView } from "../../OverlayView"; -import { CollectionView } from "../CollectionView"; -import "./CollectionSchemaView.scss"; +import { DocumentIconContainer } from '../../nodes/DocumentIcon'; +import { OverlayView } from '../../OverlayView'; +import { CollectionView } from '../CollectionView'; +import './CollectionSchemaView.scss'; +import { OpenWhere } from '../../nodes/DocumentView'; +import { PinProps } from '../../nodes/trails'; // intialize cell properties export interface CellProps { @@ -46,10 +48,9 @@ export interface CellProps { // currently unused renderDepth: number; // called when a button is pressed on the node itself - addDocTab: (document: Doc, where: string) => boolean; - pinToPres: (document: Doc) => void; - moveDocument?: (document: Doc | Doc[], targetCollection: Doc | undefined, - addDocument: (document: Doc | Doc[]) => boolean) => boolean; + addDocTab: (document: Doc, where: OpenWhere) => boolean; + pinToPres: (document: Doc, pinProps: PinProps) => void; + moveDocument?: (document: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean; isFocused: boolean; changeFocusedCellByIndex: (row: number, col: number) => void; // set whether the cell is in the isEditing mode @@ -67,7 +68,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> { // return a field key that is corrected for whether it COMMENT public static resolvedFieldKey(column: string, rowDoc: Doc) { const fieldKey = column; - if (fieldKey.startsWith("*")) { + 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)); @@ -82,33 +83,37 @@ export class CollectionSchemaCell extends React.Component<CellProps> { protected _rowDataDoc = Doc.GetProto(this.props.rowProps.original); // methods for dragging and dropping protected _dropDisposer?: DragManager.DragDropDisposer; - @observable contents: string = ""; + @observable contents: string = ''; - componentDidMount() { document.addEventListener("keydown", this.onKeyDown); } - componentWillUnmount() { document.removeEventListener("keydown", this.onKeyDown); } + componentDidMount() { + document.addEventListener('keydown', this.onKeyDown); + } + componentWillUnmount() { + document.removeEventListener('keydown', this.onKeyDown); + } @action onKeyDown = (e: KeyboardEvent): void => { // If a cell is editable and clicked, hitting enter shoudl allow the user to edit it if (this.props.isFocused && this.props.isEditable && e.keyCode === KeyCodes.ENTER) { - document.removeEventListener("keydown", this.onKeyDown); + document.removeEventListener('keydown', this.onKeyDown); this._isEditing = true; this.props.setIsEditing(true); } - } + }; @action isEditingCallback = (isEditing: boolean): void => { // a general method that takes a boolean that determines whether the cell should be in // is-editing mode // remove the event listener if it's there - document.removeEventListener("keydown", this.onKeyDown); + document.removeEventListener('keydown', this.onKeyDown); // it's not already in is-editing mode, re-add the event listener - isEditing && document.addEventListener("keydown", this.onKeyDown); + isEditing && document.addEventListener('keydown', this.onKeyDown); this._isEditing = isEditing; this.props.setIsEditing(isEditing); this.props.changeFocusedCellByIndex(this.props.row, this.props.col); - } + }; @action onPointerDown = async (e: React.PointerEvent): Promise<void> => { @@ -119,19 +124,19 @@ export class CollectionSchemaCell extends React.Component<CellProps> { this.props.setPreviewDoc(this.props.rowProps.original); let url: string; - if (url = StrCast(this.props.rowProps.row.href)) { + if ((url = StrCast(this.props.rowProps.row.href))) { // opens up the the doc in a new window, blurring the old one try { new URL(url); const temp = window.open(url)!; temp.blur(); window.focus(); - } catch { } + } catch {} } 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) => { @@ -142,7 +147,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> { doc[this.renderFieldKey] = res.result; return true; // return whether the change was successful - } + }; private drop = (e: Event, de: DragManager.DropEvent) => { // if the drag has data at its completion @@ -151,41 +156,51 @@ export class CollectionSchemaCell extends React.Component<CellProps> { if (de.complete.docDragData.draggedDocuments.length === 1) { // update the renderFieldKey this._rowDataDoc[this.renderFieldKey] = de.complete.docDragData.draggedDocuments[0]; - } - else { + } else { // create schema document reflecting the new column arrangement - const coll = Docs.Create.SchemaDocument([new SchemaHeaderField("title", "#f1efeb")], de.complete.docDragData.draggedDocuments, {}); + const coll = Docs.Create.SchemaDocument([new SchemaHeaderField('title', '#f1efeb')], de.complete.docDragData.draggedDocuments, {}); this._rowDataDoc[this.renderFieldKey] = coll; } e.stopPropagation(); } - } + }; protected dropRef = (ele: HTMLElement | null) => { // if the drop disposer is not undefined, run its function this._dropDisposer?.(); // if ele is not null, give ele a non-undefined drop disposer ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this))); - } + }; 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"; + const color = contents ? 'black' : 'grey'; - results.push(<span key="-1" style={{ color }}>{contents?.slice(0, positions[0])}</span>); + results.push( + <span key="-1" style={{ color }}> + {contents?.slice(0, positions[0])} + </span> + ); positions.forEach((num, cur) => { - results.push(<span key={"start" + cur} style={{ backgroundColor: "#FFFF00", color }}>{contents?.slice(num, num + length)}</span>); + results.push( + <span key={'start' + cur} style={{ backgroundColor: '#FFFF00', color }}> + {contents?.slice(num, num + length)} + </span> + ); let end = 0; - cur === positions.length - 1 ? end = contents.length : end = positions[cur + 1]; - results.push(<span key={"end" + cur} style={{ color }}>{contents?.slice(num + length, end)}</span>); - } - ); + cur === positions.length - 1 ? (end = contents.length) : (end = positions[cur + 1]); + results.push( + <span key={'end' + cur} style={{ color }}> + {contents?.slice(num + length, end)} + </span> + ); + }); return results; } - return <span style={{ color: contents ? "black" : "grey" }}>{contents ? contents?.valueOf() : "undefined"}</span>; + return <span style={{ color: contents ? 'black' : 'grey' }}>{contents ? contents?.valueOf() : 'undefined'}</span>; } @computed get renderFieldKey() { @@ -199,10 +214,9 @@ export class CollectionSchemaCell extends React.Component<CellProps> { const aliasdoc = await SearchUtil.GetAliasesOfDocument(this._rowDataDoc); const targetContext = aliasdoc.length <= 0 ? undefined : Cast(aliasdoc[0].context, Doc, null); // Jump to the this document - DocumentManager.Instance.jumpToDocument(this._rowDoc, false, emptyFunction, targetContext ? [targetContext] : [], - undefined, undefined, undefined, () => this.props.setPreviewDoc(this._rowDoc)); + DocumentManager.Instance.jumpToDocument(this._rowDoc, { willPan: true }, emptyFunction, targetContext ? [targetContext] : [], () => this.props.setPreviewDoc(this._rowDoc)); } - } + }; renderCellWithType(type: string | undefined) { const dragRef: React.RefObject<HTMLDivElement> = React.createRef(); @@ -214,29 +228,29 @@ export class CollectionSchemaCell extends React.Component<CellProps> { const onPointerEnter = (e: React.PointerEvent): void => { // e.buttons === 1 means the left moue pointer is down - if (e.buttons === 1 && SnappingManager.GetIsDragging() && (type === "document" || type === undefined)) { - dragRef.current!.className = "collectionSchemaView-cellContainer doc-drag-over"; + if (e.buttons === 1 && SnappingManager.GetIsDragging() && (type === 'document' || type === undefined)) { + dragRef.current!.className = 'collectionSchemaView-cellContainer doc-drag-over'; } }; const onPointerLeave = (e: React.PointerEvent): void => { // change the class name to indicate that the cell is no longer being dragged - dragRef.current!.className = "collectionSchemaView-cellContainer"; + dragRef.current!.className = 'collectionSchemaView-cellContainer'; }; let contents = Field.toString(field as Field); // display 2 hyphens instead of a blank box for empty cells - contents = contents === "" ? "--" : contents; + contents = contents === '' ? '--' : contents; // classname reflects the tatus of the cell - 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 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'; const positions = []; - if (StrCast(this.props.Document._searchString).toLowerCase() !== "") { + if (StrCast(this.props.Document._searchString).toLowerCase() !== '') { // term is ...promise pending... if the field is a Promise, otherwise it is the cell's contents - let term = (field instanceof Promise) ? "...promise pending..." : contents.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; @@ -256,56 +270,60 @@ export class CollectionSchemaCell extends React.Component<CellProps> { positions.pop(); } } - const placeholder = type === "number" ? "0" : contents === "" ? "--" : "undefined"; + const placeholder = type === 'number' ? '0' : contents === '' ? '--' : 'undefined'; return ( - <div className="collectionSchemaView-cellContainer" style={{ cursor: field instanceof Doc ? "grab" : "auto" }} - ref={dragRef} onPointerDown={this.onPointerDown} onClick={action(e => this._isEditing = true)} onPointerEnter={onPointerEnter} onPointerLeave={onPointerLeave}> + <div + className="collectionSchemaView-cellContainer" + style={{ cursor: field instanceof Doc ? 'grab' : 'auto' }} + ref={dragRef} + onPointerDown={this.onPointerDown} + onClick={action(e => (this._isEditing = true))} + onPointerEnter={onPointerEnter} + onPointerLeave={onPointerLeave}> <div className={className} ref={this._focusRef} tabIndex={-1}> - <div className="collectionSchemaView-cellContents" ref={type === undefined || type === "document" ? this.dropRef : null}> - {!this.props.Document._searchDoc ? + <div className="collectionSchemaView-cellContents" ref={type === undefined || type === 'document' ? this.dropRef : null}> + {!this.props.Document._searchDoc ? ( <EditableView editing={this._isEditing} isEditingCallback={this.isEditingCallback} - display={"inline"} + display={'inline'} contents={contents} - height={"auto"} + height={'auto'} maxHeight={Number(MAX_ROW_HEIGHT)} placeholder={placeholder} GetValue={() => { 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) : ""; + 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) => { // sets what is displayed after the user makes an input let retVal = false; - if (value.startsWith(":=") || value.startsWith("=:=")) { + if (value.startsWith(':=') || value.startsWith('=:=')) { // decides how to compute a value when given either of the above strings - 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); + 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 { // check if the input is a number let inputIsNum = true; for (const s of value) { - if (isNaN(parseInt(s)) && !(s === ".") && !(s === ",")) { + if (isNaN(parseInt(s)) && !(s === '.') && !(s === ',')) { inputIsNum = false; } } // check if the input is a boolean - const inputIsBool: boolean = value === "false" || value === "true"; - // what to do in the case - if (!inputIsNum && !inputIsBool && !value.startsWith("=")) { + const inputIsBool: boolean = value === 'false' || value === 'true'; + // what to do in the case + if (!inputIsNum && !inputIsBool && !value.startsWith('=')) { // if it's not a number, it's a string, and should be processed as such - // strips the string of quotes when it is edited to prevent quotes form being added to the text automatically + // strips the string of quotes when it is edited to prevent quotes form being added to the text automatically // after each edit let valueSansQuotes = value; if (this._isEditing) { const vsqLength = valueSansQuotes.length; // get rid of outer quotes - valueSansQuotes = valueSansQuotes.substring(value.startsWith("\"") ? 1 : 0, - valueSansQuotes.charAt(vsqLength - 1) === "\"" ? vsqLength - 1 : vsqLength); + valueSansQuotes = valueSansQuotes.substring(value.startsWith('"') ? 1 : 0, valueSansQuotes.charAt(vsqLength - 1) === '"' ? vsqLength - 1 : vsqLength); } let inputAsString = '"'; // escape any quotes in the string @@ -319,27 +337,27 @@ export class CollectionSchemaCell extends React.Component<CellProps> { // add a closing quote inputAsString += '"'; //two options here: we can strip off outer quotes or we can figure out what's going on with the script - const script = CompileScript(inputAsString, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); + const script = CompileScript(inputAsString, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: 'number', $c: 'number', $: 'any' } }); const changeMade = inputAsString.length !== value.length || inputAsString.length - 2 !== value.length; // change it if a change is made, otherwise, just compile using the old cell conetnts script.compiled && (retVal = this.applyToDoc(changeMade ? this._rowDoc : this._rowDataDoc, this.props.row, this.props.col, script.run)); // handle numbers and expressions - } else if (inputIsNum || value.startsWith("=")) { + } else if (inputIsNum || value.startsWith('=')) { //TODO: make accept numbers - const inputscript = value.substring(value.startsWith("=") ? 1 : 0); + const inputscript = value.substring(value.startsWith('=') ? 1 : 0); // if commas are not stripped, the parser only considers the numbers after the last comma - let inputSansCommas = ""; + let inputSansCommas = ''; for (const s of inputscript) { - if (!(s === ",")) { + if (!(s === ',')) { inputSansCommas += s; } } - const script = CompileScript(inputSansCommas, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); + const script = CompileScript(inputSansCommas, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: 'number', $c: 'number', $: 'any' } }); const changeMade = value.length - 2 !== value.length; script.compiled && (retVal = this.applyToDoc(changeMade ? this._rowDoc : this._rowDataDoc, this.props.row, this.props.col, script.run)); // handle booleans } else if (inputIsBool) { - const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); + const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: 'number', $c: 'number', $: 'any' } }); const changeMade = value.length - 2 !== value.length; script.compiled && (retVal = this.applyToDoc(changeMade ? this._rowDoc : this._rowDataDoc, this.props.row, this.props.col, script.run)); } @@ -352,33 +370,47 @@ export class CollectionSchemaCell extends React.Component<CellProps> { })} OnFillDown={async (value: string) => { // computes all of the value preceded by := - const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); - 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)); + const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: 'number', $c: 'number', $: 'any' } }); + 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(contents, positions) - } - </div > + )} + </div> </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 { @@ -396,24 +428,24 @@ export class CollectionSchemaDateCell extends CollectionSchemaCell { // ^ DateCast is always undefined for some reason, but that is what the field should be set to this._rowDoc[this.renderFieldKey] = new DateField(date as Date); //} - } + }; render() { - 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)} - />; + 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)} /> + ); } } @observer export class CollectionSchemaDocCell extends CollectionSchemaCell { - _overlayDisposer?: () => void; - @computed get _doc() { return FieldValue(Cast(this._rowDoc[this.renderFieldKey], Doc)); } + @computed get _doc() { + return FieldValue(Cast(this._rowDoc[this.renderFieldKey], Doc)); + } @action onSetValue = (value: string) => { @@ -422,7 +454,7 @@ export class CollectionSchemaDocCell extends CollectionSchemaCell { const script = CompileScript(value, { addReturn: true, typecheck: true, - transformer: DocumentIconContainer.getTransformer() + transformer: DocumentIconContainer.getTransformer(), }); // compile the script const results = script.compiled && script.run(); @@ -432,44 +464,43 @@ export class CollectionSchemaDocCell extends CollectionSchemaCell { return true; } return false; - } + }; - componentWillUnmount() { this.onBlur(); } + componentWillUnmount() { + this.onBlur(); + } - onBlur = () => { this._overlayDisposer?.(); }; + onBlur = () => { + this._overlayDisposer?.(); + }; onFocus = () => { this.onBlur(); this._overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 }); - } + }; @action isEditingCallback = (isEditing: boolean): void => { // the isEditingCallback from a general CollectionSchemaCell - document.removeEventListener("keydown", this.onKeyDown); - isEditing && document.addEventListener("keydown", this.onKeyDown); + document.removeEventListener('keydown', this.onKeyDown); + isEditing && document.addEventListener('keydown', this.onKeyDown); this._isEditing = isEditing; this.props.setIsEditing(isEditing); this.props.changeFocusedCellByIndex(this.props.row, this.props.col); - } + }; render() { // if there's a doc, render it - 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} - > + 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}> <EditableView editing={this._isEditing} isEditingCallback={this.isEditingCallback} - display={"inline"} - contents={this._doc.title || "--"} - height={"auto"} + display={'inline'} + contents={this._doc.title || '--'} + height={'auto'} maxHeight={Number(MAX_ROW_HEIGHT)} GetValue={() => StrCast(this._doc?.title)} SetValue={action((value: string) => { @@ -477,33 +508,36 @@ export class CollectionSchemaDocCell extends CollectionSchemaCell { return true; })} /> - </div > - <div onClick={() => this._doc && this.props.addDocTab(this._doc, "add:right")} className="collectionSchemaView-cellContents-docButton"> + </div> + <div onClick={() => this._doc && this.props.addDocTab(this._doc, OpenWhere.addRight)} className="collectionSchemaView-cellContents-docButton"> <FontAwesomeIcon icon="external-link-alt" size="lg" /> </div> - </div>; + </div> + ); } } @observer export class CollectionSchemaImageCell extends CollectionSchemaCell { - choosePath(url: URL) { - if (url.protocol === "data") return url.href; // if the url ises the data protocol, just return the href + if (url.protocol === 'data') return url.href; // if the url ises the data protocol, just return the href if (url.href.indexOf(window.location.origin) === -1) return Utils.CorsProxy(url.href); // otherwise, put it through the cors proxy erver - if (!/\.(png|jpg|jpeg|gif|webp)$/.test(url.href.toLowerCase())) return url.href;//Why is this here — good question + if (!/\.(png|jpg|jpeg|gif|webp)$/.test(url.href.toLowerCase())) return url.href; //Why is this here — good question const ext = extname(url.href); - return url.href.replace(ext, "_o" + ext); + return url.href.replace(ext, '_o' + ext); } render() { 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 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; // If there is a path, follow it; otherwise, follow a link to a default image icon - const url = paths.length ? paths : [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")]; + const url = paths.length ? paths : [Utils.CorsProxy('http://www.cs.brown.edu/~bcz/noImage.png')]; const aspect = Doc.NativeAspect(this._rowDoc); // aspect ratio let width = Math.min(75, this.props.rowProps.width); // get a with that is no smaller than 75px @@ -511,25 +545,28 @@ export class CollectionSchemaImageCell extends CollectionSchemaCell { width = height * aspect; // increase the width of the image if necessary to maintain proportionality 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>; + 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; - @computed get _field() { return this._rowDoc[this.renderFieldKey]; } - @computed get _optionsList() { return 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; // whether the list is opened - @observable private _text = "select an item"; + @observable private _text = 'select an item'; @observable private _selectedNum = 0; // the index of the list item selected @action @@ -538,102 +575,109 @@ export class CollectionSchemaListCell extends CollectionSchemaCell { this._optionsList[this._selectedNum] = this._text = value; (this._field as List<any>).splice(this._selectedNum, 1, value); - } + }; @action onSelected = (element: string, index: number) => { // if an item is selected, the private variables should update to reflect this this._text = element; this._selectedNum = index; - } + }; onFocus = () => { this._overlayDisposer?.(); this._overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 }); - } + }; render() { const link = false; const reference = React.createRef<HTMLDivElement>(); - // if the list is not opened, don't display it; otherwise, do. + // if the list is not opened, don't display it; otherwise, do. if (this._optionsList?.length) { - const options = !this._opened ? (null) : + 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>; + 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 = <div className="collectionSchemaView-cellContents" key={this._rowDoc[Id]} style={{ padding: "5.9px" }} ref={this.dropRef} > - <EditableView - editing={this._isEditing} - isEditingCallback={this.isEditingCallback} - display={"inline"} - contents={this._text} - height={"auto"} - maxHeight={Number(MAX_ROW_HEIGHT)} - GetValue={() => this._text} - SetValue={action((value: string) => { - // add special for params - this.onSetValue(value); - return true; - })} - /> - </div >; + </div> + ); + + const plainText = <div style={{ padding: '5.9px' }}>{this._text}</div>; + const textarea = ( + <div className="collectionSchemaView-cellContents" key={this._rowDoc[Id]} style={{ padding: '5.9px' }} ref={this.dropRef}> + <EditableView + editing={this._isEditing} + isEditingCallback={this.isEditingCallback} + display={'inline'} + contents={this._text} + height={'auto'} + maxHeight={Number(MAX_ROW_HEIGHT)} + GetValue={() => this._text} + SetValue={action((value: string) => { + // add special for params + this.onSetValue(value); + return true; + })} + /> + </div> + ); //☰ return ( <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}> <div className="collectionSchemaView-cellContents" key={this._rowDoc[Id]} ref={reference}> <div className="collectionSchemaView-dropDownWrapper"> - <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 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> {options} - </div > + </div> </div> ); } - return this.renderCellWithType("list"); + return this.renderCellWithType('list'); } } - @observer export class CollectionSchemaCheckboxCell extends CollectionSchemaCell { - @computed get _isChecked() { return BoolCast(this._rowDoc[this.renderFieldKey]); } + @computed get _isChecked() { + return BoolCast(this._rowDoc[this.renderFieldKey]); + } render() { const reference = React.createRef<HTMLDivElement>(); return ( <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}> - <input type="checkbox" checked={this._isChecked} onChange={e => this._rowDoc[this.renderFieldKey] = e.target.checked} /> + <input type="checkbox" checked={this._isChecked} onChange={e => (this._rowDoc[this.renderFieldKey] = e.target.checked)} /> </div> ); } } - @observer export class CollectionSchemaButtons extends CollectionSchemaCell { // the navigation buttons for schema view when it is used for search. render() { - return !this.props.Document._searchDoc || ![DocumentType.PDF, DocumentType.RTF].includes(StrCast(this._rowDoc.type) as DocumentType) ? <></> : - <div style={{ paddingTop: 8, paddingLeft: 3 }} > + 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)} > + <button style={{ padding: 2 }} onClick={() => Doc.SearchMatchNext(this._rowDoc, false)}> <FontAwesomeIcon icon="arrow-down" size="sm" /> </button> - </div>; + </div> + ); } } diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx index f872637e5..3cb2df7d3 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx @@ -10,6 +10,7 @@ import { SnappingManager } from '../../../util/SnappingManager'; import { Transform } from '../../../util/Transform'; import { undoBatch } from '../../../util/UndoManager'; import { ContextMenu } from '../../ContextMenu'; +import { OpenWhere } from '../../nodes/DocumentView'; import './CollectionSchemaView.scss'; export interface MovableRowProps { @@ -138,7 +139,7 @@ export class MovableRow extends React.Component<React.PropsWithChildren<MovableR <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, 'add:right')}> + <div className="row-option" onClick={() => this.props.addDocTab(this.props.rowInfo.original, OpenWhere.addRight)}> <FontAwesomeIcon icon="external-link-alt" size="sm" /> </div> </div> diff --git a/src/client/views/collections/collectionSchema/SchemaTable.tsx b/src/client/views/collections/collectionSchema/SchemaTable.tsx index fafea5ce3..16910cc83 100644 --- a/src/client/views/collections/collectionSchema/SchemaTable.tsx +++ b/src/client/views/collections/collectionSchema/SchemaTable.tsx @@ -23,7 +23,8 @@ import { undoBatch } from '../../../util/UndoManager'; import '../../../views/DocumentDecorations.scss'; import { ContextMenu } from '../../ContextMenu'; import { COLLECTION_BORDER_WIDTH, SCHEMA_DIVIDER_WIDTH } from '../../global/globalCssVariables.scss'; -import { DocumentView } from '../../nodes/DocumentView'; +import { DocumentView, OpenWhere } from '../../nodes/DocumentView'; +import { PinProps } from '../../nodes/trails'; import { DefaultStyleProvider } from '../../StyleProvider'; import { CollectionView } from '../CollectionView'; import { @@ -86,8 +87,8 @@ export interface SchemaTableProps { ScreenToLocalTransform: () => Transform; active: (outsideReaction: boolean | undefined) => boolean | undefined; onDrop: (e: React.DragEvent<Element>, options: DocumentOptions, completed?: (() => void) | undefined) => void; - addDocTab: (document: Doc, where: string) => boolean; - pinToPres: (document: Doc) => void; + addDocTab: (document: Doc, where: OpenWhere) => boolean; + pinToPres: (document: Doc, pinProps: PinProps) => void; isSelected: (outsideReaction?: boolean) => boolean; isFocused: (document: Doc, outsideReaction: boolean) => boolean; setFocused: (document: Doc) => void; @@ -625,7 +626,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { }; onOpenClick = () => { - this._showDoc && this.props.addDocTab(this._showDoc, 'add:right'); + this._showDoc && this.props.addDocTab(this._showDoc, OpenWhere.addRight); }; getPreviewTransform = (): Transform => { diff --git a/src/client/views/global/globalCssVariables.scss b/src/client/views/global/globalCssVariables.scss index e68f9abe3..430a36dce 100644 --- a/src/client/views/global/globalCssVariables.scss +++ b/src/client/views/global/globalCssVariables.scss @@ -84,4 +84,5 @@ $TREE_BULLET_WIDTH: 20px; LEFT_MENU_WIDTH: $LEFT_MENU_WIDTH; TREE_BULLET_WIDTH: $TREE_BULLET_WIDTH; INK_MASK_SIZE: $INK_MASK_SIZE; + MEDIUM_GRAY: $medium-gray; } diff --git a/src/client/views/global/globalCssVariables.scss.d.ts b/src/client/views/global/globalCssVariables.scss.d.ts index 76259113c..3375579d6 100644 --- a/src/client/views/global/globalCssVariables.scss.d.ts +++ b/src/client/views/global/globalCssVariables.scss.d.ts @@ -11,6 +11,7 @@ interface IGlobalScss { LEFT_MENU_WIDTH: string; TREE_BULLET_WIDTH: string; INK_MASK_SIZE: number; + MEDIUM_GRAY: string; } declare const globalCssVariables: IGlobalScss; diff --git a/src/client/views/linking/LinkEditor.scss b/src/client/views/linking/LinkEditor.scss deleted file mode 100644 index b0ee4e46d..000000000 --- a/src/client/views/linking/LinkEditor.scss +++ /dev/null @@ -1,334 +0,0 @@ -@import '../global/globalCssVariables'; - -.linkEditor { - width: 100%; - height: auto; - font-size: 13px; // TODO - user-select: none; - max-width: 280px; -} - -.linkEditor-button-back { - //margin-bottom: 6px; - border-radius: 10px; - width: 18px; - height: 18px; - padding: 0; - - &:hover { - cursor: pointer; - } -} - -.linkEditor-info { - padding-top: 12px; - padding-left: 5px; - padding-bottom: 3px; - //margin-bottom: 6px; - display: flex; - justify-content: space-between; - color: black; - - .linkEditor-linkedTo { - width: calc(100% - 46px); - overflow: hidden; - position: relative; - text-overflow: ellipsis; - white-space: pre; - - .linkEditor-downArrow { - &:hover { - cursor: pointer; - } - } - } -} - -.linkEditor-moreInfo { - margin-left: 12px; - padding-left: 13px; - padding-right: 6.5px; - padding-bottom: 4px; - font-size: 9px; - //font-style: italic; - text-decoration-color: grey; - - .button { - color: black; - - &:hover { - cursor: pointer; - } - } -} - -.linkEditor-zoomFollow { - padding-left: 26px; - padding-right: 6.5px; - padding-bottom: 3.5px; - display: flex; - - .linkEditor-zoomFollow-label { - text-decoration-color: black; - color: black; - line-height: 1.7; - } - - .linkEditor-zoomFollow-input { - display: block; - width: 20px; - } -} -.linkEditor-deleteBtn { - padding-left: 3px; -} - -.linkEditor-description { - padding-left: 26px; - padding-bottom: 3.5px; - display: flex; - - .linkEditor-description-label { - text-decoration-color: black; - color: black; - } - - .linkEditor-description-input { - display: flex; - - .linkEditor-description-editing { - min-width: 85%; - //border: 1px solid grey; - //border-radius: 4px; - padding-left: 2px; - //margin-right: 4px; - color: black; - text-decoration-color: grey; - } - - .linkEditor-description-add-button { - display: inline; - border-radius: 7px; - font-size: 9px; - background: black; - height: 80%; - color: white; - padding: 3px; - margin-left: 3px; - - &:hover { - cursor: pointer; - background: grey; - } - } - } -} - -.linkEditor-relationship-dropdown { - position: absolute; - width: 154px; - max-height: 90px; - overflow: auto; - background: white; - - p { - padding: 3px; - cursor: pointer; - border: 1px solid $medium-gray; - } - - p:hover { - background: $light-blue; - } -} - -.linkEditor-followingDropdown { - padding-left: 26px; - padding-right: 6.5px; - padding-bottom: 15px; - display: flex; - - &:hover { - cursor: pointer; - } - - .linkEditor-followingDropdown-label { - color: black; - padding-right: 3px; - } - - .linkEditor-followingDropdown-dropdown { - .linkEditor-followingDropdown-header { - border: 1px solid grey; - border-radius: 4px; - //background-color: rgb(236, 236, 236); - padding-left: 2px; - padding-right: 2px; - text-decoration-color: black; - color: rgb(94, 94, 94); - - .linkEditor-followingDropdown-icon { - float: right; - color: black; - } - } - - .linkEditor-followingDropdown-optionsList { - padding-left: 3px; - padding-right: 3px; - - &:last-child { - border-bottom: none; - } - - .linkEditor-followingDropdown-option { - border: 0.25px solid grey; - //background-color: rgb(236, 236, 236); - padding-left: 2px; - padding-right: 2px; - color: grey; - text-decoration-color: grey; - font-size: 9px; - border-top: none; - - &:hover { - background-color: rgb(187, 220, 231); - } - } - } - } -} - -.linkEditor-button, -.linkEditor-addbutton { - width: 15%; - border-radius: 7px; - font-size: 9px; - background: black; - padding: 3px; - height: 80%; - color: white; - text-align: center; - margin: auto; - margin-left: 3px; - > svg { - margin: auto; - } - &:disabled { - background-color: gray; - } -} - -.linkEditor-addbutton { - margin-left: 0px; -} - -.linkEditor-groupsLabel { - display: flex; - justify-content: space-between; -} - -.linkEditor-group { - background-color: $light-gray; - padding: 6px; - margin: 3px 0; - border-radius: 3px; - - .linkEditor-group-row { - display: flex; - margin-bottom: 3px; - } - - .linkEditor-group-row-label { - margin-right: 6px; - display: inline-block; - } - - .linkEditor-metadata-row { - display: flex; - justify-content: space-between; - margin-bottom: 6px; - - .linkEditor-error { - border-color: red; - } - - input { - width: calc(50% - 16px); - height: 20px; - } - - button { - width: 20px; - height: 20px; - margin-left: 3px; - padding: 0; - font-size: 10px; - } - } -} - -.linkEditor-dropdown { - width: 100%; - position: relative; - z-index: 999; - - input { - width: 100%; - } - - .linkEditor-options-wrapper { - width: 100%; - position: absolute; - top: 19px; - left: 0; - display: flex; - flex-direction: column; - } - - .linkEditor-option { - background-color: $light-gray; - border: 1px solid $medium-gray; - border-top: 0; - padding: 3px; - cursor: pointer; - - &:hover { - background-color: lightgray; - } - - &.onDown { - background-color: gray; - } - } -} - -.linkEditor-typeButton { - background-color: transparent; - color: $dark-gray; - height: 20px; - padding: 0 3px; - padding-bottom: 2px; - text-align: left; - text-transform: none; - letter-spacing: normal; - font-size: 12px; - font-weight: bold; - display: inline-block; - width: calc(100% - 40px); - - &:hover { - background-color: $white; - } -} - -.linkEditor-group-buttons { - height: 20px; - display: flex; - justify-content: flex-end; - margin-top: 5px; - - .linkEditor-button { - margin-left: 3px; - } -} diff --git a/src/client/views/linking/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx deleted file mode 100644 index 1697062f4..000000000 --- a/src/client/views/linking/LinkEditor.tsx +++ /dev/null @@ -1,394 +0,0 @@ -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Tooltip } from '@material-ui/core'; -import { action, computed, observable } from 'mobx'; -import { observer } from 'mobx-react'; -import { Doc, NumListCast, StrListCast, Field } from '../../../fields/Doc'; -import { DateCast, StrCast, Cast, BoolCast } from '../../../fields/Types'; -import { LinkManager } from '../../util/LinkManager'; -import { undoBatch } from '../../util/UndoManager'; -import './LinkEditor.scss'; -import { LinkRelationshipSearch } from './LinkRelationshipSearch'; -import React = require('react'); -import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../Utils'; - -interface LinkEditorProps { - sourceDoc: Doc; - linkDoc: Doc; - showLinks?: () => void; - hideback?: boolean; -} -@observer -export class LinkEditor extends React.Component<LinkEditorProps> { - @observable description = Field.toString(LinkManager.currentLink?.description as any as Field); - @observable relationship = StrCast(LinkManager.currentLink?.linkRelationship); - @observable zoomFollow = BoolCast(this.props.sourceDoc.followLinkZoom); - @observable audioFollow = BoolCast(this.props.sourceDoc.followLinkAudio); - @observable openDropdown: boolean = false; - @observable private buttonColor: string = ''; - @observable private relationshipButtonColor: string = ''; - @observable private relationshipSearchVisibility: string = 'none'; - @observable private searchIsActive: boolean = false; - - //@observable description = this.props.linkDoc.description ? StrCast(this.props.linkDoc.description) : "DESCRIPTION"; - - @undoBatch - setRelationshipValue = action((value: string) => { - if (LinkManager.currentLink) { - const prevRelationship = LinkManager.currentLink.linkRelationship as string; - LinkManager.currentLink.linkRelationship = value; - Doc.GetProto(LinkManager.currentLink).linkRelationship = value; - const linkRelationshipList = StrListCast(Doc.UserDoc().linkRelationshipList); - const linkRelationshipSizes = NumListCast(Doc.UserDoc().linkRelationshipSizes); - const linkColorList = StrListCast(Doc.UserDoc().linkColorList); - - // if the relationship does not exist in the list, add it and a corresponding unique randomly generated color - if (!linkRelationshipList?.includes(value)) { - linkRelationshipList.push(value); - linkRelationshipSizes.push(1); - const randColor = 'rgb(' + Math.floor(Math.random() * 255) + ',' + Math.floor(Math.random() * 255) + ',' + Math.floor(Math.random() * 255) + ')'; - linkColorList.push(randColor); - // if the relationship is already in the list AND the new rel is different from the prev rel, update the rel sizes - } else if (linkRelationshipList && value !== prevRelationship) { - const index = linkRelationshipList.indexOf(value); - //increment size of new relationship size - if (index !== -1 && index < linkRelationshipSizes.length) { - const pvalue = linkRelationshipSizes[index]; - linkRelationshipSizes[index] = pvalue === undefined || !Number.isFinite(pvalue) ? 1 : pvalue + 1; - } - //decrement the size of the previous relationship if it already exists (i.e. not default 'link' relationship upon link creation) - if (linkRelationshipList.includes(prevRelationship)) { - const pindex = linkRelationshipList.indexOf(prevRelationship); - if (pindex !== -1 && pindex < linkRelationshipSizes.length) { - const pvalue = linkRelationshipSizes[pindex]; - linkRelationshipSizes[pindex] = Math.max(0, pvalue === undefined || !Number.isFinite(pvalue) ? 1 : pvalue - 1); - } - } - } - this.relationshipButtonColor = 'rgb(62, 133, 55)'; - setTimeout( - action(() => (this.relationshipButtonColor = '')), - 750 - ); - return true; - } - }); - - /** - * returns list of strings with possible existing relationships that contain what is currently in the input field - */ - @action - getRelationshipResults = () => { - const query = this.relationship; //current content in input box - const linkRelationshipList = StrListCast(Doc.UserDoc().linkRelationshipList); - if (linkRelationshipList) { - return linkRelationshipList.filter(rel => rel.includes(query)); - } - }; - - /** - * toggles visibility of the relationship search results when the input field is focused on - */ - @action - toggleRelationshipResults = () => { - this.relationshipSearchVisibility = this.relationshipSearchVisibility === 'none' ? 'block' : 'none'; - }; - - @undoBatch - setDescripValue = action((value: string) => { - if (LinkManager.currentLink) { - Doc.GetProto(LinkManager.currentLink).description = value; - this.buttonColor = 'rgb(62, 133, 55)'; - setTimeout( - action(() => (this.buttonColor = '')), - 750 - ); - return true; - } - }); - - onDescriptionKey = (e: React.KeyboardEvent<HTMLInputElement>) => { - if (e.key === 'Enter') { - this.setDescripValue(this.description); - document.getElementById('input')?.blur(); - } - e.stopPropagation(); - }; - - onRelationshipKey = (e: React.KeyboardEvent<HTMLInputElement>) => { - if (e.key === 'Enter') { - this.setRelationshipValue(this.relationship); - document.getElementById('input')?.blur(); - } - e.stopPropagation(); - }; - - onDescriptionDown = () => this.setDescripValue(this.description); - onRelationshipDown = () => this.setRelationshipValue(this.relationship); - - onBlur = () => { - //only hide the search results if the user clicks out of the input AND not on any of the search results - // i.e. if search is not active - if (!this.searchIsActive) { - this.toggleRelationshipResults(); - } - }; - onFocus = () => { - this.toggleRelationshipResults(); - }; - toggleSearchIsActive = () => { - this.searchIsActive = !this.searchIsActive; - }; - - @action - handleDescriptionChange = (e: React.ChangeEvent<HTMLInputElement>) => { - this.description = e.target.value; - }; - @action - handleRelationshipChange = (e: React.ChangeEvent<HTMLInputElement>) => { - this.relationship = e.target.value; - }; - @action - handleZoomFollowChange = () => { - this.props.sourceDoc.followLinkZoom = !this.props.sourceDoc.followLinkZoom; - }; - @action - handleAudioFollowChange = () => { - this.props.sourceDoc.followLinkAudio = !this.props.sourceDoc.followLinkAudio; - }; - @action - handleRelationshipSearchChange = (result: string) => { - this.setRelationshipValue(result); - this.toggleRelationshipResults(); - this.relationship = result; - }; - @computed - get editRelationship() { - //NOTE: confusingly, the classnames for the following relationship JSX elements are the same as the for the description elements for shared CSS - return ( - <div className="linkEditor-description"> - <div className="linkEditor-description-label">Relationship:</div> - <div className="linkEditor-description-input"> - <div className="linkEditor-description-editing"> - <input - style={{ width: '100%' }} - id="input" - value={this.relationship} - autoComplete={'off'} - placeholder={'Enter link relationship'} - onKeyDown={this.onRelationshipKey} - onChange={this.handleRelationshipChange} - onFocus={this.onFocus} - onBlur={this.onBlur}></input> - <LinkRelationshipSearch results={this.getRelationshipResults()} display={this.relationshipSearchVisibility} handleRelationshipSearchChange={this.handleRelationshipSearchChange} toggleSearch={this.toggleSearchIsActive} /> - </div> - <div className="linkEditor-description-add-button" style={{ background: this.relationshipButtonColor }} onPointerDown={this.onRelationshipDown}> - Set - </div> - </div> - </div> - ); - } - @computed - get editZoomFollow() { - //NOTE: confusingly, the classnames for the following relationship JSX elements are the same as the for the description elements for shared CSS - return ( - <div className="linkEditor-zoomFollow"> - <div className="linkEditor-zoomFollow-label">Zoom To Link Target:</div> - <div className="linkEditor-zoomFollow-input"> - <div className="linkEditor-zoomFollow-editing"> - <input type="checkbox" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, this.handleZoomFollowChange)} defaultChecked={this.zoomFollow} /> - </div> - </div> - </div> - ); - } - - @computed - get editAudioFollow() { - //NOTE: confusingly, the classnames for the following relationship JSX elements are the same as the for the description elements for shared CSS - console.log('AudioFollow:' + this.audioFollow); - return ( - <div className="linkEditor-zoomFollow"> - <div className="linkEditor-zoomFollow-label">Play Target Audio:</div> - <div className="linkEditor-zoomFollow-input"> - <div className="linkEditor-zoomFollow-editing"> - <input type="checkbox" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, this.handleAudioFollowChange)} defaultChecked={this.audioFollow} /> - </div> - </div> - </div> - ); - } - - @computed - get editDescription() { - return ( - <div className="linkEditor-description"> - <div className="linkEditor-description-label">Description:</div> - <div className="linkEditor-description-input"> - <div className="linkEditor-description-editing"> - <input style={{ width: '100%' }} autoComplete={'off'} id="input" value={this.description} placeholder={'Enter link description'} onKeyDown={this.onDescriptionKey} onChange={this.handleDescriptionChange}></input> - </div> - <div className="linkEditor-description-add-button" style={{ background: this.buttonColor }} onPointerDown={this.onDescriptionDown}> - Set - </div> - </div> - </div> - ); - } - - @action - changeDropdown = () => { - this.openDropdown = !this.openDropdown; - }; - - @undoBatch - changeFollowBehavior = action((follow: string) => { - this.openDropdown = false; - Doc.GetProto(this.props.linkDoc).followLinkLocation = follow; - }); - - @computed - get followingDropdown() { - return ( - <div className="linkEditor-followingDropdown"> - <div className="linkEditor-followingDropdown-label">Follow Behavior:</div> - <div className="linkEditor-followingDropdown-dropdown"> - <div className="linkEditor-followingDropdown-header" onPointerDown={this.changeDropdown}> - {StrCast(this.props.linkDoc.followLinkLocation, 'default')} - <FontAwesomeIcon className="linkEditor-followingDropdown-icon" icon={this.openDropdown ? 'chevron-up' : 'chevron-down'} size={'lg'} /> - </div> - <div className="linkEditor-followingDropdown-optionsList" style={{ display: this.openDropdown ? '' : 'none' }}> - <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('default')}> - Default - </div> - <div className="linkEditor-followingDropdown-option" 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('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')}> - Always open in external page - </div> - ) : null} - </div> - </div> - </div> - ); - } - - autoMove = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => (this.props.linkDoc.linkAutoMove = !this.props.linkDoc.linkAutoMove)))); - }; - - showAnchor = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => (this.props.linkDoc.hidden = !this.props.linkDoc.hidden)))); - }; - - showLink = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => (this.props.linkDoc.linkDisplay = !this.props.linkDoc.linkDisplay)))); - }; - - deleteLink = (e: React.PointerEvent): void => { - setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => LinkManager.Instance.deleteLink(this.props.linkDoc)))); - }; - - render() { - const destination = LinkManager.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc); - - return !destination ? null : ( - <div className="linkEditor" tabIndex={0} onKeyDown={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> - <div className="linkEditor-info"> - {!this.props.showLinks ? null : ( - <Tooltip title={<div className="dash-tooltip">Return to link menu</div>} placement="top"> - <button className="linkEditor-button-back" style={{ display: this.props.hideback ? 'none' : '' }} onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => this.props.showLinks?.())}> - <FontAwesomeIcon icon="arrow-left" size="sm" />{' '} - </button> - </Tooltip> - )} - <p className="linkEditor-linkedTo"> - Editing Link to: <b>{StrCast(destination.proto?.title, StrCast(destination.title, 'untitled'))}</b> - </p> - <Tooltip title={<div className="dash-tooltip">Delete Link</div>}> - <div className="linkEditor-deleteBtn" onPointerDown={this.deleteLink} onClick={e => e.stopPropagation()}> - <FontAwesomeIcon className="fa-icon" icon="trash" size="sm" /> - </div> - </Tooltip> - </div> - <div className="linkEditor-moreInfo"> - {this.props.linkDoc.author ? ( - <> - {' '} - <b>Author:</b> {StrCast(this.props.linkDoc.author)} - </> - ) : null} - {this.props.linkDoc.creationDate ? ( - <> - {' '} - <b>Creation Date:</b> - {DateCast(this.props.linkDoc.creationDate).toString()} - </> - ) : null} - </div> - - {this.editDescription} - {this.editRelationship} - {this.editZoomFollow} - {this.editAudioFollow} - <div className="linkEditor-description"> - Show Anchor: - <Tooltip title={<div className="dash-tooltip">{this.props.linkDoc.hidden ? 'Show Link Anchor' : 'Hide Link Anchor'}</div>}> - <div - className="linkEditor-button" - style={{ background: this.props.linkDoc.hidden ? 'gray' : '#4476f7' /* $medium-blue */ }} - onPointerDown={this.showAnchor} - onClick={e => e.stopPropagation()}> - <FontAwesomeIcon className="fa-icon" icon={'eye'} size="sm" /> - </div> - </Tooltip> - </div> - <div className="linkEditor-description"> - Show Link Line: - <Tooltip title={<div className="dash-tooltip">{this.props.linkDoc.linkDisplay ? 'Hide Link Line' : 'Show Link Line'}</div>}> - <div - className="linkEditor-button" - style={{ background: this.props.linkDoc.hidden ? 'gray' : this.props.linkDoc.linkDisplay ? '#4476f7' /* $medium-blue */ : '' }} - onPointerDown={this.showLink} - onClick={e => e.stopPropagation()}> - <FontAwesomeIcon className="fa-icon" icon={'project-diagram'} size="sm" /> - </div> - </Tooltip> - </div> - <div className="linkEditor-description"> - Freeze Anchor: - <Tooltip title={<div className="dash-tooltip">{this.props.linkDoc.linkAutoMove ? 'Click to freeze link anchor position' : 'Click to auto move link anchor'}</div>}> - <div - className="linkEditor-button" - style={{ background: this.props.linkDoc.hidden ? 'gray' : this.props.linkDoc.linkAutoMove ? '' : '#4476f7' /* $medium-blue */ }} - onPointerDown={this.autoMove} - onClick={e => e.stopPropagation()}> - <FontAwesomeIcon className="fa-icon" icon={'play'} size="sm" /> - </div> - </Tooltip> - </div> - {this.followingDropdown} - </div> - ); - } -} diff --git a/src/client/views/linking/LinkMenu.scss b/src/client/views/linking/LinkMenu.scss index 77c16a28f..80cf93ed8 100644 --- a/src/client/views/linking/LinkMenu.scss +++ b/src/client/views/linking/LinkMenu.scss @@ -1,4 +1,4 @@ -@import "../global/globalCssVariables"; +@import '../global/globalCssVariables'; .linkMenu { width: auto; @@ -13,7 +13,6 @@ border: 1px solid #e4e4e4; box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23); background: white; - min-width: 170px; max-height: 230px; overflow-y: scroll; z-index: 10; @@ -22,7 +21,7 @@ .linkMenu-list { white-space: nowrap; overflow-x: hidden; - width: 240px; + width: 100%; scrollbar-color: white; &:last-child { @@ -39,7 +38,6 @@ border-bottom: 0.5px solid lightgray; //@extend: 5px 0; - &:last-child { border-bottom: none; } @@ -76,4 +74,4 @@ display: none; } } -}
\ No newline at end of file +} diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx index 0096a58bd..c9112eec3 100644 --- a/src/client/views/linking/LinkMenu.tsx +++ b/src/client/views/linking/LinkMenu.tsx @@ -1,19 +1,19 @@ -import { action, computed, observable } from 'mobx'; +import { action, observable } from 'mobx'; import { observer } from 'mobx-react'; import { Doc } from '../../../fields/Doc'; +import { DocCast } from '../../../fields/Types'; import { LinkManager } from '../../util/LinkManager'; import { DocumentView } from '../nodes/DocumentView'; import { LinkDocPreview } from '../nodes/LinkDocPreview'; -import { LinkEditor } from './LinkEditor'; import './LinkMenu.scss'; import { LinkMenuGroup } from './LinkMenuGroup'; import React = require('react'); interface Props { docView: DocumentView; - position?: { x?: number; y?: number }; + style?: { left: number; top: number }; itemHandler?: (doc: Doc) => void; - clearLinkEditor: () => void; + clearLinkEditor?: () => void; } /** @@ -22,23 +22,15 @@ interface Props { @observer export class LinkMenu extends React.Component<Props> { _editorRef = React.createRef<HTMLDivElement>(); - @observable _editingLink?: Doc; @observable _linkMenuRef = React.createRef<HTMLDivElement>(); - @computed get position() { - return this.props.position ?? (dv => ({ x: dv?.left || 0, y: (dv?.bottom || 0) + 15 }))(this.props.docView.getBounds()); - } - - clear = action(() => { - this.props.clearLinkEditor(); - this._editingLink = undefined; - }); + clear = () => this.props.clearLinkEditor?.(); componentDidMount() { - document.addEventListener('pointerdown', this.onPointerDown, true); + this.props.clearLinkEditor && document.addEventListener('pointerdown', this.onPointerDown, true); } componentWillUnmount() { - document.removeEventListener('pointerdown', this.onPointerDown, true); + this.props.clearLinkEditor && document.removeEventListener('pointerdown', this.onPointerDown, true); } onPointerDown = action((e: PointerEvent) => { @@ -55,32 +47,20 @@ export class LinkMenu extends React.Component<Props> { */ renderAllGroups = (groups: Map<string, Array<Doc>>): Array<JSX.Element> => { const linkItems = Array.from(groups.entries()).map(group => ( - <LinkMenuGroup - key={group[0]} - itemHandler={this.props.itemHandler} - docView={this.props.docView} - sourceDoc={this.props.docView.props.Document} - group={group[1]} - groupType={group[0]} - clearLinkEditor={this.clear} - showEditor={action(linkDoc => (this._editingLink = linkDoc))} - /> + <LinkMenuGroup key={group[0]} itemHandler={this.props.itemHandler} docView={this.props.docView} sourceDoc={this.props.docView.props.Document} group={group[1]} groupType={group[0]} clearLinkEditor={this.clear} /> )); - return linkItems.length ? linkItems : this.props.position ? [<></>] : [<p key="">No links have been created yet. Drag the linking button onto another document to create a link.</p>]; + return linkItems.length ? linkItems : this.props.style ? [<></>] : [<p key="">No links have been created yet. Drag the linking button onto another document to create a link.</p>]; }; render() { - const sourceDoc = this.props.docView.props.Document; + const sourceDoc = this.props.docView.rootDoc; + const sourceAnchor = this.props.docView.anchorViewDoc ?? sourceDoc; + const style = this.props.style ?? (dv => ({ left: dv?.left || 0, top: this.props.docView.topMost ? undefined : (dv?.bottom || 0) + 15, bottom: this.props.docView.topMost ? 20 : undefined, maxWidth: 200 }))(this.props.docView.getBounds()); + return ( - <div className="linkMenu" ref={this._linkMenuRef} style={{ left: this.position.x, top: this.props.docView.topMost ? undefined : this.position.y, bottom: this.props.docView.topMost ? 20 : undefined }}> - {this._editingLink ? ( - <div className="linkMenu-listEditor"> - <LinkEditor sourceDoc={sourceDoc} linkDoc={this._editingLink} showLinks={action(() => (this._editingLink = undefined))} /> - </div> - ) : ( - <div className="linkMenu-list">{this.renderAllGroups(LinkManager.Instance.getRelatedGroupedLinks(sourceDoc))}</div> - )} + <div className="linkMenu" ref={this._linkMenuRef} style={{ ...style }}> + <div className="linkMenu-list">{this.renderAllGroups(LinkManager.Instance.getRelatedGroupedLinks(sourceAnchor))}</div> </div> ); } diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx index fa6a2f506..d02a1c4eb 100644 --- a/src/client/views/linking/LinkMenuGroup.tsx +++ b/src/client/views/linking/LinkMenuGroup.tsx @@ -1,20 +1,20 @@ -import { observer } from "mobx-react"; -import { observable, action } from "mobx"; -import { Doc, StrListCast } from "../../../fields/Doc"; -import { Id } from "../../../fields/FieldSymbols"; -import { Cast } from "../../../fields/Types"; -import { LinkManager } from "../../util/LinkManager"; -import { DocumentView } from "../nodes/DocumentView"; +import { observer } from 'mobx-react'; +import { observable, action } from 'mobx'; +import { Doc, StrListCast } from '../../../fields/Doc'; +import { Id } from '../../../fields/FieldSymbols'; +import { Cast, DocCast } from '../../../fields/Types'; +import { LinkManager } from '../../util/LinkManager'; +import { DocumentView } from '../nodes/DocumentView'; import './LinkMenu.scss'; -import { LinkMenuItem } from "./LinkMenuItem"; -import React = require("react"); +import { LinkMenuItem } from './LinkMenuItem'; +import React = require('react'); +import { DocumentType } from '../../documents/DocumentTypes'; interface LinkMenuGroupProps { sourceDoc: Doc; group: Doc[]; groupType: string; - clearLinkEditor: () => void; - showEditor: (linkDoc: Doc) => void; + clearLinkEditor?: () => void; docView: DocumentView; itemHandler?: (doc: Doc) => void; } @@ -26,49 +26,60 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> { getBackgroundColor = (): string => { const linkRelationshipList = StrListCast(Doc.UserDoc().linkRelationshipList); const linkColorList = StrListCast(Doc.UserDoc().linkColorList); - let color = "white"; + let color = 'white'; // if this link's relationship property is not default "link", set its color if (linkRelationshipList) { const relationshipIndex = linkRelationshipList.indexOf(this.props.groupType); const RGBcolor: string = linkColorList[relationshipIndex]; if (RGBcolor) { //set opacity to 0.25 by modifiying the rgb string - color = RGBcolor.slice(0, RGBcolor.length - 1) + ", 0.25)"; + color = RGBcolor.slice(0, RGBcolor.length - 1) + ', 0.25)'; } } return color; - } + }; @observable _collapsed = false; render() { 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={linkDoc[Id]} + const sourceDoc = + this.props.docView.anchorViewDoc ?? + (this.props.docView.rootDoc.type === DocumentType.LINK // + ? this.props.docView.props.LayoutTemplateString?.includes('anchor1') + ? DocCast(linkDoc.anchor1) + : DocCast(linkDoc.anchor2) + : this.props.sourceDoc); + const destDoc = !sourceDoc + ? undefined + : this.props.docView.rootDoc.type === DocumentType.LINK + ? this.props.docView.props.LayoutTemplateString?.includes('anchor1') + ? DocCast(linkDoc.anchor2) + : DocCast(linkDoc.anchor1) + : LinkManager.getOppositeAnchor(linkDoc, sourceDoc) || LinkManager.getOppositeAnchor(linkDoc, Cast(linkDoc.anchor2, Doc, null).annotationOn === sourceDoc ? Cast(linkDoc.anchor2, Doc, null) : Cast(linkDoc.anchor1, Doc, null)); + return !destDoc || !sourceDoc ? null : ( + <LinkMenuItem + key={linkDoc[Id]} itemHandler={this.props.itemHandler} groupType={this.props.groupType} docView={this.props.docView} linkDoc={linkDoc} - sourceDoc={this.props.sourceDoc} - destinationDoc={destination} + sourceDoc={sourceDoc} + destinationDoc={destDoc} clearLinkEditor={this.props.clearLinkEditor} - showEditor={this.props.showEditor} - menuRef={this._menuRef} />; - } + menuRef={this._menuRef} + /> + ); }); return ( <div className="linkMenu-group" ref={this._menuRef}> - <div className="linkMenu-group-name" onClick={action(() => this._collapsed = !this._collapsed)} style={{ background: this.getBackgroundColor() }}> - <p className={this.props.groupType === "*" || this.props.groupType === "" ? "" : "expand-one"}> {this.props.groupType}:</p> + <div className="linkMenu-group-name" onClick={action(() => (this._collapsed = !this._collapsed))} style={{ background: this.getBackgroundColor() }}> + <p className={this.props.groupType === '*' || this.props.groupType === '' ? '' : 'expand-one'}> {this.props.groupType}:</p> </div> - {this._collapsed ? (null) : <div className="linkMenu-group-wrapper"> - {groupItems} - </div>} - </div > + {this._collapsed ? null : <div className="linkMenu-group-wrapper">{groupItems}</div>} + </div> ); } -}
\ No newline at end of file +} diff --git a/src/client/views/linking/LinkMenuItem.scss b/src/client/views/linking/LinkMenuItem.scss index 8333aa374..806a2c381 100644 --- a/src/client/views/linking/LinkMenuItem.scss +++ b/src/client/views/linking/LinkMenuItem.scss @@ -1,7 +1,7 @@ -@import "../global/globalCssVariables"; +@import '../global/globalCssVariables'; .linkMenu-item { - // border-top: 0.5px solid $medium-gray; + // border-top: 0.5px solid $medium-gray; position: relative; display: flex; border-top: 0.5px solid #cdcdcd; @@ -14,7 +14,6 @@ background-color: white; - .linkMenu-name { position: relative; width: auto; @@ -22,9 +21,7 @@ display: flex; .linkMenu-text { - - // padding: 4px 2px; - //display: inline; + width: 100%; .linkMenu-source-title { text-decoration: none; @@ -35,9 +32,7 @@ margin-left: 20px; } - .linkMenu-title-wrapper { - display: flex; align-items: center; min-height: 20px; @@ -59,7 +54,7 @@ .linkMenu-destination-title { text-decoration: none; - color: #4476F7; + color: #4476f7; font-size: 13px; line-height: 0.9; padding-bottom: 2px; @@ -84,7 +79,6 @@ font-size: 9px; line-height: 0.9; margin-left: 20px; - max-width: 125px; height: auto; white-space: break-spaces; } @@ -96,9 +90,7 @@ //overflow-wrap: break-word; user-select: none; } - } - } .linkMenu-item-content { @@ -114,15 +106,13 @@ } &:hover { - background-color: rgb(201, 239, 252); .linkMenu-item-buttons { - display: flex; + opacity: 1; } .linkMenu-item-content { - .linkMenu-destination-title { text-decoration: underline; color: rgb(60, 90, 156); @@ -135,11 +125,9 @@ .linkMenu-item-buttons { //@extend: right; - position: absolute; - top: 50%; - right: 0; - transform: translateY(-50%); - display: none; + position: relative; + display: flex; + opacity: 0; .button { width: 20px; @@ -172,4 +160,4 @@ cursor: pointer; } } -}
\ No newline at end of file +} diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx index 3f9db2612..91b63c1a6 100644 --- a/src/client/views/linking/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -1,17 +1,20 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; -import { action, observable } from 'mobx'; +import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import { Doc, DocListCast } from '../../../fields/Doc'; -import { Cast, StrCast } from '../../../fields/Types'; +import { Cast, DocCast, StrCast } from '../../../fields/Types'; import { WebField } from '../../../fields/URLField'; -import { emptyFunction, setupMoveUpEvents } from '../../../Utils'; +import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../Utils'; import { DocumentType } from '../../documents/DocumentTypes'; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager } from '../../util/DragManager'; import { LinkFollower } from '../../util/LinkFollower'; import { LinkManager } from '../../util/LinkManager'; +import { SelectionManager } from '../../util/SelectionManager'; +import { SettingsManager } from '../../util/SettingsManager'; +import { undoBatch } from '../../util/UndoManager'; import { DocumentView } from '../nodes/DocumentView'; import { LinkDocPreview } from '../nodes/LinkDocPreview'; import './LinkMenuItem.scss'; @@ -23,8 +26,7 @@ interface LinkMenuItemProps { docView: DocumentView; sourceDoc: Doc; destinationDoc: Doc; - clearLinkEditor: () => void; - showEditor: (linkDoc: Doc) => void; + clearLinkEditor?: () => void; menuRef: React.Ref<HTMLDivElement>; itemHandler?: (doc: Doc) => void; } @@ -68,7 +70,6 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { private _drag = React.createRef<HTMLDivElement>(); _editRef = React.createRef<HTMLDivElement>(); - _buttonRef = React.createRef<HTMLDivElement>(); @observable private _showMore: boolean = false; @action toggleShowMore(e: React.PointerEvent) { @@ -76,8 +77,18 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { this._showMore = !this._showMore; } - onEdit = (e: React.PointerEvent): void => { - LinkManager.currentLink = this.props.linkDoc; + @computed get sourceAnchor() { + const ldoc = this.props.linkDoc; + if (this.props.sourceDoc !== ldoc.anchor1 && this.props.sourceDoc !== ldoc.anchor2) { + if (Doc.AreProtosEqual(DocCast(DocCast(ldoc.anchor1).annotationOn), this.props.sourceDoc)) return DocCast(ldoc.anchor1); + if (Doc.AreProtosEqual(DocCast(DocCast(ldoc.anchor2).annotationOn), this.props.sourceDoc)) return DocCast(ldoc.anchor2); + } + return this.props.sourceDoc; + } + @action + onEdit = (e: React.PointerEvent) => { + LinkManager.currentLink = this.props.linkDoc === LinkManager.currentLink ? undefined : this.props.linkDoc; + LinkManager.currentLinkAnchor = this.sourceAnchor; setupMoveUpEvents( this, e, @@ -88,7 +99,12 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { return true; }, emptyFunction, - () => this.props.showEditor(this.props.linkDoc) + action(() => { + SelectionManager.SelectView(this.props.docView, false); + if ((SettingsManager.propertiesWidth ?? 0) < 100) { + SettingsManager.propertiesWidth = 250; + } + }) ); }; @@ -100,12 +116,12 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { const eleClone: any = this._drag.current!.cloneNode(true); eleClone.style.transform = `translate(${e.x}px, ${e.y}px)`; StartLinkTargetsDrag(eleClone, this.props.docView, e.x, e.y, this.props.sourceDoc, [this.props.linkDoc]); - this.props.clearLinkEditor(); + this.props.clearLinkEditor?.(); return true; }, emptyFunction, () => { - this.props.clearLinkEditor(); + this.props.clearLinkEditor?.(); if (this.props.itemHandler) { this.props.itemHandler?.(this.props.linkDoc); } else { @@ -116,21 +132,20 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { ? Cast(this.props.linkDoc.anchor12, Doc, null) : undefined; - if (focusDoc) this.props.docView.ComponentView?.scrollFocus?.(focusDoc, true); + if (focusDoc) this.props.docView.ComponentView?.scrollFocus?.(focusDoc, { instant: true }); LinkFollower.FollowLink(this.props.linkDoc, this.props.sourceDoc, this.props.docView.props, false); } } ); }; + deleteLink = (e: React.PointerEvent): void => setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => LinkManager.Instance.deleteLink(this.props.linkDoc)))); + render() { const destinationIcon = Doc.toIcon(this.props.destinationDoc) as any as IconProp; const title = StrCast(this.props.destinationDoc.title).length > 18 ? StrCast(this.props.destinationDoc.title).substr(0, 14) + '...' : this.props.destinationDoc.title; - // ... - // from anika to bob: here's where the text that is specifically linked would show up (linkDoc.storedText) - // ... const source = this.props.sourceDoc.type === DocumentType.RTF ? this.props.linkDoc.storedText @@ -141,24 +156,34 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { : undefined; return ( - <div className="linkMenu-item"> + <div className="linkMenu-item" style={{ background: LinkManager.currentLink === this.props.linkDoc ? 'lightBlue' : undefined }}> <div className={'linkMenu-item-content expand-two'}> <div ref={this._drag} className="linkMenu-name" //title="drag to view target. click to customize." - onPointerLeave={LinkDocPreview.Clear} - onPointerEnter={e => - this.props.linkDoc && - LinkDocPreview.SetLinkInfo({ - docProps: this.props.docView.props, - linkSrc: this.props.sourceDoc, - linkDoc: this.props.linkDoc, - showHeader: false, - location: [this._drag.current?.getBoundingClientRect().right ?? 100, this._drag.current?.getBoundingClientRect().top ?? e.clientY], - }) - } onPointerDown={this.onLinkButtonDown}> - <div className="linkMenu-text"> + <div className="linkMenu-item-buttons"> + <Tooltip title={<div className="dash-tooltip">Edit Link</div>}> + <div className="button" style={{ background: LinkManager.currentLink === this.props.linkDoc ? 'black' : 'gray' }} ref={this._editRef} onPointerDown={this.onEdit} onClick={e => e.stopPropagation()}> + <FontAwesomeIcon className="fa-icon" icon="edit" size="sm" /> + </div> + </Tooltip> + </div> + <div + className="linkMenu-text" + onPointerLeave={LinkDocPreview.Clear} + onPointerEnter={e => + this.props.linkDoc && + this.props.clearLinkEditor && + LinkDocPreview.SetLinkInfo({ + docProps: this.props.docView.props, + linkSrc: this.props.sourceDoc, + linkDoc: this.props.linkDoc, + showHeader: false, + location: [this._drag.current?.getBoundingClientRect().right ?? 100, this._drag.current?.getBoundingClientRect().top ?? e.clientY], + noPreview: false, + }) + }> {source ? ( <p className="linkMenu-source-title"> {' '} @@ -176,10 +201,10 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { {!this.props.linkDoc.description ? null : <p className="linkMenu-description">{StrCast(this.props.linkDoc.description)}</p>} </div> - <div className="linkMenu-item-buttons" ref={this._buttonRef}> - <Tooltip title={<div className="dash-tooltip">Edit Link</div>}> - <div className="button" ref={this._editRef} onPointerDown={this.onEdit} onClick={e => e.stopPropagation()}> - <FontAwesomeIcon className="fa-icon" icon="edit" size="sm" /> + <div className="linkMenu-item-buttons"> + <Tooltip title={<div className="dash-tooltip">Delete Link</div>}> + <div className="button" style={{ background: 'red' }} onPointerDown={this.deleteLink} onClick={e => e.stopPropagation()}> + <FontAwesomeIcon className="fa-icon" icon="trash" size="sm" /> </div> </Tooltip> </div> diff --git a/src/client/views/linking/LinkPopup.scss b/src/client/views/linking/LinkPopup.scss index 60c9ebfcd..b20ad9476 100644 --- a/src/client/views/linking/LinkPopup.scss +++ b/src/client/views/linking/LinkPopup.scss @@ -1,7 +1,7 @@ .linkPopup-container { background: white; box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23); - top: 35px; + top: 0; height: 200px; width: 200px; position: absolute; @@ -38,8 +38,7 @@ } } - .searchBox-container { background: pink; } -}
\ No newline at end of file +} diff --git a/src/client/views/linking/LinkPopup.tsx b/src/client/views/linking/LinkPopup.tsx index 0bcb68f82..7bdace2b6 100644 --- a/src/client/views/linking/LinkPopup.tsx +++ b/src/client/views/linking/LinkPopup.tsx @@ -11,10 +11,13 @@ import { SearchBox } from '../search/SearchBox'; import { DefaultStyleProvider } from '../StyleProvider'; import './LinkPopup.scss'; import React = require('react'); +import { OpenWhere } from '../nodes/DocumentView'; interface LinkPopupProps { showPopup: boolean; linkFrom?: () => Doc | undefined; + linkCreateAnchor?: () => Doc | undefined; + linkCreated?: (link: Doc) => void; // groupType: string; // linkDoc: Doc; // docView: DocumentView; @@ -32,14 +35,10 @@ export class LinkPopup extends React.Component<LinkPopupProps> { // TODO: should check for valid URL @undoBatch - makeLinkToURL = (target: string, lcoation: string) => { - ((this.view as any)?.TextView as FormattedTextBox).makeLinkAnchor(undefined, 'onRadd:rightight', target, target); - }; + makeLinkToURL = (target: string, lcoation: string) => ((this.view as any)?.TextView as FormattedTextBox).makeLinkAnchor(undefined, OpenWhere.addRight, target, target); @action - onLinkChange = (e: React.ChangeEvent<HTMLInputElement>) => { - this.linkURL = e.target.value; - }; + onLinkChange = (e: React.ChangeEvent<HTMLInputElement>) => (this.linkURL = e.target.value); getPWidth = () => 500; getPHeight = () => 500; @@ -67,7 +66,9 @@ export class LinkPopup extends React.Component<LinkPopupProps> { Document={Doc.MySearcher} DataDoc={Doc.MySearcher} linkFrom={linkDoc} + linkCreateAnchor={this.props.linkCreateAnchor} linkSearch={true} + linkCreated={this.props.linkCreated} fieldKey="data" dropAction="move" isSelected={returnTrue} diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index 497b4993c..d95668c89 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -70,6 +70,12 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @observable _muted: boolean = false; @observable _paused: boolean = false; // is recording paused // @observable rawDuration: number = 0; // computed from the length of the audio element when loaded + + constructor(props: any) { + super(props); + this.props.setContentView?.(this); + } + @computed get recordingStart() { return DateCast(this.dataDoc[this.fieldKey + '-recordingStart'])?.date.getTime(); } @@ -111,8 +117,6 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @action componentDidMount() { - this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link. - if (this.path) { this.mediaState = media_state.Paused; this.setPlayheadTime(NumCast(this.layoutDoc.clipStart)); diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 260c98816..f8ef87fb1 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -16,7 +16,7 @@ import { CollectionFreeFormView } from '../collections/collectionFreeForm/Collec import { DocComponent } from '../DocComponent'; import { StyleProp } from '../StyleProvider'; import './CollectionFreeFormDocumentView.scss'; -import { DocumentView, DocumentViewProps } from './DocumentView'; +import { DocumentView, DocumentViewProps, OpenWhere } from './DocumentView'; import React = require('react'); export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { @@ -24,7 +24,6 @@ export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { sizeProvider?: (doc: Doc, replica: string) => { width: number; height: number } | undefined; renderCutoffProvider: (doc: Doc) => boolean; zIndex?: number; - highlight?: boolean; rotation: number; dataTransition?: string; replica: string; @@ -75,9 +74,6 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF get Color() { return this.dataProvider?.color ?? Cast(this.Document._color, 'string', null); } - get Highlight() { - return this.dataProvider?.highlight; - } @computed get dataProvider() { return this.props.dataProvider?.(this.props.Document, this.props.replica); } @@ -119,45 +115,30 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF }); } - public static updateKeyframe(docs: Doc[], time: number, targetDoc?: Doc) { + public static updateKeyframe(timer: NodeJS.Timeout | undefined, docs: Doc[], time: number, targetDoc?: Doc) { + if (timer) clearTimeout(timer); + const newTimer = DocumentView.SetViewTransition(docs, 'all', 1000, undefined, true); const timecode = Math.round(time); - docs.forEach( - action(doc => { - doc._viewTransition = doc.dataTransition = 'all 1s'; - CollectionFreeFormDocumentView.animFields.forEach(val => { - const findexed = Cast(doc[`${val.key}-indexed`], listSpec('number'), null); - findexed?.length <= timecode + 1 && findexed.push(undefined as any as number); - }); - CollectionFreeFormDocumentView.animStringFields.forEach(val => { - const findexed = Cast(doc[`${val}-indexed`], listSpec('string'), null); - findexed?.length <= timecode + 1 && findexed.push(undefined as any as string); - }); - CollectionFreeFormDocumentView.animDataFields(doc).forEach(val => { - const findexed = Cast(doc[`${val}-indexed`], listSpec(InkField), null); - findexed?.length <= timecode + 1 && findexed.push(undefined as any); - }); - }) - ); - setTimeout( - () => - docs.forEach(doc => { - doc._viewTransition = undefined; - doc.dataTransition = 'inherit'; - }), - 1010 - ); + docs.forEach(doc => { + CollectionFreeFormDocumentView.animFields.forEach(val => { + const findexed = Cast(doc[`${val.key}-indexed`], listSpec('number'), null); + findexed?.length <= timecode + 1 && findexed.push(undefined as any as number); + }); + CollectionFreeFormDocumentView.animStringFields.forEach(val => { + const findexed = Cast(doc[`${val}-indexed`], listSpec('string'), null); + findexed?.length <= timecode + 1 && findexed.push(undefined as any as string); + }); + CollectionFreeFormDocumentView.animDataFields(doc).forEach(val => { + const findexed = Cast(doc[`${val}-indexed`], listSpec(InkField), null); + findexed?.length <= timecode + 1 && findexed.push(undefined as any); + }); + }); + return newTimer; } - public static gotoKeyframe(docs: Doc[], duration = 1000) { - docs.forEach(doc => (doc._viewTransition = doc.dataTransition = `all ${duration}ms`)); - setTimeout( - () => - docs.forEach(doc => { - doc._viewTransition = undefined; - doc.dataTransition = 'inherit'; - }), - 1010 - ); + public static gotoKeyframe(timer: NodeJS.Timeout | undefined, docs: Doc[], duration = 1000) { + if (timer) clearTimeout(timer); + return DocumentView.SetViewTransition(docs, 'all', duration, undefined, true); } public static setupZoom(doc: Doc, targDoc: Doc) { @@ -203,7 +184,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF topDoc.x = spt[0]; topDoc.y = spt[1]; this.props.removeDocument?.(topDoc); - this.props.addDocTab(topDoc, 'inParent'); + this.props.addDocTab(topDoc, OpenWhere.inParentFromScreen); } else { const spt = this.screenToLocalTransform().inverse().transformPoint(0, 0); const fpt = screenXf.transformPoint(spt[0], spt[1]); @@ -238,7 +219,6 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF <div className={'collectionFreeFormDocumentView-container'} style={{ - outline: this.Highlight ? 'orange solid 2px' : '', width: this.panelWidth(), height: this.panelHeight(), transform: this.transform, diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index d74da9748..dd03b9b99 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -103,7 +103,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl <> <DocumentView ref={r => { - whichDoc !== targetDoc && r?.focus(whichDoc, {}); + whichDoc !== targetDoc && r?.focus(whichDoc, { instant: true }); }} {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight']).omit} isContentActive={returnFalse} diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 6d6609317..569579996 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -1,4 +1,4 @@ -import { computed } from 'mobx'; +import { computed, trace } from 'mobx'; import { observer } from 'mobx-react'; import { AclPrivate, Doc, Opt } from '../../../fields/Doc'; import { ScriptField } from '../../../fields/ScriptField'; @@ -59,7 +59,7 @@ class ObserverJsxParser1 extends JsxParser { } } -const ObserverJsxParser: typeof JsxParser = ObserverJsxParser1 as any; +export const ObserverJsxParser: typeof JsxParser = ObserverJsxParser1 as any; interface HTMLtagProps { Document: Doc; diff --git a/src/client/views/nodes/DocumentLinksButton.scss b/src/client/views/nodes/DocumentLinksButton.scss index 0f3eb14bc..6da0b73ba 100644 --- a/src/client/views/nodes/DocumentLinksButton.scss +++ b/src/client/views/nodes/DocumentLinksButton.scss @@ -1,8 +1,9 @@ -@import "../global/globalCssVariables.scss"; +@import '../global/globalCssVariables.scss'; .documentLinksButton-wrapper { transform-origin: top left; width: 100%; + height: 100%; } .documentLinksButton-menu { @@ -21,6 +22,16 @@ position: absolute; } +.documentLinksButton-showCount { + position: absolute; + border-radius: 50%; + opacity: 0.9; + pointer-events: auto; + display: flex; + align-items: center; + background-color: $light-blue; + color: black; +} .documentLinksButton, .documentLinksButton-endLink, .documentLinksButton-startLink { @@ -34,6 +45,7 @@ text-transform: uppercase; letter-spacing: 2px; font-size: 10px; + transform-origin: top left; transition: transform 0.2s; text-align: center; display: flex; @@ -46,13 +58,10 @@ cursor: pointer; } } - .documentLinksButton { background-color: $dark-gray; color: $white; font-weight: bold; - width: 80%; - height: 80%; font-size: 100%; font-family: 'Roboto'; transition: 0.2s ease all; @@ -61,31 +70,20 @@ background-color: $black; } } - .documentLinksButton.startLink { background-color: $medium-blue; + width: 75%; + height: 75%; color: $white; font-weight: bold; - width: 80%; - height: 80%; font-size: 100%; transition: 0.2s ease all; - - &:hover { - background-color: $black; - } } .documentLinksButton-endLink { border: $medium-blue 2px dashed; color: $medium-blue; background-color: none !important; - width: 80%; - height: 80%; font-size: 100%; transition: 0.2s ease all; - - &:hover { - background-color: $light-blue; - } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index 9ffbf8e37..6f3152981 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -4,19 +4,18 @@ import { action, computed, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { Doc, Opt } from '../../../fields/Doc'; import { StrCast } from '../../../fields/Types'; -import { TraceMobx } from '../../../fields/util'; import { emptyFunction, returnFalse, setupMoveUpEvents, StopEvent } from '../../../Utils'; import { DocUtils } from '../../documents/Documents'; import { DragManager } from '../../util/DragManager'; import { Hypothesis } from '../../util/HypothesisUtils'; import { LinkManager } from '../../util/LinkManager'; import { undoBatch, UndoManager } from '../../util/UndoManager'; -import { Colors } from '../global/globalEnums'; import './DocumentLinksButton.scss'; import { DocumentView } from './DocumentView'; import { LinkDescriptionPopup } from './LinkDescriptionPopup'; import { TaskCompletionBox } from './TaskCompletedBox'; import React = require('react'); +import _ = require('lodash'); const higflyout = require('@hig/flyout'); export const { anchorPoints } = higflyout; @@ -24,10 +23,12 @@ export const Flyout = higflyout.default; interface DocumentLinksButtonProps { View: DocumentView; - Offset?: (number | undefined)[]; + Bottom?: boolean; AlwaysOn?: boolean; InMenu?: boolean; + OnHover?: boolean; StartLink?: boolean; //whether the link HAS been started (i.e. now needs to be completed) + ShowCount?: boolean; scaling?: () => number; // how uch doc is scaled so that link buttons can invert it } @observer @@ -39,9 +40,6 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp @observable public static AnnotationUri: string | undefined; @observable public static LinkEditorDocView: DocumentView | undefined; - @observable public static invisibleWebDoc: Opt<Doc>; - public static invisibleWebRef = React.createRef<HTMLDivElement>(); - @action @undoBatch onLinkButtonMoved = (e: PointerEvent) => { @@ -119,7 +117,6 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp DocumentLinksButton.StartLink = this.props.View.props.Document; DocumentLinksButton.StartLinkView = this.props.View; } - //action(() => Doc.BrushDoc(this.props.View.Document)); } }; @@ -256,28 +253,32 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp * todo:glr / anh seperate functionality such as onClick onPointerDown of link menu button */ @computed get linkButtonInner() { - const btnDim = '30px'; - const link = <img style={{ width: '22px', height: '16px' }} src={`/assets/${'link.png'}`} />; + const btnDim = 30; const isActive = DocumentLinksButton.StartLink === this.props.View.props.Document && this.props.StartLink; - return !this.props.InMenu ? ( - <div className="documentLinksButton-cont" style={{ left: this.props.Offset?.[0], top: this.props.Offset?.[1], right: this.props.Offset?.[2], bottom: this.props.Offset?.[3] }}> - <div - className={'documentLinksButton'} - onPointerDown={this.onLinkMenuOpen} - onClick={this.onLinkClick} - style={{ - backgroundColor: Colors.LIGHT_BLUE, - color: Colors.BLACK, - fontSize: '20px', - width: btnDim, - height: btnDim, - }}> - {Array.from(this.filteredLinks).length} - </div> + const scaling = Math.min(1, this.props.scaling?.() || 1); + const showLinkCount = (onHover?: boolean, offset?: boolean) => ( + <div + className="documentLinksButton-showCount" + onPointerDown={this.onLinkMenuOpen} + style={{ + fontSize: (onHover ? btnDim / 2 : 20) * scaling, + width: (onHover ? btnDim / 2 : btnDim) * scaling, + height: (onHover ? btnDim / 2 : btnDim) * scaling, + bottom: offset ? 5 * scaling : onHover ? (-btnDim / 2) * scaling : undefined, + }}> + <span style={{ width: '100%', display: 'inline-block', textAlign: 'center' }}>{Array.from(this.filteredLinks).length}</span> </div> + ); + return this.props.ShowCount ? ( + showLinkCount(this.props.OnHover, this.props.Bottom) ) : ( <div className="documentLinksButton-menu"> - {this.props.InMenu && !this.props.StartLink && DocumentLinksButton.StartLink !== this.props.View.props.Document ? ( //if the origin node is not this node + {this.props.StartLink ? ( //if link has been started from current node, then set behavior of link button to deactivate linking when clicked again + <div className={`documentLinksButton ${isActive ? `startLink` : ``}`} ref={this._linkButton} onPointerDown={isActive ? StopEvent : this.onLinkButtonDown} onClick={isActive ? this.clearLinks : this.onLinkClick}> + <FontAwesomeIcon className="documentdecorations-icon" icon="link" /> + </div> + ) : null} + {!this.props.StartLink && DocumentLinksButton.StartLink !== this.props.View.props.Document ? ( //if the origin node is not this node <div className={'documentLinksButton-endLink'} ref={this._linkButton} @@ -286,34 +287,25 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp <FontAwesomeIcon className="documentdecorations-icon" icon="link" /> </div> ) : null} - {this.props.InMenu && this.props.StartLink ? ( //if link has been started from current node, then set behavior of link button to deactivate linking when clicked again - <div className={`documentLinksButton ${isActive ? `startLink` : ``}`} ref={this._linkButton} onPointerDown={isActive ? StopEvent : this.onLinkButtonDown} onClick={isActive ? this.clearLinks : this.onLinkClick}> - <FontAwesomeIcon className="documentdecorations-icon" icon="link" /> - </div> - ) : null} </div> ); } render() { - TraceMobx(); - const menuTitle = this.props.StartLink ? 'Drag or tap to start link' : 'Tap to complete link'; const buttonTitle = 'Tap to view links; double tap to open link collection'; - const title = this.props.InMenu ? menuTitle : buttonTitle; + const title = this.props.ShowCount ? buttonTitle : menuTitle; //render circular tooltip if it isn't set to invisible and show the number of doc links the node has, and render inner-menu link button for starting/stopping links if currently in menu return !Array.from(this.filteredLinks).length && !this.props.AlwaysOn ? null : ( <div className="documentLinksButton-wrapper" style={{ - transform: `scale(${this.props.scaling?.() || 1})`, + position: this.props.InMenu ? 'relative' : 'absolute', + top: 0, + pointerEvents: 'none', }}> - {(this.props.InMenu && (DocumentLinksButton.StartLink || this.props.StartLink)) || (!DocumentLinksButton.LinkEditorDocView && !this.props.InMenu) ? ( - <Tooltip title={<div className="dash-tooltip">{title}</div>}>{this.linkButtonInner}</Tooltip> - ) : ( - this.linkButtonInner - )} + <Tooltip title={<div className="dash-tooltip">{title}</div>}>{this.linkButtonInner}</Tooltip> </div> ); } diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index 6cadeec41..453bdac8e 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -28,7 +28,7 @@ transition: outline 0.3s linear; // background: $white; //overflow: hidden; - transform-origin: left top; + transform-origin: center; &.minimized { width: 30px; @@ -51,6 +51,22 @@ height: calc(100% - 20px); } + .documentView-htmlOverlay { + position: absolute; + display: flex; + top: 0; + height: 100%; + width: 100%; + .documentView-htmlOverlayInner { + box-shadow: black 0.2vw 0.2vw 0.8vw; + background: rgb(255, 255, 255); + overflow: auto; + position: relative; + margin: auto; + padding: 20px; + } + } + .documentView-linkAnchorBoxAnchor { display: flex; overflow: hidden; @@ -198,9 +214,9 @@ > .documentView-titleWrapper-hover { display: inline-block; } - } - - > .documentView-styleWrapper { + // > .documentView-contentsView { + // opacity: 0.5; + // } > .documentView-captionWrapper { opacity: 1; } diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 5b26469ed..1df46488b 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -3,7 +3,9 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; -import { AclAdmin, AclEdit, AclPrivate, DataSym, Doc, DocListCast, Field, Opt, StrListCast, WidthSym } from '../../../fields/Doc'; +import { ObserverJsxParser } from './DocumentContentsView'; +import { Bounce, Fade, Flip, LightSpeed, Roll, Rotate, Zoom } from 'react-reveal'; +import { AclAdmin, AclEdit, AclPrivate, AnimationSym, DataSym, Doc, DocListCast, Field, Opt, StrListCast, WidthSym } from '../../../fields/Doc'; import { Document } from '../../../fields/documentSchemas'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; @@ -11,7 +13,7 @@ import { List } from '../../../fields/List'; import { ObjectField } from '../../../fields/ObjectField'; import { listSpec } from '../../../fields/Schema'; import { ScriptField } from '../../../fields/ScriptField'; -import { BoolCast, Cast, ImageCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; +import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { AudioField } from '../../../fields/URLField'; import { GetEffectiveAcl, SharingPermissions, TraceMobx } from '../../../fields/util'; import { MobileInterface } from '../../../mobile/MobileInterface'; @@ -51,9 +53,9 @@ import { LinkAnchorBox } from './LinkAnchorBox'; import { LinkDocPreview } from './LinkDocPreview'; import { RadialMenu } from './RadialMenu'; import { ScriptingBox } from './ScriptingBox'; -import { PresBox } from './trails/PresBox'; +import { PresEffect, PresEffectDirection } from './trails'; +import { PinProps } from './trails/PresBox'; import React = require('react'); -import { CollectionTreeView } from '../collections/CollectionTreeView'; const { Howl } = require('howler'); interface Window { @@ -70,15 +72,49 @@ export enum ViewAdjustment { doNothing = 0, } +export enum OpenWhere { + inPlace = 'inPlace', + lightbox = 'lightbox', + add = 'add', + addLeft = 'add:left', + addRight = 'add:right', + addBottom = 'add:bottom', + dashboard = 'dashboard', + close = 'close', + fullScreen = 'fullScreen', + toggle = 'toggle', + replace = 'replace', + replaceRight = 'replace:right', + replaceLeft = 'replace:left', + inParent = 'inParent', + inParentFromScreen = 'inParentFromScreen', +} +export enum OpenWhereMod { + none = '', + left = 'left', + right = 'right', + top = 'top', + bottom = 'bottom', +} + export const ViewSpecPrefix = 'viewSpec'; // field prefix for anchor fields that are immediately copied over to the target document when link is followed. Other anchor properties will be copied over in the specific setViewSpec() method on their view (which allows for seting preview values instead of writing to the document) export interface DocFocusOptions { originalTarget?: Doc; // set in JumpToDocument, used by TabDocView to determine whether to fit contents to tab - willZoom?: boolean; // determines whether to zoom in on target document - scale?: number; // percent of containing frame to zoom into document + willPan?: boolean; // determines whether to pan to target document + willPanZoom?: boolean; // determines whether to zoom in on target document + zoomScale?: number; // percent of containing frame to zoom into document + zoomTime?: number; afterFocus?: DocAfterFocusFunc; // function to call after focusing on a document docTransform?: Transform; // when a document can't be panned and zoomed within its own container (say a group), then we need to continue to move up the render hierarchy to find something that can pan and zoom. when this happens the docTransform must accumulate all the transforms of each level of the hierarchy instant?: boolean; // whether focus should happen instantly (as opposed to smooth zoom) + effect?: Doc; // animation effect for focus + noSelect?: boolean; // whether target should be selected after focusing + playAudio?: boolean; // whether to play audio annotation on focus + zoomTextSelections?: boolean; // whether to display a zoomed overlay of anchor text selections + toggleTarget?: boolean; // whether to toggle target on and off + originatingDoc?: Doc; // document that triggered the focus + easeFunc?: 'linear' | 'ease'; // transition method for scrolling } export type DocAfterFocusFunc = (notFocused: boolean) => Promise<ViewAdjustment>; export type DocFocusFunc = (doc: Doc, options: DocFocusOptions) => void; @@ -86,7 +122,7 @@ export type StyleProviderFunc = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, p export interface DocComponentView { updateIcon?: () => void; // updates the icon representation of the document getAnchor?: () => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box) - scrollFocus?: (doc: Doc, smooth: boolean) => Opt<number>; // returns the duration of the focus + scrollFocus?: (doc: Doc, options: DocFocusOptions) => Opt<number>; // returns the duration of the focus brushView?: (view: { width: number; height: number; panX: number; panY: number }) => void; setViewSpec?: (anchor: Doc, preview: boolean) => void; // sets viewing information for a componentview, typically when following a link. 'preview' tells the view to use the values without writing to the document reverseNativeScaling?: () => boolean; // DocumentView's setup screenToLocal based on the doc having a nativeWidth/Height. However, some content views (e.g., FreeFormView w/ fitContentsToBox set) may ignore the native dimensions so this flags the DocumentView to not do Nativre scaling. @@ -99,6 +135,7 @@ export interface DocComponentView { Pause?: () => void; setFocus?: () => void; componentUI?: (boundsLeft: number, boundsTop: number) => JSX.Element | null; + incrementalRendering?: () => void; fieldKey?: string; annotationKey?: string; getTitle?: () => string; @@ -140,14 +177,15 @@ export interface DocumentViewSharedProps { showTitle?: () => string; whenChildContentsActiveChanged: (isActive: boolean) => void; rootSelected: (outsideReaction?: boolean) => boolean; // whether the root of a template has been selected - addDocTab: (doc: Doc, where: string) => boolean; + addDocTab: (doc: Doc, where: OpenWhere) => boolean; filterAddDocument?: (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) addDocument?: (doc: Doc | Doc[]) => boolean; removeDocument?: (doc: Doc | Doc[]) => boolean; moveDocument?: (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean; - pinToPres: (document: Doc) => void; + pinToPres: (document: Doc, pinProps: PinProps) => void; ScreenToLocalTransform: () => Transform; bringToFront: (doc: Doc, sendToBack?: boolean) => void; + canEmbedOnDrag?: boolean; xPadding?: number; yPadding?: number; dropAction?: dropActionType; @@ -167,7 +205,8 @@ export interface DocumentViewSharedProps { // these props are specific to DocuentViews export interface DocumentViewProps extends DocumentViewSharedProps { // properties specific to DocumentViews but not to FieldView - hideResizeHandles?: boolean; // whether to suppress DocumentDecorations when this document is selected + hideDecorations?: boolean; // whether to suppress all DocumentDecorations when doc is selected + hideResizeHandles?: boolean; // whether to suppress resized handles on doc decorations when this document is selected hideTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings hideDecorationTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings hideDocumentButtonBar?: boolean; @@ -199,6 +238,7 @@ export interface DocumentViewInternalProps extends DocumentViewProps { NativeWidth: () => number; NativeHeight: () => number; isSelected: (outsideReaction?: boolean) => boolean; + isHovering: () => boolean; select: (ctrlPressed: boolean) => void; DocumentView: () => DocumentView; viewPath: () => DocumentView[]; @@ -207,9 +247,8 @@ export interface DocumentViewInternalProps extends DocumentViewProps { @observer export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps>() { public static SelectAfterContextMenu = true; // whether a document should be selected after it's contextmenu is triggered. - _animateScaleTime = 300; // milliseconds; - @observable _animateScalingTo = 0; - @observable _pendingDoubleClick = false; + private _cursorTimer: NodeJS.Timeout | undefined; + private _longPress = false; private _disposers: { [name: string]: IReactionDisposer } = {}; private _downX: number = 0; private _downY: number = 0; @@ -223,11 +262,19 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps private _dropDisposer?: DragManager.DragDropDisposer; private _holdDisposer?: InteractionUtils.MultiTouchEventDisposer; protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; + @observable _componentView: Opt<DocComponentView>; // needs to be accessed from DocumentView wrapper class + @observable _animateScaleTime: Opt<number>; // milliseconds for animating between views. defaults to 300 if not uset + @observable _animateScalingTo = 0; + @observable _pendingDoubleClick = false; + @observable _cursorPress = false; private get topMost() { return this.props.renderDepth === 0 && !LightboxView.LightboxDoc; } + public get animateScaleTime() { + return this._animateScaleTime ?? 300; + } public get displayName() { return 'DocumentView(' + this.props.Document.title + ')'; } // this makes mobx trace() statements more descriptive @@ -258,9 +305,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @computed get borderRounding() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding); } - @computed get hideLinkButton() { - return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HideLinkButton + (this.props.isSelected() ? ':selected' : '')); - } @computed get widgetDecorations() { return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Decorations + (this.props.isSelected() ? ':selected' : '')); } @@ -475,7 +519,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps 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 }), "add:right"), 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 }), OpenWhere.addRight), icon: "map-pin", selected: -1 }); const effectiveAcl = GetEffectiveAcl(this.props.Document[DataSym]); (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) && RadialMenu.Instance.addItem({ @@ -486,8 +530,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps icon: 'external-link-square-alt', 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 in a new tab", event: () => this.props.addDocTab(this.props.Document, OpenWhere.addRight), 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 }); SelectionManager.DeselectAll(); @@ -507,6 +551,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps dragData.treeViewDoc = this.props.treeViewDoc; dragData.removeDocument = this.props.removeDocument; dragData.moveDocument = this.props.moveDocument; + dragData.canEmbed = this.props.canEmbedOnDrag; //dragData.dimSource : // dragEffects field, set dim // add kv pairs to a doc, swap properties with the node while dragging, and then swap when dropping @@ -527,7 +572,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps e.preventDefault(); if (e.key === '†' || e.key === 't') { if (!StrCast(this.layoutDoc._showTitle)) this.layoutDoc._showTitle = 'title'; - if (!this._titleRef.current) setTimeout(() => this._titleRef.current?.setIsFocused(true), 0); + if (!this._titleRef.current) setTimeout(() => this._titleRef.current?.setIsFocused(true)); else if (!this._titleRef.current.setIsFocused(true)) { // if focus didn't change, focus on interior text... this._titleRef.current?.setIsFocused(false); @@ -542,16 +587,21 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps // copying over VIEW fields immediately allows the view type to switch to create the right _componentView Array.from(Object.keys(Doc.GetProto(anchor))) .filter(key => key.startsWith(ViewSpecPrefix)) - .forEach(spec => { - this.layoutDoc[spec.replace(ViewSpecPrefix, '')] = (field => (field instanceof ObjectField ? ObjectField.MakeCopy(field) : field))(anchor[spec]); - }); - // after a timeout, the right _componentView should have been created, so call it to update its view spec values + .forEach(spec => (this.layoutDoc[spec.replace(ViewSpecPrefix, '')] = (field => (field instanceof ObjectField ? ObjectField.MakeCopy(field) : field))(anchor[spec]))); + // after a render the general viewSpec should have created the right _componentView, so after a timeout, call the componentview to update its specific view specs setTimeout(() => this._componentView?.setViewSpec?.(anchor, LinkDocPreview.LinkInfo ? true : false)); - const focusSpeed = this._componentView?.scrollFocus?.(anchor, options?.instant === false || !LinkDocPreview.LinkInfo); // bcz: smooth parameter should really be passed into focus() instead of inferred here + const focusSpeed = this._componentView?.scrollFocus?.(anchor, { ...options, instant: options?.instant || LinkDocPreview.LinkInfo ? true : false }); const endFocus = focusSpeed === undefined ? options?.afterFocus : async (moved: boolean) => options?.afterFocus?.(true) ?? ViewAdjustment.doNothing; + const startTime = Date.now(); this.props.focus(options?.docTransform ? anchor : this.rootDoc, { ...options, - afterFocus: (didFocus: boolean) => new Promise<ViewAdjustment>(res => setTimeout(async () => res(endFocus ? await endFocus(didFocus || focusSpeed !== undefined) : ViewAdjustment.doNothing), focusSpeed ?? 0)), + afterFocus: (didFocus: boolean) => + new Promise<ViewAdjustment>(async res => + setTimeout( + async () => res(endFocus ? await endFocus(didFocus || focusSpeed !== undefined) : ViewAdjustment.doNothing), // + didFocus ? Math.max(0, (options.zoomTime ?? 500) - (Date.now() - startTime)) : 0 + ) + ), }); }; onClick = action((e: React.MouseEvent | React.PointerEvent) => { @@ -588,11 +638,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps ); UndoManager.RunInBatch(() => (func().result?.select === true ? this.props.select(false) : ''), 'on double click'); } else if (!Doc.IsSystem(this.rootDoc) && !this.rootDoc.isLinkButton) { - UndoManager.RunInBatch(() => LightboxView.AddDocTab(this.rootDoc, 'lightbox', this.props.LayoutTemplate?.(), this.props.addDocTab), 'double tap'); + UndoManager.RunInBatch(() => LightboxView.AddDocTab(this.rootDoc, OpenWhere.lightbox, this.props.LayoutTemplate?.(), this.props.addDocTab), 'double tap'); SelectionManager.DeselectAll(); Doc.UnBrushDoc(this.props.Document); } - } else if (this.onClickHandler?.script && !isScriptBox()) { + } else if (!this._longPress && this.onClickHandler?.script && !isScriptBox()) { // bcz: hack? don't execute script if you're clicking on a scripting box itself const { clientX, clientY, shiftKey, altKey } = e; const func = () => @@ -621,11 +671,12 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps clickFunc(); }, 350); } else clickFunc(); - } else if (this.allLinks && this.Document.type !== DocumentType.LINK && !isScriptBox() && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) { + } else if (!this._longPress && this.allLinks.length && this.Document.type !== DocumentType.LINK && !isScriptBox() && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) { + SelectionManager.DeselectAll(); this.allLinks.length && LinkFollower.FollowLink(undefined, this.props.Document, this.props, 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 + // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTemplateForField 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 { runInAction(() => (this._pendingDoubleClick = true)); @@ -656,6 +707,13 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } return; } + this._cursorTimer = setTimeout( + action(() => { + this._cursorPress = true; + this.props.select(false); + }), + 1000 // long press required duration + ); this._downX = e.clientX; this._downY = e.clientY; if ((Doc.ActiveTool === InkTool.None || this.props.addDocTab === returnFalse) && !(this.props.Document.rootDocument && !(e.ctrlKey || e.button > 0))) { @@ -681,6 +739,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } }; + @action onPointerMove = (e: PointerEvent): void => { if (e.cancelBubble) return; if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || [InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) return; @@ -690,7 +749,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps if (!e.altKey && (!this.topMost || this.layoutDoc.onDragStart || this.onClickHandler) && (e.buttons === 1 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE))) { document.removeEventListener('pointermove', this.onPointerMove); document.removeEventListener('pointerup', this.onPointerUp); - this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && 'alias') || ((this.props.dropAction || this.Document.dropAction || undefined) as dropActionType)); + this._cursorTimer && clearTimeout(this._cursorTimer); + this._cursorPress = false; + this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && 'alias') || ((this.Document.dropAction || this.props.dropAction || undefined) as dropActionType)); } } e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers @@ -704,8 +765,12 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps document.removeEventListener('pointerup', this.onPointerUp); }; + @action onPointerUp = (e: PointerEvent): void => { this.cleanupPointerEvents(); + this._longPress = this._cursorPress; + this._cursorTimer && clearTimeout(this._cursorTimer); + this._cursorPress = false; if (this.onPointerUpHandler?.script && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) { this.onPointerUpHandler.script.run({ self: this.rootDoc, this: this.layoutDoc }, console.log); @@ -721,8 +786,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps toggleFollowLink = (location: Opt<string>, zoom?: boolean, setPushpin?: boolean): void => { this.Document.ignoreClick = false; if (setPushpin) { - this.Document.isPushpin = !this.Document.isPushpin; - this.Document._isLinkButton = this.Document.isPushpin || this.Document._isLinkButton; + this.Document.followLinkToggle = !this.Document.followLinkToggle; + this.Document._isLinkButton = this.Document.followLinkToggle || this.Document._isLinkButton; } else { this.Document._isLinkButton = !this.Document._isLinkButton; } @@ -739,14 +804,14 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps toggleTargetOnClick = (): void => { this.Document.ignoreClick = false; this.Document._isLinkButton = true; - this.Document.isPushpin = true; + this.Document.followLinkToggle = true; }; @undoBatch @action followLinkOnClick = (location: Opt<string>, zoom: boolean): void => { this.Document.ignoreClick = false; this.Document._isLinkButton = true; - this.Document.isPushpin = false; + this.Document.followLinkToggle = false; this.Document.followLinkZoom = zoom; this.Document.followLinkLocation = location; }; @@ -755,7 +820,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps selectOnClick = (): void => { this.Document.ignoreClick = false; this.Document._isLinkButton = false; - this.Document.isPushpin = false; + this.Document.followLinkToggle = false; this.Document.onClick = this.layoutDoc.onClick = undefined; }; @undoBatch @@ -803,7 +868,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const portal = Docs.Create.FreeformDocument([], { _width: NumCast(this.layoutDoc._width) + 10, _height: NumCast(this.layoutDoc._height), _fitWidth: true, title: StrCast(this.props.Document.title) + ' [Portal]' }); DocUtils.MakeLink({ doc: this.props.Document }, { doc: portal }, 'portal to:portal from'); } - this.Document.followLinkLocation = 'inPlace'; + this.Document.followLinkLocation = OpenWhere.inPlace; this.Document.followLinkZoom = true; this.Document._isLinkButton = true; }; @@ -834,14 +899,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps if (!cm || (e as any)?.nativeEvent?.SchemaHandled) return; if (e && !(e.nativeEvent as any).dash) { - const onDisplay = () => - setTimeout(() => { - DocumentViewInternal.SelectAfterContextMenu && !this.props.isSelected(true) && SelectionManager.SelectView(this.props.DocumentView(), false); // on a mac, the context menu is triggered on mouse down, but a YouTube video becaomes interactive when selected which means that the context menu won't show up. by delaying the selection until hopefully after the pointer up, the context menu will appear. - setTimeout(() => { - const ele = document.elementFromPoint(e.clientX, e.clientY); - simulateMouseClick(ele, e.clientX, e.clientY, e.screenX, e.screenY); - }); - }); + const onDisplay = () => { + DocumentViewInternal.SelectAfterContextMenu && !this.props.isSelected(true) && SelectionManager.SelectView(this.props.DocumentView(), false); // on a mac, the context menu is triggered on mouse down, but a YouTube video becaomes interactive when selected which means that the context menu won't show up. by delaying the selection until hopefully after the pointer up, the context menu will appear. + setTimeout(() => simulateMouseClick(document.elementFromPoint(e.clientX, e.clientY), e.clientX, e.clientY, e.screenX, e.screenY)); + }; if (navigator.userAgent.includes('Macintosh')) { cm.displayMenu((e?.pageX || pageX || 0) - 15, (e?.pageY || pageY || 0) - 15, undefined, undefined, onDisplay); } else { @@ -862,7 +923,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps 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 : []; - !Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template ', event: () => this.props.addDocTab(templateDoc, 'add:right'), icon: 'eye' }); + !Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template ', event: () => this.props.addDocTab(templateDoc, OpenWhere.addRight), icon: 'eye' }); !Doc.noviceMode && appearanceItems.push({ description: 'Add a Field', @@ -915,10 +976,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'compass' }); 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' }); + onClicks.push({ description: this.Document.isLinkButton ? 'Remove Follow Behavior' : 'Follow Link in Place', event: () => this.toggleFollowLink('inPlace', false, 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: (this.Document.followLinkToggle ? '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) { @@ -962,8 +1023,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } const help = cm.findByDescription('Help...'); const helpItems: ContextMenuProps[] = help && 'subitems' in help ? help.subitems : []; - helpItems.push({ description: 'Show Metadata', event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), 'add:right'), icon: 'layer-group' }); - !Doc.noviceMode && helpItems.push({ description: 'Text Shortcuts Ctrl+/', event: () => this.props.addDocTab(Docs.Create.PdfDocument('/assets/cheat-sheet.pdf', { _width: 300, _height: 300 }), 'add:right'), icon: 'keyboard' }); + helpItems.push({ description: 'Show Metadata', event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), OpenWhere.addRight), icon: 'layer-group' }); + !Doc.noviceMode && helpItems.push({ description: 'Text Shortcuts Ctrl+/', event: () => this.props.addDocTab(Docs.Create.PdfDocument('/assets/cheat-sheet.pdf', { _width: 300, _height: 300 }), OpenWhere.addRight), icon: 'keyboard' }); !Doc.noviceMode && helpItems.push({ description: 'Print Document in Console', event: () => console.log(this.props.Document), icon: 'hand-point-right' }); !Doc.noviceMode && helpItems.push({ description: 'Print DataDoc in Console', event: () => console.log(this.props.Document[DataSym]), icon: 'hand-point-right' }); @@ -1046,34 +1107,41 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps }; @observable _retryThumb = 1; thumbShown = () => { + const childHighlighted = () => + Array.from(Doc.highlightedDocs.keys()) + .concat(Array.from(Doc.brushManager.BrushedDoc.keys())) + .some(doc => Doc.AreProtosEqual(DocCast(doc.annotationOn), this.rootDoc)); + const childOverlayed = () => Array.from(DocumentManager._overlayViews).some(view => Doc.AreProtosEqual(view.rootDoc, this.rootDoc)); return !this.props.isSelected() && LightboxView.LightboxDoc !== this.rootDoc && this.thumb && !Doc.AreProtosEqual(DocumentLinksButton.StartLink, this.rootDoc) && - (!Doc.isBrushedHighlightedDegree(this.props.Document) || this.rootDoc._viewType === CollectionViewType.Docking) && + ((!childHighlighted() && !childOverlayed() && !Doc.isBrushedHighlightedDegree(this.rootDoc)) || this.rootDoc._viewType === CollectionViewType.Docking) && !this._componentView?.isAnyChildContentActive?.() ? true : false; }; - linkButtonInverseScaling = () => (this.props.NativeDimScaling?.() || 1) * this.props.DocumentView().screenToLocalTransform().Scale; - @computed get contents() { - TraceMobx(); + get audioAnnoState() { + return this.dataDoc.audioAnnoState ?? 'stopped'; + } + @computed get audioAnnoView() { const audioAnnosCount = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations'], listSpec(AudioField), null)?.length; const audioTextAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations-text'], listSpec('string'), null); - const audioView = - (!this.props.isSelected() && !this._isHovering && this.dataDoc.audioAnnoState !== 2) || this.props.renderDepth === -1 || SnappingManager.GetIsDragging() || (!audioAnnosCount && !this.dataDoc.audioAnnoState) ? null : ( - <Tooltip title={<div>{audioTextAnnos?.lastElement()}</div>}> - <div className="documentView-audioBackground" onPointerDown={this.playAnnotation}> - <FontAwesomeIcon - className="documentView-audioFont" - style={{ color: [audioAnnosCount ? 'blue' : 'gray', 'green', 'red'][NumCast(this.dataDoc.audioAnnoState)] }} - icon={!audioAnnosCount ? 'microphone' : 'file-audio'} - size="sm" - /> - </div> - </Tooltip> - ); - + const audioIconColors = new Map<string, string>([ + ['recording', 'red'], + ['playing', 'green'], + ['stopped', audioAnnosCount ? 'blue' : 'gray'], + ]); + return this.props.renderDepth === -1 || SnappingManager.GetIsDragging() || (!this.props.isSelected() && !this.props.isHovering() && this.audioAnnoState !== 'recording') || (!audioAnnosCount && this.audioAnnoState === 'stopped') ? null : ( + <Tooltip title={<div>{audioTextAnnos?.lastElement()}</div>}> + <div className="documentView-audioBackground" onPointerDown={this.playAnnotation}> + <FontAwesomeIcon className="documentView-audioFont" style={{ color: audioIconColors.get(StrCast(this.audioAnnoState)) }} icon={!audioAnnosCount ? 'microphone' : 'file-audio'} size="sm" /> + </div> + </Tooltip> + ); + } + @computed get contents() { + TraceMobx(); return ( <div className="documentView-contentsView" @@ -1095,10 +1163,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps width={this.props.PanelWidth()} height={this.props.PanelHeight()} onError={(e: any) => { - setTimeout( - action(() => (this._retryThumb = 0)), - 0 - ); + setTimeout(action(() => (this._retryThumb = 0))); setTimeout( action(() => (this._retryThumb = 1)), 150 @@ -1111,7 +1176,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps {...this.props} docViewPath={this.props.viewPath} thumbShown={this.thumbShown} - isHovering={this.isHovering} + isHovering={this.props.isHovering} setContentView={this.setContentView} NativeDimScaling={this.props.NativeDimScaling} PanelHeight={this.panelHeight} @@ -1124,14 +1189,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps layoutKey={this.finalLayoutKey} /> {this.layoutDoc.hideAllLinks ? null : this.allLinkEndpoints} - {(!this.props.isSelected() && !this._isHovering) || this.hideLinkButton || this.props.renderDepth === -1 || SnappingManager.GetIsDragging() ? null : ( - <DocumentLinksButton - View={this.props.DocumentView()} - scaling={this.linkButtonInverseScaling} - Offset={[this.topMost ? 0 : !this.props.isSelected() ? -15 : -36, undefined, undefined, this.topMost ? 10 : !this.props.isSelected() ? -15 : -32]} - /> - )} - {audioView} + {this.audioAnnoView} </div> ); } @@ -1147,16 +1205,13 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps anchorPanelWidth = () => this.props.PanelWidth() || 1; anchorPanelHeight = () => this.props.PanelHeight() || 1; anchorStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any => { - switch (property) { - case StyleProp.ShowTitle: - return ''; - case StyleProp.PointerEvents: - return 'none'; - case StyleProp.LinkSource: - return this.props.Document; // pass the LinkSource to the LinkAnchorBox - default: - return this.props.styleProvider?.(doc, props, property); + // prettier-ignore + switch (property.split(':')[0]) { + case StyleProp.ShowTitle: return ''; + case StyleProp.PointerEvents: return 'none'; + case StyleProp.LinkSource: return this.props.Document; // pass the LinkSource to the LinkAnchorBox } + return this.props.styleProvider?.(doc, props, property); }; // We need to use allrelatedLinks to get not just links to the document as a whole, but links to // anchors that are not rendered as DocumentViews (marked as 'unrendered' with their 'annotationOn' set to this document). e.g., @@ -1183,7 +1238,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps TraceMobx(); if (this.layoutDoc.unrendered || this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return null; if (this.rootDoc.type === DocumentType.PRES || this.rootDoc.type === DocumentType.LINK || this.props.dontRegisterView) return null; - const filtered = DocUtils.FilterDocs(this.directLinks, this.props.docFilters?.() ?? [], []).filter(d => !d.hidden); + const filtered = DocUtils.FilterDocs(this.directLinks, this.props.docFilters?.() ?? [], []).filter(d => d.linkDisplay); return filtered.map((link, i) => ( <div className="documentView-anchorCont" key={link[Id]}> <DocumentView @@ -1210,7 +1265,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const self = this; const audioAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations'], listSpec(AudioField), null); const anno = audioAnnos.lastElement(); - if (anno instanceof AudioField && this.dataDoc.audioAnnoState === 0) { + if (anno instanceof AudioField && this.audioAnnoState === 'stopped') { new Howl({ src: [anno.url.href], format: ['mp3'], @@ -1218,16 +1273,14 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps loop: false, volume: 0.5, onend: function () { - runInAction(() => { - self.dataDoc.audioAnnoState = 0; - }); + runInAction(() => (self.dataDoc.audioAnnoState = 'stopped')); }, }); - this.dataDoc.audioAnnoState = 1; + this.dataDoc.audioAnnoState = 'playing'; } }; - static recordAudioAnnotation(dataDoc: Doc, field: string, onEnd?: () => void) { + static recordAudioAnnotation(dataDoc: Doc, field: string, onRecording?: (stop: () => void) => void, onEnd?: () => void) { let gumStream: any; let recorder: any; navigator.mediaDevices @@ -1262,14 +1315,16 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } } }; - runInAction(() => (dataDoc.audioAnnoState = 2)); + runInAction(() => (dataDoc.audioAnnoState = 'recording')); recorder.start(); - setTimeout(() => { + const stopFunc = () => { recorder.stop(); DictationManager.Controls.stop(false); - runInAction(() => (dataDoc.audioAnnoState = 0)); + runInAction(() => (dataDoc.audioAnnoState = 'stopped')); gumStream.getAudioTracks()[0].stop(); - }, 5000); + }; + if (onRecording) onRecording(stopFunc); + else setTimeout(stopFunc, 5000); }); } @@ -1329,7 +1384,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps .join('\\')} display={'block'} fontSize={10} - GetValue={() => (showTitle.split(';').length === 1 ? showTitle + '=' + Field.toString(targetDoc[showTitle.split(';')[0]] as any as Field) : '#' + showTitle)} + GetValue={() => { + this.props.select(false); + return showTitle.split(';').length === 1 ? showTitle + '=' + Field.toString(targetDoc[showTitle.split(';')[0]] as any as Field) : '#' + showTitle; + }} SetValue={undoBatch((input: string) => { if (input?.startsWith('#')) { if (this.props.showTitle) { @@ -1360,10 +1418,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps </div> ); } - isHovering = () => this._isHovering; - @observable _isHovering = false; @observable _: string = ''; - _hoverTimeout: any = undefined; renderDoc = (style: object) => { TraceMobx(); const thumb = ImageCast(this.layoutDoc['thumb-frozen'], ImageCast(this.layoutDoc.thumb))?.url?.href.replace('.png', '_m.png'); @@ -1374,27 +1429,16 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps <div className={`documentView-node${this.topMost ? '-topmost' : ''}`} id={this.props.Document[Id]} - onPointerEnter={action(() => { - clearTimeout(this._hoverTimeout); - this._isHovering = true; - })} - onPointerLeave={action(() => { - clearTimeout(this._hoverTimeout); - this._hoverTimeout = setTimeout( - action(() => (this._isHovering = false)), - 500 - ); - })} style={{ ...style, background: isButton || thumb ? undefined : this.backgroundColor, opacity: this.opacity, - cursor: Doc.ActiveTool === InkTool.None ? 'grab' : 'crosshair', + cursor: this._cursorPress ? 'wait' : Doc.ActiveTool === InkTool.None ? 'grab' : 'crosshair', color: StrCast(this.layoutDoc.color, 'inherit'), fontFamily: StrCast(this.Document._fontFamily, 'inherit'), fontSize: Cast(this.Document._fontSize, 'string', null), transform: this._animateScalingTo ? `scale(${this._animateScalingTo})` : undefined, - transition: !this._animateScalingTo ? StrCast(this.Document.dataTransition) : `transform ${this._animateScaleTime / 1000}s ease-${this._animateScalingTo < 1 ? 'in' : 'out'}`, + transition: !this._animateScalingTo ? StrCast(this.Document.dataTransition) : `transform ${this.animateScaleTime / 1000}s ease-${this._animateScalingTo < 1 ? 'in' : 'out'}`, }}> {this.innards} {this.onClickHandler && this.props.ContainingCollectionView?.props.Document._viewType === CollectionViewType.Time ? <div className="documentView-contentBlocker" /> : null} @@ -1403,25 +1447,55 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps ) ); }; + + /** + * returns an entrance animation effect function to wrap a JSX element + * @param presEffectDoc presentation effects document that specifies the animation effect parameters + * @returns a function that will wrap a JSX animation element wrapping any JSX element + */ + public static AnimationEffect(renderDoc: JSX.Element, presEffectDoc: Opt<Doc>, root: Doc) { + const dir = presEffectDoc?.presEffectDirection ?? presEffectDoc?.linkAnimDirection; + const effectProps = { + left: dir === PresEffectDirection.Left, + right: dir === PresEffectDirection.Right, + top: dir === PresEffectDirection.Top, + bottom: dir === PresEffectDirection.Bottom, + opposite: true, + delay: 0, + duration: Cast(presEffectDoc?.presTransition, 'number', null), + }; + //prettier-ignore + switch (StrCast(presEffectDoc?.presEffect, StrCast(presEffectDoc?.followLinkAnimEffect))) { + default: + case PresEffect.None: return renderDoc; + case PresEffect.Zoom: return <Zoom {...effectProps}>{renderDoc}</Zoom>; + case PresEffect.Fade: return <Fade {...effectProps}>{renderDoc}</Fade>; + case PresEffect.Flip: return <Flip {...effectProps}>{renderDoc}</Flip>; + case PresEffect.Rotate: return <Rotate {...effectProps}>{renderDoc}</Rotate>; + case PresEffect.Bounce: return <Bounce {...effectProps}>{renderDoc}</Bounce>; + case PresEffect.Roll: return <Roll {...effectProps}>{renderDoc}</Roll>; + case PresEffect.Lightspeed: return <LightSpeed {...effectProps}>{renderDoc}</LightSpeed>; + } + } render() { TraceMobx(); const highlighting = this.props.styleProvider?.(this.props.Document, this.props, StyleProp.Highlighting); const borderPath = this.props.styleProvider?.(this.props.Document, this.props, StyleProp.BorderPath) || { path: undefined }; const boxShadow = this.props.treeViewDoc || !highlighting - ? null + ? this.boxShadow : highlighting && this.borderRounding && highlighting.highlightStyle !== 'dashed' ? `0 0 0 ${highlighting.highlightIndex}px ${highlighting.highlightColor}` : this.boxShadow || (this.props.Document.isTemplateForField ? 'black 0.2vw 0.2vw 0.8vw' : undefined); const renderDoc = this.renderDoc({ borderRadius: this.borderRounding, - outline: highlighting && !this.borderRounding && highlighting ? `${highlighting.highlightColor} ${highlighting.highlightStyle} ${highlighting.highlightIndex}px` : 'solid 0px', - border: highlighting && this.borderRounding && highlighting && highlighting.highlightStyle === 'dashed' ? `${highlighting.highlightStyle} ${highlighting.highlightColor} ${highlighting.highlightIndex}px` : undefined, + outline: highlighting && !this.borderRounding && !highlighting.highlightStroke ? `${highlighting.highlightColor} ${highlighting.highlightStyle} ${highlighting.highlightIndex}px` : 'solid 0px', + border: highlighting && this.borderRounding && highlighting.highlightStyle === 'dashed' ? `${highlighting.highlightStyle} ${highlighting.highlightColor} ${highlighting.highlightIndex}px` : undefined, boxShadow, clipPath: borderPath.path ? `path('${borderPath.path}')` : undefined, }); - const animRenderDoc = PresBox.Instance?.isActiveItemTarget(this.layoutDoc) ? PresBox.AnimationEffect(renderDoc, PresBox.Instance.activeItem) : renderDoc; + const animRenderDoc = DocumentViewInternal.AnimationEffect(renderDoc, this.rootDoc[AnimationSym], this.rootDoc); return ( <div className={`${DocumentView.ROOT_DIV} docView-hack`} @@ -1474,7 +1548,37 @@ export class DocumentView extends React.Component<DocumentViewProps> { return 'DocumentView(' + this.props.Document?.title + ')'; } // this makes mobx trace() statements more descriptive public ContentRef = React.createRef<HTMLDivElement>(); + public ViewTimer: NodeJS.Timeout | undefined; // timer for res private _disposers: { [name: string]: IReactionDisposer } = {}; + public clearViewTransition = () => { + this.ViewTimer && clearTimeout(this.ViewTimer); + this.rootDoc._viewTransition = undefined; + }; + public setViewTransition = (transProp: string, timeInMs: number, afterTrans?: () => void, dataTrans = false) => { + this.rootDoc._viewTransition = `${transProp} ${timeInMs}ms`; + if (dataTrans) this.rootDoc._dataTransition = `${transProp} ${timeInMs}ms`; + this.ViewTimer && clearTimeout(this.ViewTimer); + return (this.ViewTimer = setTimeout(() => { + this.rootDoc._viewTransition = undefined; + this.rootDoc._dataTransition = 'inherit'; + afterTrans?.(); + }, timeInMs + 10)); + }; + public static SetViewTransition(docs: Doc[], transProp: string, timeInMs: number, afterTrans?: () => void, dataTrans = false) { + docs.forEach(doc => { + doc._viewTransition = `${transProp} ${timeInMs}ms`; + dataTrans && (doc.dataTransition = `${transProp} ${timeInMs}ms`); + }); + return setTimeout( + () => + docs.forEach(doc => { + doc._viewTransition = undefined; + dataTrans && (doc.dataTransition = 'inherit'); + afterTrans?.(); + }), + timeInMs + 10 + ); + } public static showBackLinks(linkSource: Doc) { const docid = Doc.CurrentUserEmail + Doc.GetProto(linkSource)[Id] + '-pivotish'; @@ -1535,6 +1639,16 @@ export class DocumentView extends React.Component<DocumentViewProps> { return this.props.fitWidth?.(this.rootDoc) || this.layoutDoc.fitWidth; } + @computed get hideLinkButton() { + return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HideLinkButton + (this.isSelected() ? ':selected' : '')); + } + linkButtonInverseScaling = () => (this.props.NativeDimScaling?.() || 1) * this.screenToLocalTransform().Scale; + + @computed get linkCountView() { + const hideCount = this.props.renderDepth === -1 || SnappingManager.GetIsDragging() || (this.isSelected() && this.props.renderDepth) || !this._isHovering || this.hideLinkButton; + return hideCount ? null : <DocumentLinksButton View={this} scaling={this.linkButtonInverseScaling} OnHover={true} Bottom={this.topMost} ShowCount={true} />; + } + @computed get docViewPath(): DocumentView[] { return this.props.docViewPath ? [...this.props.docViewPath(), this] : [this]; } @@ -1576,7 +1690,7 @@ export class DocumentView extends React.Component<DocumentViewProps> { return this.props.PanelHeight(); } @computed get Xshift() { - return this.effectiveNativeWidth ? (this.props.PanelWidth() - this.effectiveNativeWidth * this.nativeScaling) / 2 : 0; + return this.effectiveNativeWidth ? Math.max(0, (this.props.PanelWidth() - this.effectiveNativeWidth * this.nativeScaling) / 2) : 0; } @computed get Yshift() { return this.effectiveNativeWidth && this.effectiveNativeHeight && Math.abs(this.Xshift) < 0.001 && (!this.layoutDoc.nativeHeightUnfrozen || (!this.fitWidth && this.effectiveNativeHeight * this.nativeScaling <= this.props.PanelHeight())) @@ -1593,10 +1707,13 @@ export class DocumentView extends React.Component<DocumentViewProps> { toggleNativeDimensions = () => this.docView && Doc.toggleNativeDimensions(this.layoutDoc, this.docView.NativeDimScaling, this.props.PanelWidth(), this.props.PanelHeight()); focus = (doc: Doc, options: DocFocusOptions) => this.docView?.focus(doc, options); getBounds = () => { - if (!this.docView || !this.docView.ContentDiv || this.props.Document.presBox || this.props.treeViewDoc || Doc.AreProtosEqual(this.props.Document, Doc.UserDoc())) { + if (!this.docView || !this.docView.ContentDiv || this.props.Document.type === DocumentType.PRES || this.props.treeViewDoc || Doc.AreProtosEqual(this.props.Document, Doc.UserDoc())) { return undefined; } - const xf = this.docView?.props.ScreenToLocalTransform().scale(this.nativeScaling).inverse(); + const xf = this.docView?.props + .ScreenToLocalTransform() + .scale(this.trueNativeWidth() ? this.nativeScaling : 1) + .inverse(); const [[left, top], [right, bottom]] = [xf.transformPoint(0, 0), xf.transformPoint(this.panelWidth, this.panelHeight)]; if (this.docView.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) { const docuBox = this.docView.ContentDiv.getElementsByClassName('linkAnchorBox-cont'); @@ -1605,15 +1722,21 @@ export class DocumentView extends React.Component<DocumentViewProps> { return { left, top, right, bottom, center: this.ComponentView?.getCenter?.(xf) }; }; - public iconify(finished?: () => void) { + public iconify(finished?: () => void, animateTime?: number) { this.ComponentView?.updateIcon?.(); + const animTime = this.docView?._animateScaleTime; + runInAction(() => this.docView && animateTime !== undefined && (this.docView._animateScaleTime = animateTime)); + const finalFinished = action(() => { + finished?.(); + this.docView && (this.docView._animateScaleTime = animTime); + }); const layoutKey = Cast(this.Document.layoutKey, 'string', null); if (layoutKey !== 'layout_icon') { - this.switchViews(true, 'icon', finished); + this.switchViews(true, 'icon', finalFinished); if (layoutKey && layoutKey !== 'layout' && layoutKey !== 'layout_icon') this.Document.deiconifyLayout = layoutKey.replace('layout_', ''); } else { const deiconifyLayout = Cast(this.Document.deiconifyLayout, 'string', null); - this.switchViews(deiconifyLayout ? true : false, deiconifyLayout, finished); + this.switchViews(deiconifyLayout ? true : false, deiconifyLayout, finalFinished); this.Document.deiconifyLayout = undefined; this.props.bringToFront(this.rootDoc); } @@ -1639,15 +1762,19 @@ export class DocumentView extends React.Component<DocumentViewProps> { this.docView && (this.docView._animateScalingTo = 0); finished?.(); }), - this.docView!._animateScaleTime - 10 + this.docView ? Math.max(0, this.docView.animateScaleTime - 10) : 0 ); }), - this.docView!._animateScaleTime - 10 + this.docView ? Math.max(0, this.docView?.animateScaleTime - 10) : 0 ); }); startDragging = (x: number, y: number, dropAction: dropActionType, hideSource = false) => this.docView?.startDragging(x, y, dropAction, hideSource); + @observable textHtmlOverlay: Opt<string>; + @computed get anchorViewDoc() { + return this.props.LayoutTemplateString?.includes('anchor2') ? DocCast(this.rootDoc['anchor2']) : this.props.LayoutTemplateString?.includes('anchor1') ? DocCast(this.rootDoc['anchor1']) : undefined; + } docViewPathFunc = () => this.docViewPath; isSelected = (outsideReaction?: boolean) => SelectionManager.IsSelected(this, outsideReaction); select = (extendSelection: boolean) => SelectionManager.SelectView(this, !SelectionManager.Views().some(v => v.props.Document === this.props.ContainingCollectionDoc) && extendSelection); @@ -1657,11 +1784,12 @@ export class DocumentView extends React.Component<DocumentViewProps> { PanelHeight = () => this.panelHeight; NativeDimScaling = () => this.nativeScaling; selfView = () => this; + trueNativeWidth = () => returnVal(this.props.NativeWidth?.(), Doc.NativeWidth(this.layoutDoc, this.props.DataDoc, false)); screenToLocalTransform = () => this.props .ScreenToLocalTransform() .translate(-this.centeringX, -this.centeringY) - .scale(1 / this.nativeScaling); + .scale(this.trueNativeWidth() ? 1 / this.nativeScaling : 1); componentDidMount() { this._disposers.reactionScript = reaction( () => ScriptCast(this.rootDoc.reactionScript)?.script?.run({ this: this.props.Document, self: Cast(this.rootDoc, Doc, null) || this.props.Document }).result, @@ -1680,14 +1808,48 @@ export class DocumentView extends React.Component<DocumentViewProps> { Object.values(this._disposers).forEach(disposer => disposer?.()); !BoolCast(this.props.Document.dontRegisterView, this.props.dontRegisterView) && DocumentManager.Instance.RemoveView(this); } + _hoverTimeout: any = undefined; + isHovering = () => this._isHovering; + @observable _isHovering = false; + + htmlOverlayEffect = ''; + @computed get htmlOverlay() { + const effectProps = { + delay: 0, + duration: 500, + }; + const highlight = ( + <div className="webBox-textHighlight"> + <ObserverJsxParser autoCloseVoidElements={true} key={42} onError={(e: any) => console.log('PARSE error', e)} renderInWrapper={false} jsx={StrCast(this.textHtmlOverlay)} /> + </div> + ); + return !this.textHtmlOverlay ? null : ( + <div className="documentView-htmlOverlay"> + <div className="documentView-htmlOverlayInner">{<Fade {...effectProps}>{DocumentViewInternal.AnimationEffect(highlight, { presEffect: this.htmlOverlayEffect ?? 'Zoom' } as any as Doc, this.rootDoc)} </Fade>}</div> + </div> + ); + } render() { TraceMobx(); const xshift = Math.abs(this.Xshift) <= 0.001 ? this.props.PanelWidth() : undefined; const yshift = Math.abs(this.Yshift) <= 0.001 ? this.props.PanelHeight() : undefined; const isButton = this.props.Document.type === DocumentType.FONTICON || this.props.Document._viewType === CollectionViewType.Linear; + return ( - <div className="contentFittingDocumentView"> + <div + className="contentFittingDocumentView" + onPointerEnter={action(() => { + clearTimeout(this._hoverTimeout); + this._isHovering = true; + })} + onPointerLeave={action(() => { + clearTimeout(this._hoverTimeout); + this._hoverTimeout = setTimeout( + action(() => (this._isHovering = false)), + 500 + ); + })}> {!this.props.Document || !this.props.PanelWidth() ? null : ( <div className="contentFittingDocumentView-previewDoc" @@ -1712,12 +1874,16 @@ export class DocumentView extends React.Component<DocumentViewProps> { NativeDimScaling={this.NativeDimScaling} isSelected={this.isSelected} select={this.select} + isHovering={this.isHovering} ScreenToLocalTransform={this.screenToLocalTransform} focus={this.props.focus || emptyFunction} ref={action((r: DocumentViewInternal | null) => r && (this.docView = r))} /> + {this.htmlOverlay} </div> )} + + {this.linkCountView} </div> ); } diff --git a/src/client/views/nodes/FilterBox.tsx b/src/client/views/nodes/FilterBox.tsx index 04d252abe..78ba499ec 100644 --- a/src/client/views/nodes/FilterBox.tsx +++ b/src/client/views/nodes/FilterBox.tsx @@ -33,7 +33,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() { constructor(props: Readonly<FieldViewProps>) { super(props); const targetDoc = FilterBox.targetDoc; - if (targetDoc && !targetDoc.currentFilter) targetDoc.currentFilter = FilterBox.createFilterDoc(); + if (targetDoc && !targetDoc.currentFilter) runInAction(() => (targetDoc.currentFilter = FilterBox.createFilterDoc())); } /// creates a new, empty filter doc diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx index e09155ac2..24562ccbd 100644 --- a/src/client/views/nodes/FunctionPlotBox.tsx +++ b/src/client/views/nodes/FunctionPlotBox.tsx @@ -13,6 +13,7 @@ import { Docs } from '../../documents/Documents'; import { DragManager } from '../../util/DragManager'; import { undoBatch } from '../../util/UndoManager'; import { ViewBoxBaseComponent } from '../DocComponent'; +import { DocFocusOptions } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; const EquationSchema = createSchema({}); @@ -47,7 +48,7 @@ export class FunctionPlotBox extends ViewBoxBaseComponent<FieldViewProps>() { return anchor; }; @action - scrollFocus = (doc: Doc, smooth: boolean) => { + scrollFocus = (doc: Doc, smooth: DocFocusOptions) => { this.dataDoc.xRange = new List<number>(Array.from(Cast(doc.xRange, listSpec('number'), Cast(this.dataDoc.xRange, listSpec('number'), [-10, 10])))); this.dataDoc.yRange = new List<number>(Array.from(Cast(doc.yRange, listSpec('number'), Cast(this.dataDoc.xRange, listSpec('number'), [-1, 9])))); return 0; diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index cd2b23f02..6359a9491 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -5,7 +5,6 @@ position: relative; transform-origin: top left; - .imageBox-annotationLayer { position: absolute; transform-origin: left top; @@ -95,7 +94,6 @@ height: 100%; } - .imageBox-fader { position: relative; width: 100%; @@ -103,7 +101,8 @@ display: flex; height: 100%; - .imageBox-fadeBlocker, .imageBox-fadeBlocker-hover{ + .imageBox-fadeBlocker, + .imageBox-fadeBlocker-hover { width: 100%; height: 100%; position: absolute; @@ -126,17 +125,6 @@ left: 0; } -.imageBox-fadeBlocker { - -webkit-transition: opacity 1s ease-in-out; - -moz-transition: opacity 1s ease-in-out; - -o-transition: opacity 1s ease-in-out; - transition: opacity 1s ease-in-out; -} - .imageBox-fadeBlocker-hover { - -webkit-transition: opacity 1s ease-in-out; - -moz-transition: opacity 1s ease-in-out; - -o-transition: opacity 1s ease-in-out; - transition: opacity 1s ease-in-out; opacity: 0; -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 461d6984d..ac953d13b 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -8,13 +8,13 @@ import { List } from '../../../fields/List'; import { ObjectField } from '../../../fields/ObjectField'; import { createSchema } from '../../../fields/Schema'; import { ComputedField } from '../../../fields/ScriptField'; -import { Cast, NumCast, StrCast } from '../../../fields/Types'; +import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../Utils'; +import { emptyFunction, OmitKeys, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../Utils'; import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices'; -import { DocUtils } from '../../documents/Documents'; +import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; import { Networking } from '../../Network'; import { DragManager } from '../../util/DragManager'; @@ -30,6 +30,8 @@ import { FaceRectangles } from './FaceRectangles'; import { FieldView, FieldViewProps } from './FieldView'; import './ImageBox.scss'; import React = require('react'); +import { PresBox } from './trails'; +import { DocFocusOptions, DocumentViewProps } from './DocumentView'; export const pageSchema = createSchema({ googlePhotosUrl: 'string', @@ -54,24 +56,53 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @observable _curSuffix = ''; @observable _uploadIcon = uploadIcons.idle; + constructor(props: any) { + super(props); + this.props.setContentView?.(this); + } + protected createDropTarget = (ele: HTMLDivElement) => { this._dropDisposer?.(); ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.props.Document)); }; - setViewSpec = (anchor: Doc, preview: boolean) => {}; // sets viewing information for a componentview, typically when following a link. 'preview' tells the view to use the values without writing to the document + + @action + scrollFocus = (anchor: Doc, options: DocFocusOptions) => { + const focusSpeed = options.instant ? 0 : options.zoomTime ?? 500; + return PresBox.restoreTargetDocView( + this.props.DocumentView?.(), // + { pinDocLayout: BoolCast(anchor.presPinDocLayout) }, + anchor, + focusSpeed, + !anchor.presPinData + ? {} + : { + pannable: true, + dataannos: anchor.presAnnotations !== undefined, + dataview: true, + } + ) + ? focusSpeed + : undefined; + }; // sets viewing information for a componentview, typically when following a link. 'preview' tells the view to use the values without writing to the document getAnchor = () => { - const anchor = this._getAnchor?.(this._savedAnnotations); - anchor && this.addDocument(anchor); - return anchor ?? this.rootDoc; + const anchor = + this._getAnchor?.(this._savedAnnotations) ?? // use marquee anchor, otherwise, save zoom/pan as anchor + Docs.Create.ImageanchorDocument({ presTransition: 1000, unrendered: true, annotationOn: this.rootDoc }); + if (anchor) { + PresBox.pinDocView(anchor, { pinData: { pannable: true, dataview: true, dataannos: true } }, this.rootDoc); + this.addDocument(anchor); + return anchor; + } + return this.rootDoc; }; componentDidMount() { - this.props.setContentView?.(this); // bcz: do not remove this. without it, stepping into an image in the lightbox causes an infinite loop.... this._disposers.sizer = reaction( () => ({ forceFull: this.props.renderDepth < 1 || this.layoutDoc._showFullRes, - scrSize: this.props.ScreenToLocalTransform().inverse().transformDirection(this.nativeSize.nativeWidth, this.nativeSize.nativeHeight)[0] / this.nativeSize.nativeWidth, + scrSize: (this.props.ScreenToLocalTransform().inverse().transformDirection(this.nativeSize.nativeWidth, this.nativeSize.nativeHeight)[0] / this.nativeSize.nativeWidth) * NumCast(this.rootDoc._viewScale, 1), selected: this.props.isSelected(), }), ({ forceFull, scrSize, selected }) => (this._curSuffix = selected ? '_o' : this.fieldKey === 'icon' ? '_m' : forceFull ? '_o' : scrSize < 0.25 ? '_s' : scrSize < 0.5 ? '_m' : scrSize < 0.8 ? '_l' : '_o'), @@ -120,6 +151,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @undoBatch resolution = () => (this.layoutDoc._showFullRes = !this.layoutDoc._showFullRes); + @undoBatch + setUseAlt = () => (this.layoutDoc[this.fieldKey + '-useAlt'] = !this.layoutDoc[this.fieldKey + '-useAlt']); @undoBatch rotate = action(() => { @@ -137,10 +170,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp crop = (region: Doc | undefined, addCrop?: boolean) => { if (!region) return; const cropping = Doc.MakeCopy(region, true); - Doc.GetProto(region).backgroundColor = 'transparent'; Doc.GetProto(region).lockedPosition = true; Doc.GetProto(region).title = 'region:' + this.rootDoc.title; - Doc.GetProto(region).isPushpin = true; + Doc.GetProto(region).followLinkToggle = true; this.addDocument(region); const anchx = NumCast(cropping.x); const anchy = NumCast(cropping.y); @@ -183,6 +215,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp const funcs: ContextMenuProps[] = []; funcs.push({ description: 'Rotate Clockwise 90', event: this.rotate, icon: 'expand-arrows-alt' }); funcs.push({ description: `Show ${this.layoutDoc._showFullRes ? 'Dynamic Res' : 'Full Res'}`, event: this.resolution, icon: 'expand-arrows-alt' }); + funcs.push({ description: `${this.layoutDoc[this.fieldKey + '-useAlt'] ? 'Show Alternate' : 'Show Primary'}`, event: this.setUseAlt, icon: 'expand-arrows-alt' }); funcs.push({ description: 'Copy path', event: () => Utils.CopyText(this.choosePath(field.url)), icon: 'expand-arrows-alt' }); if (!Doc.noviceMode) { funcs.push({ description: 'Export to Google Photos', event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: 'caret-square-right' }); @@ -281,9 +314,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp setTimeout( action(() => { this._uploadIcon = idle; - if (data) { - dataDoc[this.fieldKey] = data; - } + data && (dataDoc[this.fieldKey] = data); }), 2000 ); @@ -315,7 +346,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp TraceMobx(); const srcpath = this.paths[0]; - const fadepath = this.paths[Math.min(1, this.paths.length - 1)]; + const fadepath = this.paths.lastElement(); const { nativeWidth, nativeHeight, nativeOrientation } = this.nativeSize; const rotation = NumCast(this.dataDoc[this.fieldKey + '-rotation']); const aspect = rotation % 180 ? nativeHeight / nativeWidth : 1; @@ -336,8 +367,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp <div className="imageBox-fader" style={{ overflow: Array.from(this.props.docViewPath?.()).slice(-1)[0].fitWidth ? 'auto' : undefined }}> <img key="paths" src={srcpath} style={{ transform, transformOrigin }} draggable={false} width={nativeWidth} /> {fadepath === srcpath ? null : ( - <div className={`imageBox-fadeBlocker${this.props.isHovering?.() ? '-hover' : ''}`}> - <img className="imageBox-fadeaway" key={'fadeaway'} src={fadepath} style={{ transform, transformOrigin }} draggable={false} width={nativeWidth} /> + <div + className={`imageBox-fadeBlocker${(this.props.isHovering?.() && this.layoutDoc[this.fieldKey + '-useAlt'] === undefined) || BoolCast(this.layoutDoc[this.fieldKey + '-useAlt']) ? '-hover' : ''}`} + style={{ transition: StrCast(this.layoutDoc.viewTransition, 'opacity 1000ms') }}> + <img className="imageBox-fadeaway" key="fadeaway" src={fadepath} style={{ transform, transformOrigin }} draggable={false} width={nativeWidth} /> </div> )} </div> @@ -382,6 +415,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this.props.select(false); }; savedAnnotations = () => this._savedAnnotations; + styleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any => { + if (property === StyleProp.BoxShadow) return undefined; + return this.props.styleProvider?.(doc, props, property); + }; render() { TraceMobx(); @@ -402,6 +439,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit} renderDepth={this.props.renderDepth + 1} fieldKey={this.annotationKey} + styleProvider={this.styleProvider} CollectionView={undefined} isAnnotationOverlay={true} annotationLayerHostsContent={true} @@ -427,8 +465,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp addDocument={this.addDocument} finishMarquee={this.finishMarquee} savedAnnotations={this.savedAnnotations} + selectionText={returnEmptyString} annotationLayer={this._annotationLayer.current} mainCont={this._mainCont.current} + highlightDragSrcColor={''} anchorMenuCrop={this.crop} /> )} diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index 7d04c4b64..18c5b81ec 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -20,6 +20,7 @@ import { ContextMenuProps } from '../ContextMenuItem'; import e = require('express'); import { FormattedTextBox } from './formattedText/FormattedTextBox'; import { ImageBox } from './ImageBox'; +import { OpenWhere } from './DocumentView'; export type KVPScript = { script: CompiledScript; @@ -259,8 +260,8 @@ 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, 'add:right'); + this.props.addDocTab(this.props.Document, OpenWhere.close); + this.props.addDocTab(this.fieldDocToLayout, OpenWhere.addRight); }, icon: 'image', }); diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index 80def3025..e74ef4a39 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -1,18 +1,19 @@ import { action, observable } from 'mobx'; -import { observer } from "mobx-react"; +import { observer } from 'mobx-react'; import { Doc, Field, Opt } from '../../../fields/Doc'; import { emptyFunction, returnFalse, returnOne, returnZero, returnEmptyFilter, returnEmptyDoclist, emptyPath } from '../../../Utils'; import { Docs } from '../../documents/Documents'; import { Transform } from '../../util/Transform'; import { undoBatch } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; -import { EditableView } from "../EditableView"; +import { EditableView } from '../EditableView'; import { FieldView, FieldViewProps } from './FieldView'; import { KeyValueBox } from './KeyValueBox'; -import "./KeyValueBox.scss"; -import "./KeyValuePair.scss"; -import React = require("react"); +import './KeyValueBox.scss'; +import './KeyValuePair.scss'; +import React = require('react'); import { DefaultStyleProvider } from '../StyleProvider'; +import { OpenWhere } from './DocumentView'; // Represents one row in a key value plane @@ -23,7 +24,7 @@ export interface KeyValuePairProps { keyWidth: number; PanelHeight: () => number; PanelWidth: () => number; - addDocTab: (doc: Doc, where: string) => boolean; + addDocTab: (doc: Doc, where: OpenWhere) => boolean; } @observer export class KeyValuePair extends React.Component<KeyValuePairProps> { @@ -34,23 +35,23 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { @action handleCheck = (e: React.ChangeEvent<HTMLInputElement>) => { this.isChecked = e.currentTarget.checked; - } + }; @action uncheck = () => { this.checkbox.current!.checked = false; this.isChecked = false; - } + }; onContextMenu = (e: React.MouseEvent) => { const value = this.props.doc[this.props.keyName]; 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 }), "add:right"), icon: "layer-group" }); + ContextMenu.Instance.addItem({ description: 'Open Fields', event: () => this.props.addDocTab(Docs.Create.KVPDocument(value, { _width: 300, _height: 300 }), OpenWhere.addRight), icon: 'layer-group' }); ContextMenu.Instance.displayMenu(e.clientX, e.clientY); } - } + }; render() { const props: FieldViewProps = { @@ -68,7 +69,7 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { isSelected: returnFalse, setHeight: returnFalse, select: emptyFunction, - dropAction: "alias", + dropAction: 'alias', bringToFront: emptyFunction, renderDepth: 1, isContentActive: returnFalse, @@ -92,30 +93,30 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { doc = doc.proto; } const parenCount = Math.max(0, protoCount - 1); - const keyStyle = protoCount === 0 ? "black" : "blue"; + const keyStyle = protoCount === 0 ? 'black' : 'blue'; - const hover = { transition: "0.3s ease opacity", opacity: this.isPointerOver || this.isChecked ? 1 : 0 }; + const hover = { transition: '0.3s ease opacity', opacity: this.isPointerOver || this.isChecked ? 1 : 0 }; return ( - <tr className={this.props.rowStyle} onPointerEnter={action(() => this.isPointerOver = true)} onPointerLeave={action(() => this.isPointerOver = false)}> + <tr className={this.props.rowStyle} onPointerEnter={action(() => (this.isPointerOver = true))} onPointerLeave={action(() => (this.isPointerOver = false))}> <td className="keyValuePair-td-key" style={{ width: `${this.props.keyWidth}%` }}> <div className="keyValuePair-td-key-container"> - <button style={hover} className="keyValuePair-td-key-delete" onClick={undoBatch(() => { - if (Object.keys(props.Document).indexOf(props.fieldKey) !== -1) { - delete props.Document[props.fieldKey]; - } - else delete props.Document.proto![props.fieldKey]; - })}> + <button + style={hover} + className="keyValuePair-td-key-delete" + onClick={undoBatch(() => { + if (Object.keys(props.Document).indexOf(props.fieldKey) !== -1) { + delete props.Document[props.fieldKey]; + } else delete props.Document.proto![props.fieldKey]; + })}> X </button> - <input - className={"keyValuePair-td-key-check"} - type="checkbox" - style={hover} - onChange={this.handleCheck} - ref={this.checkbox} - /> - <div className="keyValuePair-keyField" style={{ color: keyStyle }}>{"(".repeat(parenCount)}{props.fieldKey}{")".repeat(parenCount)}</div> + <input className={'keyValuePair-td-key-check'} type="checkbox" style={hover} onChange={this.handleCheck} ref={this.checkbox} /> + <div className="keyValuePair-keyField" style={{ color: keyStyle }}> + {'('.repeat(parenCount)} + {props.fieldKey} + {')'.repeat(parenCount)} + </div> </div> </td> <td className="keyValuePair-td-value" style={{ width: `${100 - this.props.keyWidth}%` }} onContextMenu={this.onContextMenu}> @@ -123,13 +124,13 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { <EditableView contents={contents} maxHeight={36} - height={"auto"} + height={'auto'} GetValue={() => Field.toKeyValueString(props.Document, props.fieldKey)} - SetValue={(value: string) => - KeyValueBox.SetField(props.Document, props.fieldKey, value)} /> + SetValue={(value: string) => KeyValueBox.SetField(props.Document, props.fieldKey, value)} + /> </div> </td> </tr> ); } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx index b58a9affb..10897b48f 100644 --- a/src/client/views/nodes/LabelBox.tsx +++ b/src/client/views/nodes/LabelBox.tsx @@ -15,16 +15,17 @@ import { FieldView, FieldViewProps } from './FieldView'; import BigText from './LabelBigText'; import './LabelBox.scss'; - export interface LabelBoxProps { label?: string; } @observer -export class LabelBox extends ViewBoxBaseComponent<(FieldViewProps & LabelBoxProps)>() { - public static LayoutString(fieldKey: string) { return FieldView.LayoutString(LabelBox, fieldKey); } +export class LabelBox extends ViewBoxBaseComponent<FieldViewProps & LabelBoxProps>() { + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(LabelBox, fieldKey); + } public static LayoutStringWithTitle(fieldStr: string, label?: string) { - return !label ? LabelBox.LayoutString(fieldStr) : `<LabelBox fieldKey={'${fieldStr}'} label={'${label}'} {...props} />`; //e.g., "<ImageBox {...props} fieldKey={"data} />" + return !label ? LabelBox.LayoutString(fieldStr) : `<LabelBox fieldKey={'${fieldStr}'} label={'${label}'} {...props} />`; //e.g., "<ImageBox {...props} fieldKey={"data} />" } private dropDisposer?: DragManager.DragDropDisposer; private _timeout: any; @@ -35,11 +36,12 @@ export class LabelBox extends ViewBoxBaseComponent<(FieldViewProps & LabelBoxPro this._timeout && clearTimeout(this._timeout); } + getAnchor = () => { + return this.rootDoc; + }; + getTitle() { - return this.rootDoc["title-custom"] ? StrCast(this.rootDoc.title) : - this.props.label ? this.props.label : - typeof this.rootDoc[this.fieldKey] === "string" ? StrCast(this.rootDoc[this.fieldKey]) : - StrCast(this.rootDoc.title); + return this.rootDoc['title-custom'] ? StrCast(this.rootDoc.title) : this.props.label ? this.props.label : typeof this.rootDoc[this.fieldKey] === 'string' ? StrCast(this.rootDoc[this.fieldKey]) : StrCast(this.rootDoc.title); } protected createDropTarget = (ele: HTMLDivElement) => { @@ -47,36 +49,42 @@ export class LabelBox extends ViewBoxBaseComponent<(FieldViewProps & LabelBoxPro if (ele) { this.dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.props.Document); } - } + }; - get paramsDoc() { return Doc.AreProtosEqual(this.layoutDoc, this.dataDoc) ? this.dataDoc : this.layoutDoc; } + get paramsDoc() { + return Doc.AreProtosEqual(this.layoutDoc, this.dataDoc) ? this.dataDoc : this.layoutDoc; + } specificContextMenu = (e: React.MouseEvent): void => { const funcs: ContextMenuProps[] = []; - !Doc.noviceMode && funcs.push({ - description: "Clear Script Params", event: () => { - const params = Cast(this.paramsDoc["onClick-paramFieldKeys"], listSpec("string"), []); - params?.map(p => this.paramsDoc[p] = undefined); - }, icon: "trash" - }); + !Doc.noviceMode && + funcs.push({ + description: 'Clear Script Params', + event: () => { + const params = Cast(this.paramsDoc['onClick-paramFieldKeys'], listSpec('string'), []); + params?.map(p => (this.paramsDoc[p] = undefined)); + }, + icon: 'trash', + }); - funcs.length && ContextMenu.Instance.addItem({ description: "OnClick...", noexpand: true, subitems: funcs, icon: "mouse-pointer" }); - } + funcs.length && ContextMenu.Instance.addItem({ description: 'OnClick...', noexpand: true, subitems: funcs, icon: 'mouse-pointer' }); + }; @undoBatch @action drop = (e: Event, de: DragManager.DropEvent) => { const docDragData = de.complete.docDragData; - const params = Cast(this.paramsDoc["onClick-paramFieldKeys"], listSpec("string"), []); + const params = Cast(this.paramsDoc['onClick-paramFieldKeys'], listSpec('string'), []); const missingParams = params?.filter(p => !this.paramsDoc[p]); if (docDragData && missingParams?.includes((e.target as any).textContent)) { - this.paramsDoc[(e.target as any).textContent] = new List<Doc>(docDragData.droppedDocuments.map((d, i) => - d.onDragStart ? docDragData.draggedDocuments[i] : d)); + this.paramsDoc[(e.target as any).textContent] = new List<Doc>(docDragData.droppedDocuments.map((d, i) => (d.onDragStart ? docDragData.draggedDocuments[i] : d))); e.stopPropagation(); } - } + }; @observable _mouseOver = false; - @computed get hoverColor() { return this._mouseOver ? StrCast(this.layoutDoc._hoverBackgroundColor) : "unset"; } + @computed get hoverColor() { + return this._mouseOver ? StrCast(this.layoutDoc._hoverBackgroundColor) : 'unset'; + } fitTextToBox = (r: any): any => { const singleLine = BoolCast(this.rootDoc._singleLine, true); @@ -85,63 +93,73 @@ export class LabelBox extends ViewBoxBaseComponent<(FieldViewProps & LabelBoxPro fontSizeFactor: 1, minimumFontSize: NumCast(this.rootDoc._minFontSize, 8), maximumFontSize: NumCast(this.rootDoc._maxFontSize, 1000), - limitingDimension: "both", - horizontalAlign: "center", - verticalAlign: "center", - textAlign: "center", + limitingDimension: 'both', + horizontalAlign: 'center', + verticalAlign: 'center', + textAlign: 'center', singleLine, - whiteSpace: singleLine ? "nowrap" : "pre-wrap" + whiteSpace: singleLine ? 'nowrap' : 'pre-wrap', }; this._timeout = undefined; if (!r) return params; - if (!r.offsetHeight || !r.offsetWidth) return this._timeout = setTimeout(() => this.fitTextToBox(r)); + if (!r.offsetHeight || !r.offsetWidth) return (this._timeout = setTimeout(() => this.fitTextToBox(r))); const parent = r.parentNode; const parentStyle = parent.style; - parentStyle.display = ""; - parentStyle.alignItems = ""; - r.setAttribute("style", ""); - r.style.width = singleLine ? "" : "100%"; + parentStyle.display = ''; + parentStyle.alignItems = ''; + r.setAttribute('style', ''); + r.style.width = singleLine ? '' : '100%'; - r.style.textOverflow = "ellipsis"; - r.style.overflow = "hidden"; + r.style.textOverflow = 'ellipsis'; + r.style.overflow = 'hidden'; BigText(r, params); return params; - } + }; // (!missingParams || !missingParams.length ? "" : "(" + missingParams.map(m => m + ":").join(" ") + ")") render() { - const boxParams = this.fitTextToBox(null);// this causes mobx to trigger re-render when data changes - const params = Cast(this.paramsDoc["onClick-paramFieldKeys"], listSpec("string"), []); + const boxParams = this.fitTextToBox(null); // this causes mobx to trigger re-render when data changes + const params = Cast(this.paramsDoc['onClick-paramFieldKeys'], listSpec('string'), []); const missingParams = params?.filter(p => !this.paramsDoc[p]); params?.map(p => DocListCast(this.paramsDoc[p])); // bcz: really hacky form of prefetching ... const label = this.getTitle(); return ( - <div className="labelBox-outerDiv" - onMouseLeave={action(() => this._mouseOver = false)} - onMouseOver={action(() => this._mouseOver = true)} - ref={this.createDropTarget} onContextMenu={this.specificContextMenu} + <div + className="labelBox-outerDiv" + onMouseLeave={action(() => (this._mouseOver = false))} + onMouseOver={action(() => (this._mouseOver = true))} + ref={this.createDropTarget} + onContextMenu={this.specificContextMenu} style={{ boxShadow: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BoxShadow) }}> - <div className="labelBox-mainButton" style={{ - backgroundColor: this.hoverColor, - fontSize: StrCast(this.layoutDoc._fontSize), - fontFamily: StrCast(this.layoutDoc._fontFamily) || "inherit", - letterSpacing: StrCast(this.layoutDoc.letterSpacing), - textTransform: StrCast(this.layoutDoc.textTransform) as any, - paddingLeft: NumCast(this.rootDoc._xPadding), - paddingRight: NumCast(this.rootDoc._xPadding), - paddingTop: NumCast(this.rootDoc._yPadding), - paddingBottom: NumCast(this.rootDoc._yPadding), - width: this.props.PanelWidth(), - height: this.props.PanelHeight(), - whiteSpace: boxParams.singleLine ? "pre" : "pre-wrap" - }} > - <span style={{ width: boxParams.singleLine ? "" : "100%" }} ref={action((r: any) => this.fitTextToBox(r))}> - {label.startsWith("#") ? (null) : label.replace(/([^a-zA-Z])/g, "$1\u200b")} + <div + className="labelBox-mainButton" + style={{ + backgroundColor: this.hoverColor, + fontSize: StrCast(this.layoutDoc._fontSize), + fontFamily: StrCast(this.layoutDoc._fontFamily) || 'inherit', + letterSpacing: StrCast(this.layoutDoc.letterSpacing), + textTransform: StrCast(this.layoutDoc.textTransform) as any, + paddingLeft: NumCast(this.rootDoc._xPadding), + paddingRight: NumCast(this.rootDoc._xPadding), + paddingTop: NumCast(this.rootDoc._yPadding), + paddingBottom: NumCast(this.rootDoc._yPadding), + width: this.props.PanelWidth(), + height: this.props.PanelHeight(), + whiteSpace: boxParams.singleLine ? 'pre' : 'pre-wrap', + }}> + <span style={{ width: boxParams.singleLine ? '' : '100%' }} ref={action((r: any) => this.fitTextToBox(r))}> + {label.startsWith('#') ? null : label.replace(/([^a-zA-Z])/g, '$1\u200b')} </span> </div> - <div className="labelBox-fieldKeyParams" > - {!missingParams?.length ? (null) : missingParams.map(m => <div key={m} className="labelBox-missingParam">{m}</div>)} + <div className="labelBox-fieldKeyParams"> + {!missingParams?.length + ? null + : missingParams.map(m => ( + <div key={m} className="labelBox-missingParam"> + {m} + </div> + ))} </div> </div> ); } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx index 5102eae51..e89076c1f 100644 --- a/src/client/views/nodes/LinkAnchorBox.tsx +++ b/src/client/views/nodes/LinkAnchorBox.tsx @@ -1,4 +1,3 @@ -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, observable } from 'mobx'; import { observer } from 'mobx-react'; import { Doc } from '../../../fields/Doc'; @@ -7,19 +6,17 @@ import { TraceMobx } from '../../../fields/util'; import { emptyFunction, setupMoveUpEvents, Utils } from '../../../Utils'; import { DragManager } from '../../util/DragManager'; import { LinkFollower } from '../../util/LinkFollower'; -import { SelectionManager } from '../../util/SelectionManager'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { ViewBoxBaseComponent } from '../DocComponent'; -import { LinkEditor } from '../linking/LinkEditor'; import { StyleProp } from '../StyleProvider'; import { FieldView, FieldViewProps } from './FieldView'; import './LinkAnchorBox.scss'; import { LinkDocPreview } from './LinkDocPreview'; import React = require('react'); -const higflyout = require('@hig/flyout'); -export const { anchorPoints } = higflyout; -export const Flyout = higflyout.default; +import { LinkManager } from '../../util/LinkManager'; +import globalCssVariables = require('../global/globalCssVariables.scss'); +import { SelectionManager } from '../../util/SelectionManager'; @observer export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() { @@ -33,15 +30,23 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() { _timeout: NodeJS.Timeout | undefined; @observable _x = 0; @observable _y = 0; - @observable _selected = false; - @observable _editing = false; - @observable _forceOpen = false; onPointerDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, false); + const anchorContainerDoc = this.props.styleProvider?.(this.dataDoc, this.props, StyleProp.LinkSource); + setupMoveUpEvents( + this, + e, + this.onPointerMove, + emptyFunction, + (e, doubleTap) => { + if (doubleTap) LinkFollower.FollowLink(this.rootDoc, anchorContainerDoc, this.props, false); + else this.props.select(false); + }, + false + ); }; onPointerMove = action((e: PointerEvent, down: number[], delta: number[]) => { - const cdiv = this._ref && this._ref.current && this._ref.current.parentElement; + const cdiv = this._ref?.current?.parentElement; if (!this._isOpen && cdiv) { const bounds = cdiv.getBoundingClientRect(); const pt = Utils.getNearestPointInPerimeter(bounds.left, bounds.top, bounds.width, bounds.height, e.clientX, e.clientY); @@ -59,58 +64,8 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() { } return false; }); - @action - onClick = (e: React.MouseEvent) => { - if (e.button === 2 || e.ctrlKey || !this.layoutDoc.isLinkButton) { - this.props.select(false); - } - if (!this._doubleTap && !e.ctrlKey && e.button < 2) { - const anchorContainerDoc = this.props.styleProvider?.(this.dataDoc, this.props, StyleProp.LinkSource); - this._editing = true; - anchorContainerDoc && this.props.bringToFront(anchorContainerDoc, false); - if (anchorContainerDoc && !this.layoutDoc.onClick && !this._isOpen) { - this._timeout = setTimeout( - action(() => { - LinkFollower.FollowLink(this.rootDoc, anchorContainerDoc, this.props, false); - this._editing = false; - }), - 300 - (Date.now() - this._lastTap) - ); - e.stopPropagation(); - } - } else { - this._timeout && clearTimeout(this._timeout); - this._timeout = undefined; - this._doubleTap = false; - this.openLinkEditor(e); - e.stopPropagation(); - } - }; - openLinkDocOnRight = (e: React.MouseEvent) => { - 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.layoutKey = 'layout'; - this.props.addDocTab(alias, 'add:right'); - }; - @action - openLinkEditor = action((e: React.MouseEvent) => { - SelectionManager.DeselectAll(); - this._editing = this._forceOpen = true; - }); - - specificContextMenu = (e: React.MouseEvent): void => { - const funcs: ContextMenuProps[] = []; - funcs.push({ description: 'Open Link Target on Right', event: () => this.openLinkTargetOnRight(e), icon: 'eye' }); - funcs.push({ description: 'Open Link on Right', event: () => this.openLinkDocOnRight(e), icon: 'eye' }); - funcs.push({ description: 'Open Link Editor', event: () => this.openLinkEditor(e), icon: 'eye' }); - funcs.push({ description: 'Toggle Always Show Link', event: () => (this.props.Document.linkDisplay = !this.props.Document.linkDisplay), icon: 'eye' }); - - ContextMenu.Instance.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' }); - }; + specificContextMenu = (e: React.MouseEvent): void => {}; render() { TraceMobx(); @@ -121,22 +76,13 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() { const background = this.props.styleProvider?.(this.dataDoc, this.props, StyleProp.BackgroundColor + ':anchor'); const anchor = this.fieldKey === 'anchor1' ? 'anchor2' : 'anchor1'; const anchorScale = !this.dataDoc[this.fieldKey + '-useLinkSmallAnchor'] && (x === 0 || x === 100 || y === 0 || y === 100) ? 1 : 0.25; - const targetTitle = StrCast((this.dataDoc[anchor] as Doc)?.title); - const flyout = ( - <div className="linkAnchorBoxBox-flyout" title=" " onPointerOver={() => Doc.UnBrushDoc(this.rootDoc)}> - <LinkEditor sourceDoc={Cast(this.dataDoc[this.fieldKey], Doc, null)} hideback={true} linkDoc={this.rootDoc} showLinks={action(() => {})} /> - {!this._forceOpen ? null : ( - <div className="linkAnchorBox-linkCloser" onPointerDown={action(() => (this._isOpen = this._editing = this._forceOpen = false))}> - <FontAwesomeIcon color="dimgray" icon={'times'} size={'sm'} /> - </div> - )} - </div> - ); + const selView = SelectionManager.Views().lastElement()?.props.LayoutTemplateString?.includes('anchor1') ? 'anchor1' : SelectionManager.Views().lastElement()?.props.LayoutTemplateString?.includes('anchor2') ? 'anchor2' : ''; return ( <div + ref={this._ref} + title={targetTitle} className={`linkAnchorBox-cont${small ? '-small' : ''}`} - //onPointerLeave={} //LinkDocPreview.Clear} onPointerEnter={e => LinkDocPreview.SetLinkInfo({ docProps: this.props, @@ -144,27 +90,19 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() { linkDoc: this.rootDoc, showHeader: true, location: [e.clientX, e.clientY + 20], + noPreview: false, }) } onPointerDown={this.onPointerDown} - onClick={this.onClick} - title={targetTitle} onContextMenu={this.specificContextMenu} - ref={this._ref} style={{ + border: selView && this.rootDoc[selView] === this.rootDoc[this.fieldKey] ? `solid ${globalCssVariables.MEDIUM_GRAY} 2px` : undefined, background, left: `calc(${x}% - ${small ? 2.5 : 7.5}px)`, top: `calc(${y}% - ${small ? 2.5 : 7.5}px)`, transform: `scale(${anchorScale})`, - }}> - {!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="linkAnchorBox-button"> - <FontAwesomeIcon icon={'eye'} size={'lg'} /> - </span> - </Flyout> - )} - </div> + }} + /> ); } } diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx index 27e79a83b..232c3459e 100644 --- a/src/client/views/nodes/LinkDocPreview.tsx +++ b/src/client/views/nodes/LinkDocPreview.tsx @@ -12,12 +12,11 @@ import { DocumentType } from '../../documents/DocumentTypes'; import { DragManager } from '../../util/DragManager'; import { LinkFollower } from '../../util/LinkFollower'; import { LinkManager } from '../../util/LinkManager'; +import { SettingsManager } from '../../util/SettingsManager'; import { Transform } from '../../util/Transform'; -import { undoBatch } from '../../util/UndoManager'; -import { DocumentView, DocumentViewSharedProps } from './DocumentView'; +import { DocumentView, DocumentViewSharedProps, OpenWhere } from './DocumentView'; import './LinkDocPreview.scss'; import React = require('react'); -import { LinkEditor } from '../linking/LinkEditor'; interface LinkDocPreviewProps { linkDoc?: Doc; @@ -26,6 +25,7 @@ interface LinkDocPreviewProps { location: number[]; hrefs?: string[]; showHeader?: boolean; + noPreview?: boolean; } @observer export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { @@ -40,6 +40,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { _linkDocRef = React.createRef<HTMLDivElement>(); @observable public static LinkInfo: Opt<LinkDocPreviewProps>; @observable _targetDoc: Opt<Doc>; + @observable _markerTargetDoc: Opt<Doc>; @observable _linkDoc: Opt<Doc>; @observable _linkSrc: Opt<Doc>; @observable _toolTipText = ''; @@ -56,9 +57,9 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { } if (linkTarget?.annotationOn && linkTarget?.type !== DocumentType.RTF) { // want to show annotation context document if annotation is not text - linkTarget && DocCastAsync(linkTarget.annotationOn).then(action(anno => (this._targetDoc = anno))); + linkTarget && DocCastAsync(linkTarget.annotationOn).then(action(anno => (this._markerTargetDoc = this._targetDoc = anno))); } else { - this._targetDoc = linkTarget; + this._markerTargetDoc = this._targetDoc = linkTarget; } this._toolTipText = ''; } @@ -104,13 +105,15 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { // automatic links specify the target in the link info, not the source const linkTarget = anchor; this._linkSrc = LinkManager.getOppositeAnchor(this._linkDoc, linkTarget); - this._targetDoc = linkTarget; + this._markerTargetDoc = this._targetDoc = linkTarget; } else { this._linkSrc = anchor; const linkTarget = LinkManager.getOppositeAnchor(this._linkDoc, this._linkSrc); + this._markerTargetDoc = linkTarget; this._targetDoc = /*linkTarget?.type === DocumentType.MARKER &&*/ linkTarget?.annotationOn ? Cast(linkTarget.annotationOn, Doc, null) ?? linkTarget : linkTarget; } this._toolTipText = ''; + if (LinkDocPreview.LinkInfo?.noPreview || this._markerTargetDoc?.type === DocumentType.PRES) this.followLink(); } }) ); @@ -119,15 +122,22 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { } return undefined; } - @observable _showEditor = false; + + @action editLink = (e: React.PointerEvent): void => { - LinkManager.currentLink = this.props.linkDoc; setupMoveUpEvents( this, e, returnFalse, emptyFunction, - action(() => (this._showEditor = !this._showEditor)) + action(() => { + LinkManager.currentLink = this._linkDoc; + LinkManager.currentLinkAnchor = this._linkSrc; + this.props.docProps.DocumentView?.().select(false); + if ((SettingsManager.propertiesWidth ?? 0) < 100) { + SettingsManager.propertiesWidth = 250; + } + }) ); }; nextHref = (e: React.PointerEvent) => { @@ -152,7 +162,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { LinkDocPreview.Clear(); LinkFollower.FollowLink(this._linkDoc, this._linkSrc, this.props.docProps, false); } else if (this.props.hrefs?.length) { - this.props.docProps?.addDocTab(Docs.Create.WebDocument(this.props.hrefs[0], { title: this.props.hrefs[0], _nativeWidth: 850, _width: 200, _height: 400, useCors: true }), 'add:right'); + this.props.docProps?.addDocTab(Docs.Create.WebDocument(this.props.hrefs[0], { title: this.props.hrefs[0], _nativeWidth: 850, _width: 200, _height: 400, useCors: true }), OpenWhere.addRight); } }; @@ -173,24 +183,25 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { return Math.min(225, NumCast(this._targetDoc?.[HeightSym](), 225)); }; @computed get previewHeader() { - return !this._linkDoc || !this._targetDoc || !this._linkSrc ? null : ( + return !this._linkDoc || !this._markerTargetDoc || !this._targetDoc || !this._linkSrc ? null : ( <div className="linkDocPreview-info"> + <div className="linkDocPreview-buttonBar" style={{ float: 'left' }}> + <Tooltip title={<div className="dash-tooltip">Edit Link</div>} placement="top"> + <div className="linkDocPreview-button" onPointerDown={this.editLink}> + <FontAwesomeIcon className="linkDocPreview-fa-icon" icon="edit" color="white" size="sm" /> + </div> + </Tooltip> + </div> <div className="linkDocPreview-title" style={{ pointerEvents: 'all' }}> - {StrCast(this._targetDoc.title).length > 16 ? StrCast(this._targetDoc.title).substr(0, 16) + '...' : StrCast(this._targetDoc.title)} + {StrCast(this._markerTargetDoc.title).length > 16 ? StrCast(this._markerTargetDoc.title).substr(0, 16) + '...' : StrCast(this._markerTargetDoc.title)} <p className="linkDocPreview-description"> {StrCast(this._linkDoc.description)}</p> </div> - <div className="linkDocPreview-buttonBar"> + <div className="linkDocPreview-buttonBar" style={{ float: 'right' }}> <Tooltip title={<div className="dash-tooltip">Next Link</div>} placement="top"> <div className="linkDocPreview-button" style={{ background: (this.props.hrefs?.length || 0) <= 1 ? 'gray' : 'green' }} onPointerDown={this.nextHref}> <FontAwesomeIcon className="linkDocPreview-fa-icon" icon="chevron-right" color="white" size="sm" /> </div> </Tooltip> - - <Tooltip title={<div className="dash-tooltip">Edit Link</div>} placement="top"> - <div className="linkDocPreview-button" onPointerDown={this.editLink}> - <FontAwesomeIcon className="linkDocPreview-fa-icon" icon="edit" color="white" size="sm" /> - </div> - </Tooltip> </div> </div> ); @@ -274,9 +285,8 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { className="linkDocPreview" ref={this._linkDocRef} onPointerDown={this.followLinkPointerDown} - style={{ left: this.props.location[0], top: this.props.location[1], width: this._showEditor ? 'auto' : this.width() + borders, height: this._showEditor ? 'max-content' : this.height() + borders + (this.props.showHeader ? 37 : 0) }}> - {this._showEditor ? null : this.docPreview} - {!this._showEditor || !this._linkSrc || !this._linkDoc ? null : <LinkEditor sourceDoc={this._linkSrc} linkDoc={this._linkDoc} showLinks={action(() => (this._showEditor = !this._showEditor))} />} + style={{ left: this.props.location[0], top: this.props.location[1], width: this.width() + borders, height: this.height() + borders + (this.props.showHeader ? 37 : 0) }}> + {this.docPreview} </div> ); } diff --git a/src/client/views/nodes/LoadingBox.scss b/src/client/views/nodes/LoadingBox.scss index d63ed2575..4c3b8dabe 100644 --- a/src/client/views/nodes/LoadingBox.scss +++ b/src/client/views/nodes/LoadingBox.scss @@ -6,6 +6,13 @@ background-color: #fdfdfd; height: 100%; align-items: center; + .textContainer, + .text { + overflow: hidden; + text-overflow: ellipsis; + max-width: 80%; + text-align: center; + } } .textContainer { @@ -17,14 +24,6 @@ align-content: center; } -.textContainer, -.text { - overflow: hidden; - text-overflow: ellipsis; - max-width: 80%; - text-align: center; -} - .headerText { text-align: center; font-weight: bold; diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx index 6479e933e..c8d5b0154 100644 --- a/src/client/views/nodes/MapBox/MapBox.tsx +++ b/src/client/views/nodes/MapBox/MapBox.tsx @@ -7,7 +7,7 @@ import { Doc, DocListCast, Opt, WidthSym } from '../../../../fields/Doc'; import { Id } from '../../../../fields/FieldSymbols'; import { InkTool } from '../../../../fields/InkField'; import { NumCast, StrCast } from '../../../../fields/Types'; -import { emptyFunction, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../../Utils'; +import { emptyFunction, OmitKeys, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../../Utils'; import { Docs } from '../../../documents/Documents'; import { DragManager } from '../../../util/DragManager'; import { SnappingManager } from '../../../util/SnappingManager'; @@ -322,7 +322,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps */ sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => { console.log('print all sidebar Docs'); - console.log(this.allSidebarDocs); if (!this.layoutDoc._showSidebar) this.toggleSidebar(); const docs = doc instanceof Doc ? [doc] : doc; docs.forEach(doc => { @@ -337,8 +336,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } } }); //add to annotation list - console.log('sidebaraddDocument'); - console.log(doc); return this.addDocument(doc, sidebarKey); // add to sidebar list }; @@ -352,10 +349,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps sidebarRemoveDocument = (doc: Doc | Doc[], sidebarKey?: string) => { if (this.layoutDoc._showSidebar) this.toggleSidebar(); const docs = doc instanceof Doc ? [doc] : doc; - docs.forEach(doc => { - console.log(this.allMapMarkers); - console.log(this.allSidebarDocs); - }); return this.removeDocument(doc, sidebarKey); }; @@ -405,7 +398,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps */ @action private handlePlaceChanged = () => { - console.log(this.searchBox); const place = this.searchBox.getPlace(); if (!place.geometry || !place.geometry.location) { @@ -416,7 +408,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps // zoom in on the location of the search result if (place.geometry.viewport) { - console.log(this._map); this._map.fitBounds(place.geometry.viewport); } else { this._map.setCenter(place.geometry.location); @@ -638,6 +629,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps finishMarquee={this.finishMarquee} savedAnnotations={this.savedAnnotations} annotationLayer={this._annotationLayer.current} + selectionText={returnEmptyString} mainCont={this._mainCont.current} /> )} diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index c7001f846..e22ee5021 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -5,7 +5,7 @@ import * as Pdfjs from 'pdfjs-dist'; import 'pdfjs-dist/web/pdf_viewer.css'; import { Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; -import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types'; +import { BoolCast, Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types'; import { ImageField, PdfField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; import { emptyFunction, setupMoveUpEvents, Utils } from '../../../Utils'; @@ -27,6 +27,8 @@ import { ImageBox } from './ImageBox'; import './PDFBox.scss'; import { VideoBox } from './VideoBox'; import React = require('react'); +import { PresBox } from './trails'; +import { DocFocusOptions } from './DocumentView'; @observer export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() { @@ -74,7 +76,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps if (oldDiv.className === 'pdfBox-ui' || oldDiv.className === 'pdfViewerDash-overlay-inking') { newDiv.style.display = 'none'; } - if (newDiv && newDiv.style) newDiv.style.overflow = 'hidden'; + if (newDiv?.style) newDiv.style.overflow = 'hidden'; if (oldDiv instanceof HTMLCanvasElement) { const canvas = oldDiv; const img = document.createElement('img'); // create a Image Element @@ -93,7 +95,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const cropping = Doc.MakeCopy(region, true); Doc.GetProto(region).lockedPosition = true; Doc.GetProto(region).title = 'region:' + this.rootDoc.title; - Doc.GetProto(region).isPushpin = true; + Doc.GetProto(region).followLinkToggle = true; this.addDocument(region); const docViewContent = this.props.docViewPath().lastElement().ContentDiv!; @@ -199,24 +201,34 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps brushView = (view: { width: number; height: number; panX: number; panY: number }) => { this._pdfViewer?.brushView(view); }; - scrollFocus = (doc: Doc, smooth: boolean) => { + scrollFocus = (doc: Doc, options: DocFocusOptions) => { let didToggle = false; if (DocListCast(this.props.Document[this.fieldKey + '-sidebar']).includes(doc) && !this.SidebarShown) { - this.toggleSidebar(!smooth); + this.toggleSidebar(!options.instant); didToggle = true; } if (this._sidebarRef?.current?.makeDocUnfiltered(doc)) return 1; this._initialScrollTarget = doc; - return this._pdfViewer?.scrollFocus(doc, smooth) ?? (didToggle ? 1 : undefined); + PresBox.restoreTargetDocView(this.props.DocumentView?.(), {}, doc, options.zoomTime ?? 500, { pannable: doc.presPinData ? true : false }); + return this._pdfViewer?.scrollFocus(doc, NumCast(doc.presPinViewScroll, NumCast(doc.y)), options) ?? (didToggle ? 1 : undefined); }; getAnchor = () => { - const anchor = - this._pdfViewer?._getAnchor(this._pdfViewer.savedAnnotations()) ?? - Docs.Create.TextanchorDocument({ + let ele: Opt<HTMLDivElement> = undefined; + if (this._pdfViewer?.selectionContent()) { + ele = document.createElement('div'); + ele.append(this._pdfViewer.selectionContent()!); + } + const docAnchor = () => { + const anchor = Docs.Create.TextanchorDocument({ title: StrCast(this.rootDoc.title + '@' + NumCast(this.layoutDoc._scrollTop)?.toFixed(0)), - y: NumCast(this.layoutDoc._scrollTop), unrendered: true, }); + PresBox.pinDocView(anchor, { pinData: { scrollable: true, pannable: true } }, this.rootDoc); + return anchor; + }; + const anchor = this._pdfViewer?._getAnchor(this._pdfViewer.savedAnnotations()) ?? docAnchor(); + anchor.text = ele?.textContent ?? ''; + anchor.textHtml = ele?.innerHTML; this.addDocument(anchor); return anchor; }; @@ -273,7 +285,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps setPdfViewer = (pdfViewer: PDFViewer) => { this._pdfViewer = pdfViewer; if (this._initialScrollTarget) { - this.scrollFocus(this._initialScrollTarget, false); + this.scrollFocus(this._initialScrollTarget, { instant: true }); this._initialScrollTarget = undefined; } }; @@ -398,9 +410,12 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps </div> ); } - sidebarWidth = () => - !this.SidebarShown ? 0 : PDFBox.sidebarResizerWidth + (this._previewWidth ? PDFBox.openSidebarWidth : ((NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc)) * this.props.PanelWidth()) / NumCast(this.layoutDoc.nativeWidth)); - + sidebarWidth = () => { + if (!this.SidebarShown) return 0; + if (this._previewWidth) return PDFBox.sidebarResizerWidth + PDFBox.openSidebarWidth; // return default sidebar if previewing (as in viewing a link target) + const nativeDiff = NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc); + return PDFBox.sidebarResizerWidth + nativeDiff * (this.props.NativeDimScaling?.() || 1); + }; specificContextMenu = (e: React.MouseEvent): void => { const funcs: ContextMenuProps[] = []; funcs.push({ description: 'Copy path', event: () => this.pdfUrl && Utils.CopyText(Utils.prepend('') + this.pdfUrl.url.pathname), icon: 'expand-arrows-alt' }); @@ -483,7 +498,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps crop={this.crop} /> </div> - <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this.layoutDoc[WidthSym]()}%` }}> + <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this.props.PanelWidth()}%` }}> <SidebarAnnos ref={this._sidebarRef} {...this.props} diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx index 4c8a836f1..281967a21 100644 --- a/src/client/views/nodes/ScriptingBox.tsx +++ b/src/client/views/nodes/ScriptingBox.tsx @@ -197,7 +197,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable const bindings: { [name: string]: any } = {}; this.paramsNames.forEach(key => (bindings[key] = this.rootDoc[key])); // binds vars so user doesnt have to refer to everything as self.<var> - ScriptCast(this.rootDoc[this.fieldKey], null)?.script.run({ self: this.rootDoc, this: this.layoutDoc, ...bindings }, this.onError); + ScriptCast(this.rootDoc[this.fieldKey], null)?.script.run({ ...bindings, self: this.rootDoc, this: this.layoutDoc }, this.onError); } }; diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index fb47bfc07..a8f78edd5 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -9,7 +9,7 @@ import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; import { Cast, NumCast, StrCast } from '../../../fields/Types'; import { AudioField, ImageField, VideoField } from '../../../fields/URLField'; -import { emptyFunction, formatTime, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../Utils'; +import { emptyFunction, formatTime, OmitKeys, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; import { Networking } from '../../Network'; @@ -31,6 +31,7 @@ import { FieldView, FieldViewProps } from './FieldView'; import { RecordingBox } from './RecordingBox'; import './VideoBox.scss'; import { ObjectField } from '../../../fields/ObjectField'; +import { DocFocusOptions, OpenWhere } from './DocumentView'; const path = require('path'); /** @@ -273,7 +274,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this.player && this._contentRef && this._contentRef.requestFullscreen(); } try { - this._youtubePlayer && this.props.addDocTab(this.rootDoc, 'add'); + this._youtubePlayer && this.props.addDocTab(this.rootDoc, OpenWhere.add); } catch (e) { console.log('Video FullScreen Exception:', e); } @@ -949,7 +950,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp ); }; - scrollFocus = (doc: Doc, smooth: boolean) => { + scrollFocus = (doc: Doc, options: DocFocusOptions) => { if (doc !== this.rootDoc) { const showTime = Cast(doc._timecodeToShow, 'number', null); showTime !== undefined && setTimeout(() => this.Seek(showTime), 100); @@ -960,7 +961,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // renders CollectionStackedTimeline @computed get renderTimeline() { return ( - <div className="videoBox-stackPanel" style={{ transition: this.transition, height: `${100 - this.heightPercent}%` }}> + <div className="videoBox-stackPanel" style={{ transition: this.transition, height: `${100 - this.heightPercent}%`, display: this.heightPercent === 100 ? 'none' : '' }}> <CollectionStackedTimeline ref={action((r: any) => (this._stackedTimeline = r))} {...this.props} @@ -1005,7 +1006,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp Doc.GetProto(region).backgroundColor = 'transparent'; Doc.GetProto(region).lockedPosition = true; Doc.GetProto(region).title = 'region:' + this.rootDoc.title; - Doc.GetProto(region).isPushpin = true; + Doc.GetProto(region).followLinkToggle = true; region._timecodeToHide = NumCast(region._timecodeToShow) + 0.0001; this.addDocument(region); const anchx = NumCast(cropping.x); @@ -1107,6 +1108,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp addDocument={this.addDocWithTimecode} finishMarquee={this.finishMarquee} savedAnnotations={this.savedAnnotations} + selectionText={returnEmptyString} annotationLayer={this._annotationLayer.current} mainCont={this._mainCont.current} anchorMenuCrop={this.crop} diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss index a41f66ef0..6f578a9fc 100644 --- a/src/client/views/nodes/WebBox.scss +++ b/src/client/views/nodes/WebBox.scss @@ -7,6 +7,7 @@ left: 0; position: relative; display: flex; + overflow: hidden; .webBox-sideResizer { position: absolute; diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index db493934a..8be4884ce 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -11,7 +11,7 @@ import { listSpec } from '../../../fields/Schema'; import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types'; import { ImageField, WebField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, getWordAtPoint, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, smoothScroll, StopEvent, Utils } from '../../../Utils'; +import { emptyFunction, getWordAtPoint, OmitKeys, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, smoothScroll, StopEvent, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { ScriptingGlobals } from '../../util/ScriptingGlobals'; import { SnappingManager } from '../../util/SnappingManager'; @@ -29,7 +29,7 @@ import { AnchorMenu } from '../pdf/AnchorMenu'; import { Annotation } from '../pdf/Annotation'; import { SidebarAnnos } from '../SidebarAnnos'; import { StyleProp } from '../StyleProvider'; -import { DocumentViewProps } from './DocumentView'; +import { DocFocusOptions, DocumentView, DocumentViewInternal, DocumentViewProps } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import { LinkDocPreview } from './LinkDocPreview'; import { VideoBox } from './VideoBox'; @@ -54,10 +54,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); private _keyInput = React.createRef<HTMLInputElement>(); private _initialScroll: Opt<number> = NumCast(this.layoutDoc.thumbScrollTop, NumCast(this.layoutDoc.scrollTop)); - private _getAnchor: (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => Opt<Doc> = () => undefined; private _sidebarRef = React.createRef<SidebarAnnos>(); private _searchRef = React.createRef<HTMLInputElement>(); private _searchString = ''; + + private get _getAnchor() { + return AnchorMenu.Instance?.GetAnchor; + } @observable private _webUrl = ''; // url of the src parameter of the embedded iframe but not necessarily the rendered page - eg, when following a link, the rendered page changes but we don't wan the src parameter to also change as that would cause an unnecessary re-render. @observable private _hackHide = false; // apparently changing the value of the 'sandbox' prop doesn't necessarily apply it to the active iframe. so thisforces the ifrmae to be rebuilt when allowScripts is toggled @observable private _searching: boolean = false; @@ -241,7 +244,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const durationMiliStr = viewTrans.match(/([0-9]*)ms/); const durationSecStr = viewTrans.match(/([0-9.]*)s/); const duration = durationMiliStr ? Number(durationMiliStr[1]) : durationSecStr ? Number(durationSecStr[1]) * 1000 : 0; - this.goTo(scrollTop, duration); + this.goTo(scrollTop, duration, 'ease'); }, { fireImmediately: true } ); @@ -284,18 +287,18 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps setBrushViewer = (func?: (view: { width: number; height: number; panX: number; panY: number }) => void) => (this._setBrushViewer = func); brushView = (view: { width: number; height: number; panX: number; panY: number }) => this._setBrushViewer?.(view); - scrollFocus = (doc: Doc, smooth: boolean) => { - if (StrCast(doc.webUrl) !== this._url) this.submitURL(StrCast(doc.webUrl), !smooth); + scrollFocus = (doc: Doc, options: DocFocusOptions) => { + if (this._url && StrCast(doc.webUrl) !== this._url) this.submitURL(StrCast(doc.webUrl), options.instant); if (DocListCast(this.props.Document[this.fieldKey + '-sidebar']).includes(doc) && !this.SidebarShown) { - this.toggleSidebar(!smooth); + this.toggleSidebar(options.instant); } if (this._sidebarRef?.current?.makeDocUnfiltered(doc)) return 1; if (doc !== this.rootDoc && this._outerRef.current) { const windowHeight = this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); const scrollTo = Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.layoutDoc._scrollTop), windowHeight, windowHeight * 0.1, Math.max(NumCast(doc.y) + doc[HeightSym](), this.getScrollHeight())); if (scrollTo !== undefined && this._initialScroll === undefined) { - const focusSpeed = smooth ? NumCast(doc.focusSpeed, 500) : 0; - this.goTo(scrollTo, focusSpeed); + const focusSpeed = options.instant ? 0 : options.zoomTime ?? 500; + this.goTo(scrollTo, focusSpeed, options.easeFunc); return focusSpeed; } else if (!this._webPageHasBeenRendered || !this.getScrollHeight() || this._initialScroll !== undefined) { this._initialScroll = scrollTo; @@ -305,6 +308,14 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }; getAnchor = () => { + let ele: Opt<HTMLDivElement> = undefined; + try { + const contents = this._iframe?.contentWindow?.getSelection()?.getRangeAt(0).cloneContents(); + if (contents) { + ele = document.createElement('div'); + ele.append(contents); + } + } catch (e) {} const anchor = this._getAnchor(this._savedAnnotations) ?? Docs.Create.WebanchorDocument(this._url, { @@ -312,6 +323,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps y: NumCast(this.layoutDoc._scrollTop), unrendered: true, }); + anchor.text = ele?.textContent ?? ''; + anchor.textHtml = ele?.innerHTML; this.addDocumentWrapper(anchor); return anchor; }; @@ -497,11 +510,11 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps ); }; - goTo = (scrollTop: number, duration: number) => { + goTo = (scrollTop: number, duration: number, easeFunc: 'linear' | 'ease' | undefined) => { if (this._outerRef.current) { const iframeHeight = Math.max(scrollTop, this._scrollHeight - this.panelHeight()); if (duration) { - smoothScroll(duration, [this._outerRef.current], scrollTop); + smoothScroll(duration, [this._outerRef.current], scrollTop, easeFunc); this.setDashScrollTop(scrollTop, duration); } else { this.setDashScrollTop(scrollTop); @@ -553,8 +566,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }; static urlHash = (s: string) => { + const split = s.split(''); return Math.abs( - s.split('').reduce((a: any, b: any) => { + split.reduce((a: any, b: any) => { a = (a << 5) - a + b.charCodeAt(0); return a & a; }, 0) @@ -686,7 +700,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } }; @action finishMarquee = (x?: number, y?: number, e?: PointerEvent) => { - this._getAnchor = AnchorMenu.Instance?.GetAnchor; this._marqueeing = undefined; this._isAnnotating = false; this._iframeClick = undefined; @@ -836,9 +849,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } } }); - sidebarWidth = () => - !this.SidebarShown ? 0 : WebBox.sidebarResizerWidth + (this._previewWidth ? WebBox.openSidebarWidth : ((NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc)) * this.props.PanelWidth()) / NumCast(this.layoutDoc.nativeWidth)); - + sidebarWidth = () => { + if (!this.SidebarShown) return 0; + if (this._previewWidth) return WebBox.sidebarResizerWidth + WebBox.openSidebarWidth; // return default sidebar if previewing (as in viewing a link target) + const nativeDiff = NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc); + return WebBox.sidebarResizerWidth + nativeDiff * (this.props.NativeDimScaling?.() || 1); + }; @computed get content() { const interactive = !this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick && this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && Doc.ActiveTool === InkTool.None && !DocumentDecorations.Instance?.Interacting; @@ -1000,9 +1016,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps docView={this.props.docViewPath().lastElement()} finishMarquee={this.finishMarquee} savedAnnotations={this.savedAnnotationsCreator} + selectionText={returnEmptyString} annotationLayer={this._annotationLayer.current} mainCont={this._mainCont.current} - />{' '} + /> </div> )} </div> @@ -1015,7 +1032,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }} onPointerDown={e => this.sidebarBtnDown(e, false)} /> - <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this.layoutDoc[WidthSym]()}%` }}> + <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this.props.PanelWidth()}%` }}> <SidebarAnnos ref={this._sidebarRef} {...this.props} @@ -1039,5 +1056,5 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } } ScriptingGlobals.add(function urlHash(url: string) { - return WebBox.urlHash(url); + return url ? WebBox.urlHash(url) : 0; }); diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx index 883c4460b..e477d7ae2 100644 --- a/src/client/views/nodes/button/FontIconBox.tsx +++ b/src/client/views/nodes/button/FontIconBox.tsx @@ -1,7 +1,6 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; -import Color from 'color'; import { action, computed, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -12,8 +11,9 @@ import { ScriptField } from '../../../../fields/ScriptField'; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; import { WebField } from '../../../../fields/URLField'; import { GestureUtils } from '../../../../pen-gestures/GestureUtils'; -import { aggregateBounds, Utils } from '../../../../Utils'; +import { aggregateBounds, StopEvent, Utils } from '../../../../Utils'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; +import { LinkManager } from '../../../util/LinkManager'; import { ScriptingGlobals } from '../../../util/ScriptingGlobals'; import { SelectionManager } from '../../../util/SelectionManager'; import { undoBatch, UndoManager } from '../../../util/UndoManager'; @@ -27,6 +27,7 @@ import { ActiveFillColor, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, SetAc import { InkTranscription } from '../../InkTranscription'; import { StyleProp } from '../../StyleProvider'; import { FieldView, FieldViewProps } from '.././FieldView'; +import { OpenWhere } from '../DocumentView'; import { RichTextMenu } from '../formattedText/RichTextMenu'; import { WebBox } from '../WebBox'; import { FontIconBadge } from './FontIconBadge'; @@ -62,7 +63,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { } showTemplate = (): void => { const dragFactory = Cast(this.layoutDoc.dragFactory, Doc, null); - dragFactory && this.props.addDocTab(dragFactory, 'add:right'); + dragFactory && this.props.addDocTab(dragFactory, OpenWhere.addRight); }; dragAsTemplate = (): void => { this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)'); @@ -198,7 +199,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { e.preventDefault(); }} onClick={action(() => (this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen))}> - <input style={{ width: 30 }} className="button-input" type="number" value={checkResult} onChange={action(e => setValue(Number(e.target.value)))} /> + <input style={{ width: 30 }} className="button-input" type="number" value={checkResult} onChange={undoBatch(action(e => setValue(Number(e.target.value))))} /> </div> <div className={`button`} onClick={action(e => setValue(Number(checkResult) + 1))}> <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={'plus'} /> @@ -221,7 +222,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { </div> ); } else { - return <div></div>; + return <div />; } } @@ -303,7 +304,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { fontFamily: script.script.originalScript.startsWith('setFont') ? value : undefined, backgroundColor: value === text ? Colors.LIGHT_BLUE : undefined, }} - onClick={() => script.script.run({ value }).result}> + onClick={undoBatch(() => script.script.run({ value }))}> {value[0].toUpperCase() + value.slice(1)} </div> )); @@ -352,12 +353,14 @@ export class FontIconBox extends DocComponent<ButtonProps>() { } colorPicker = (curColor: string) => { - const change = (value: ColorState) => { + const change = (value: ColorState, ev: MouseEvent) => { + ev.preventDefault(); + ev.stopPropagation(); const s = this.colorScript; s && undoBatch(() => s.script.run({ value: Utils.colorString(value), _readOnly_: false }).result)(); }; const presets = ['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']; - return <SketchPicker onChange={change} color={curColor} presetColors={presets} />; + return <SketchPicker onChange={change as any /* SketchPicker passes the mouse event to the callback, but the type system doesn't know that */} color={curColor} presetColors={presets} />; }; /** * Color button @@ -380,12 +383,15 @@ export class FontIconBox extends DocComponent<ButtonProps>() { // style={{ borderBottomRightRadius: this.dropdown ? 0 : undefined }}> // <FontAwesomeIcon icon={'caret-down'} color={color} size="sm" /> // </div>; - setTimeout(() => this.colorPicker(curColor)); // cause an update to the color picker rendered in MainView + //setTimeout(() => this.colorPicker(curColor)); // cause an update to the color picker rendered in MainView return ( <div className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? 'Label' : '')} ${this.colorPickerClosed}`} style={{ color: color, borderBottomLeftRadius: this.dropdown ? 0 : undefined }} - onClick={action(() => (this.colorPickerClosed = !this.colorPickerClosed))} + onClick={action(e => { + this.colorPickerClosed = !this.colorPickerClosed; + e.stopPropagation(); + })} onPointerDown={e => e.stopPropagation()}> {this.Icon(color)} <div className="colorButton-color" style={{ backgroundColor: curColor }} /> @@ -393,7 +399,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { {/* {dropdownCaret} */} {this.colorPickerClosed ? null : ( <div> - <div className="menuButton-dropdownBox" onPointerDown={e => e.stopPropagation()} onClick={e => e.stopPropagation()}> + <div className="menuButton-dropdownBox" onPointerDown={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onClick={e => e.stopPropagation()}> {this.colorPicker(curColor)} </div> <div @@ -504,8 +510,12 @@ export class FontIconBox extends DocComponent<ButtonProps>() { case ButtonType.EditableText: return this.editableText; case ButtonType.DropdownButton: button = this.dropdownButton; break; case ButtonType.ToggleButton: button = this.toggleButton; break; - case ButtonType.TextButton: button = ( - <div className={`menuButton ${this.type}`} style={{ color, backgroundColor, opacity: 1, gridAutoColumns: `${NumCast(this.rootDoc._height)} auto` }}> + case ButtonType.TextButton: + // Script for checking the outcome of the toggle + const script = ScriptCast(this.rootDoc.script); + const checkResult = script?.script.run({ _readOnly_: true }).result; + button = ( + <div className={`menuButton ${this.type}`} style={{ color, backgroundColor:checkResult ?? backgroundColor, opacity: 1, gridAutoColumns: `${NumCast(this.rootDoc._height)} auto` }}> {this.Icon(color)} {StrCast(this.rootDoc.buttonText) ? <div className="button-text">{StrCast(this.rootDoc.buttonText)}</div> : null} {label()} @@ -542,11 +552,11 @@ ScriptingGlobals.add(function setView(view: string) { // toggle: Set overlay status of selected document ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: boolean) { - const selected = SelectionManager.Views().lastElement(); + const selected = SelectionManager.Views().lastElement()?.props.Document ?? LinkManager.currentLink; if (checkResult) { - return selected?.props.Document._backgroundColor ?? 'transparent'; + return selected?._backgroundColor ?? 'transparent'; } - if (selected) selected.props.Document._backgroundColor = color; + if (selected) selected._backgroundColor = color; }); // toggle: Set overlay status of selected document diff --git a/src/client/views/nodes/formattedText/DashDocCommentView.tsx b/src/client/views/nodes/formattedText/DashDocCommentView.tsx index 40dd6fbc7..fcd6e0c55 100644 --- a/src/client/views/nodes/formattedText/DashDocCommentView.tsx +++ b/src/client/views/nodes/formattedText/DashDocCommentView.tsx @@ -1,5 +1,5 @@ import { TextSelection } from 'prosemirror-state'; -import * as ReactDOM from 'react-dom'; +import * as ReactDOM from 'react-dom/client'; import { Doc } from '../../../../fields/Doc'; import { DocServer } from '../../../DocServer'; import React = require('react'); @@ -9,6 +9,7 @@ import React = require('react'); // the comment can be toggled on/off with the '<-' text anchor. export class DashDocCommentView { dom: HTMLDivElement; // container for label and value + root: any; constructor(node: any, view: any, getPos: any) { this.dom = document.createElement('div'); @@ -30,15 +31,20 @@ export class DashDocCommentView { e.stopPropagation(); }; - ReactDOM.render(<DashDocCommentViewInternal view={view} getPos={getPos} docid={node.attrs.docid} />, this.dom); + this.root = ReactDOM.createRoot(this.dom); + this.root.render(<DashDocCommentViewInternal view={view} getPos={getPos} docid={node.attrs.docid} />); (this as any).dom = this.dom; } destroy() { - ReactDOM.unmountComponentAtNode(this.dom); + this.root.unmount(); + } + deselectNode() { + this.dom.classList.remove('ProseMirror-selectednode'); + } + selectNode() { + this.dom.classList.add('ProseMirror-selectednode'); } - - selectNode() {} } interface IDashDocCommentViewInternal { diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx index 5f576be41..722d0cc4f 100644 --- a/src/client/views/nodes/formattedText/DashDocView.tsx +++ b/src/client/views/nodes/formattedText/DashDocView.tsx @@ -1,7 +1,7 @@ import { action, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import { NodeSelection } from 'prosemirror-state'; -import * as ReactDOM from 'react-dom'; +import * as ReactDOM from 'react-dom/client'; import { Doc, HeightSym, WidthSym } from '../../../../fields/Doc'; import { Cast, NumCast, StrCast } from '../../../../fields/Types'; import { emptyFunction, returnFalse, Utils } from '../../../../Utils'; @@ -15,6 +15,7 @@ import React = require('react'); export class DashDocView { dom: HTMLSpanElement; // container for label and value + root: any; constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) { this.dom = document.createElement('span'); @@ -38,14 +39,14 @@ export class DashDocView { e.stopPropagation(); }; - ReactDOM.render( - <DashDocViewInternal docid={node.attrs.docid} alias={node.attrs.alias} width={node.attrs.width} height={node.attrs.height} hidden={node.attrs.hidden} fieldKey={node.attrs.fieldKey} tbox={tbox} view={view} node={node} getPos={getPos} />, - this.dom + this.root = ReactDOM.createRoot(this.dom); + this.root.render( + <DashDocViewInternal docid={node.attrs.docid} alias={node.attrs.alias} width={node.attrs.width} height={node.attrs.height} hidden={node.attrs.hidden} fieldKey={node.attrs.fieldKey} tbox={tbox} view={view} node={node} getPos={getPos} /> ); - (this as any).dom = this.dom; } destroy() { - ReactDOM.unmountComponentAtNode(this.dom); + this.root.unmount(); + // ReactDOM.unmountComponentAtNode(this.dom); } selectNode() {} } diff --git a/src/client/views/nodes/formattedText/DashFieldView.scss b/src/client/views/nodes/formattedText/DashFieldView.scss index c36e6804b..ad315acc8 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.scss +++ b/src/client/views/nodes/formattedText/DashFieldView.scss @@ -1,3 +1,5 @@ +@import '../../global/globalCssVariables'; + .dashFieldView { position: relative; display: inline-flex; @@ -22,7 +24,7 @@ position: relative; display: inline-block; font-weight: normal; - background: rgba(0,0,0,0.1); + background: rgba(0, 0, 0, 0.1); } .dashFieldView-fieldSpan { min-width: 8px; @@ -31,11 +33,13 @@ padding-left: 2px; display: inline-block; background-color: rgba(155, 155, 155, 0.24); - font-weight: bold; span { + user-select: all; min-width: 100%; display: inline-block; } } } -
\ No newline at end of file +.ProseMirror-selectedNode { + outline: solid 1px $light-blue !important; +} diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx index 35d919f38..63347015b 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.tsx +++ b/src/client/views/nodes/formattedText/DashFieldView.tsx @@ -1,6 +1,6 @@ import { action, computed, IReactionDisposer, observable } from 'mobx'; import { observer } from 'mobx-react'; -import * as ReactDOM from 'react-dom'; +import * as ReactDOM from 'react-dom/client'; import { DataSym, Doc, DocListCast, Field } from '../../../../fields/Doc'; import { List } from '../../../../fields/List'; import { listSpec } from '../../../../fields/Schema'; @@ -16,20 +16,19 @@ import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu'; import { Tooltip } from '@material-ui/core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { CollectionViewType } from '../../../documents/DocumentTypes'; +import { NodeSelection } from 'prosemirror-state'; +import { OpenWhere } from '../DocumentView'; export class DashFieldView { dom: HTMLDivElement; // container for label and value + root: any; constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) { - const { boolVal, strVal } = DashFieldViewInternal.fieldContent(tbox.props.Document, tbox.rootDoc, node.attrs.fieldKey); - this.dom = document.createElement('div'); this.dom.style.width = node.attrs.width; this.dom.style.height = node.attrs.height; - this.dom.style.fontWeight = 'bold'; this.dom.style.position = 'relative'; this.dom.style.display = 'inline-block'; - this.dom.textContent = node.attrs.fieldKey.startsWith('#') ? node.attrs.fieldKey : node.attrs.fieldKey + ' ' + strVal; this.dom.onkeypress = function (e: any) { e.stopPropagation(); }; @@ -43,13 +42,20 @@ export class DashFieldView { e.stopPropagation(); }; - setTimeout(() => ReactDOM.render(<DashFieldViewInternal fieldKey={node.attrs.fieldKey} docid={node.attrs.docid} width={node.attrs.width} height={node.attrs.height} hideKey={node.attrs.hideKey} tbox={tbox} />, this.dom)); - (this as any).dom = this.dom; + this.root = ReactDOM.createRoot(this.dom); + this.root.render( + <DashFieldViewInternal node={node} getPos={getPos} fieldKey={node.attrs.fieldKey} docid={node.attrs.docid} width={node.attrs.width} height={node.attrs.height} hideKey={node.attrs.hideKey} editable={node.attrs.editable} tbox={tbox} /> + ); } destroy() { - ReactDOM.unmountComponentAtNode(this.dom); + this.root.unmount(); + } + deselectNode() { + this.dom.classList.remove('ProseMirror-selectednode'); + } + selectNode() { + this.dom.classList.add('ProseMirror-selectednode'); } - selectNode() {} } interface IDashFieldViewInternal { @@ -59,6 +65,9 @@ interface IDashFieldViewInternal { tbox: FormattedTextBox; width: number; height: number; + editable: boolean; + node: any; + getPos: any; } @observer @@ -125,7 +134,13 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna r?.addEventListener('blur', e => r && this.updateText(r.textContent!, false)); r?.addEventListener( 'pointerdown', - action(e => e.stopPropagation()) + action(e => { + // let target = e.target as any; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span> + // while (target && !target.dataset?.targethrefs) target = target.parentElement; + this.props.tbox.EditorView!.dispatch(this.props.tbox.EditorView!.state.tr.setSelection(new NodeSelection(this.props.tbox.EditorView!.state.doc.resolve(this.props.getPos())))); + // FormattedTextBoxComment.update(this.props.tbox, this.props.tbox.EditorView!, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc); + // e.stopPropagation(); + }) ); }}> {strVal} @@ -138,6 +153,10 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna // we need to handle all key events on the input span or else they will propagate to prosemirror. @action fieldSpanKeyDown = (e: KeyboardEvent, span: HTMLSpanElement) => { + if (e.key === 'c' && (e.ctrlKey || e.metaKey)) { + navigator.clipboard.writeText(window.getSelection()?.toString() || ''); + return; + } if (e.key === 'Enter') { // handle the enter key by "submitting" the current text to Dash's database. this.updateText(span.textContent!, true); @@ -153,6 +172,9 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna } e.preventDefault(); //prevent default so that all the text in the prosemirror text box isn't selected } + if (!this.props.editable) { + e.preventDefault(); + } e.stopPropagation(); // we need to handle all events or else they will propagate to prosemirror. }; @@ -160,7 +182,6 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna updateText = (nodeText: string, forceMatch: boolean) => { if (nodeText) { const newText = nodeText.startsWith(':=') || nodeText.startsWith('=:=') ? ':=-computed-' : nodeText; - // look for a document whose id === the fieldKey being displayed. If there's a match, then that document // holds the different enumerated values for the field in the titles of its collected documents. // if there's a partial match from the start of the input text, complete the text --- TODO: make this an auto suggest box and select from a drop down. @@ -207,7 +228,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, 'add:right'); + this.props.tbox.props.addDocTab(alias, OpenWhere.addRight); } }; @@ -266,14 +287,12 @@ export class DashFieldViewMenu extends AntimodeMenu<AntimodeMenuProps> { document.addEventListener('pointerdown', hideMenu, true); }; render() { - const buttons = [ + return this.getElement([ <Tooltip key="trash" title={<div className="dash-tooltip">{`Show Pivot Viewer for '${this._fieldKey}'`}</div>}> <button className="antimodeMenu-button" onPointerDown={this.showFields}> <FontAwesomeIcon icon="eye" size="lg" /> </button> </Tooltip>, - ]; - - return this.getElement(buttons); + ]); } } diff --git a/src/client/views/nodes/formattedText/EquationView.tsx b/src/client/views/nodes/formattedText/EquationView.tsx index 4895dcdc5..714ae458c 100644 --- a/src/client/views/nodes/formattedText/EquationView.tsx +++ b/src/client/views/nodes/formattedText/EquationView.tsx @@ -1,8 +1,8 @@ import EquationEditor from 'equation-editor-react'; -import { IReactionDisposer } from 'mobx'; +import { IReactionDisposer, trace } from 'mobx'; import { observer } from 'mobx-react'; import { TextSelection } from 'prosemirror-state'; -import * as ReactDOM from 'react-dom'; +import * as ReactDOM from 'react-dom/client'; import { Doc } from '../../../../fields/Doc'; import { StrCast } from '../../../../fields/Types'; import './DashFieldView.scss'; @@ -11,8 +11,12 @@ import React = require('react'); export class EquationView { dom: HTMLDivElement; // container for label and value - + root: any; + tbox: FormattedTextBox; + view: any; constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) { + this.tbox = tbox; + this.view = view; this.dom = document.createElement('div'); this.dom.style.width = node.attrs.width; this.dom.style.height = node.attrs.height; @@ -22,19 +26,24 @@ export class EquationView { e.stopPropagation(); }; - ReactDOM.render(<EquationViewInternal fieldKey={node.attrs.fieldKey} width={node.attrs.width} height={node.attrs.height} getPos={getPos} setEditor={this.setEditor} tbox={tbox} />, this.dom); - (this as any).dom = this.dom; + this.root = ReactDOM.createRoot(this.dom); + this.root.render(<EquationViewInternal fieldKey={node.attrs.fieldKey} width={node.attrs.width} height={node.attrs.height} getPos={getPos} setEditor={this.setEditor} tbox={tbox} />); } _editor: EquationEditor | undefined; setEditor = (editor?: EquationEditor) => (this._editor = editor); destroy() { - ReactDOM.unmountComponentAtNode(this.dom); + this.root.unmount(); + // ReactDOM.unmountComponentAtNode(this.dom); } setSelection() { this._editor?.mathField.focus(); } selectNode() { - this._editor?.mathField.focus(); + this.tbox._applyingChange = this.tbox.fieldKey; // setting focus will make prosemirror lose focus, which will cause it to change its selection to a text selection, which causes this view to get rebuilt but it's no longer node selected, so the equationview won't have focus + setTimeout(() => { + this._editor?.mathField.focus(); + setTimeout(() => (this.tbox._applyingChange = '')); + }); } deselectNode() {} } diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss index d3d8c47c0..cbe0a465d 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss @@ -15,7 +15,7 @@ audiotag { position: absolute; cursor: pointer; border-radius: 10px; - width: 10px; + width: 12px; margin-top: -2px; font-size: 4px; background: lightblue; @@ -225,6 +225,8 @@ footnote::after { .prosemirror-attribution { font-size: 8px; + float: right; + display: inline; } .footnote-tooltip::before { @@ -740,6 +742,8 @@ footnote::after { .prosemirror-attribution { font-size: 8px; + float: right; + display: inline; } .footnote-tooltip::before { diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 096f9a92c..62e215521 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -1,7 +1,7 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { isEqual } from 'lodash'; -import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +import { action, computed, IReactionDisposer, observable, reaction, runInAction, trace } from 'mobx'; import { observer } from 'mobx-react'; import { baseKeymap, selectAll } from 'prosemirror-commands'; import { history } from 'prosemirror-history'; @@ -45,7 +45,7 @@ import { LightboxView } from '../../LightboxView'; import { AnchorMenu } from '../../pdf/AnchorMenu'; import { SidebarAnnos } from '../../SidebarAnnos'; import { StyleProp } from '../../StyleProvider'; -import { DocumentViewInternal } from '../DocumentView'; +import { DocFocusOptions, DocumentViewInternal, OpenWhere } from '../DocumentView'; import { FieldView, FieldViewProps } from '../FieldView'; import { LinkDocPreview } from '../LinkDocPreview'; import { DashDocCommentView } from './DashDocCommentView'; @@ -87,7 +87,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps private _ref: React.RefObject<HTMLDivElement> = React.createRef(); private _scrollRef: React.RefObject<HTMLDivElement> = React.createRef(); private _editorView: Opt<EditorView>; - private _applyingChange: string = ''; + public _applyingChange: string = ''; private _searchIndex = 0; private _lastTimedMark: Mark | undefined = undefined; private _cachedLinks: Doc[] = []; @@ -231,7 +231,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } } - getAnchor = () => this.makeLinkAnchor(undefined, 'add:right', undefined, 'Anchored Selection'); + getAnchor = () => this.makeLinkAnchor(undefined, 'add:right', undefined, 'Anchored Selection', false); @action setupAnchorMenu = () => { @@ -239,7 +239,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps AnchorMenu.Instance.OnClick = (e: PointerEvent) => { !this.layoutDoc.showSidebar && this.toggleSidebar(); - this._sidebarRef.current?.anchorMenuClick(this.getAnchor()); + setTimeout(() => this._sidebarRef.current?.anchorMenuClick(this.makeLinkAnchor(undefined, 'add:right', undefined, 'Anchored Selection', true))); // give time for sidebarRef to be created }; AnchorMenu.Instance.OnAudio = (e: PointerEvent) => { !this.layoutDoc.showSidebar && this.toggleSidebar(); @@ -393,13 +393,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const newAutoLinks = new Set<Doc>(); const oldAutoLinks = DocListCast(this.props.Document.links).filter(link => link.linkRelationship === LinkManager.AutoKeywords); if (this._editorView?.state.doc.textContent) { + const isNodeSel = this._editorView.state.selection instanceof NodeSelection; const f = this._editorView.state.selection.from; const t = this._editorView.state.selection.to; var tr = this._editorView.state.tr as any; const autoAnch = this._editorView.state.schema.marks.autoLinkAnchor; tr = tr.removeMark(0, tr.doc.content.size, autoAnch); DocListCast(Doc.MyPublishedDocs.data).forEach(term => (tr = this.hyperlinkTerm(tr, term, newAutoLinks))); - tr = tr.setSelection(new TextSelection(tr.doc.resolve(f), tr.doc.resolve(t))); + tr = tr.setSelection(isNodeSel && false ? new NodeSelection(tr.doc.resolve(f)) : new TextSelection(tr.doc.resolve(f), tr.doc.resolve(t))); this._editorView?.dispatch(tr); } oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.anchor2 !== this.rootDoc).forEach(LinkManager.Instance.deleteLink); @@ -498,12 +499,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const end = this._editorView.state.doc.nodeSize - 2; this._editorView.dispatch(this._editorView.state.tr.removeMark(0, end, mark).removeMark(0, end, activeMark)); } - if (FormattedTextBox.PasteOnLoad) { - const pdfDocId = FormattedTextBox.PasteOnLoad.clipboardData?.getData('dash/pdfOrigin'); - const pdfRegionId = FormattedTextBox.PasteOnLoad.clipboardData?.getData('dash/pdfRegion'); - FormattedTextBox.PasteOnLoad = undefined; - setTimeout(() => pdfDocId && pdfRegionId && this.addPdfReference(pdfDocId, pdfRegionId, undefined), 10); - } }; adoptAnnotation = (start: number, end: number, mark: Mark) => { const view = this._editorView!; @@ -691,12 +686,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps }; @undoBatch - pinToPres = (anchor: Doc) => this.props.pinToPres(anchor); + pinToPres = (anchor: Doc) => this.props.pinToPres(anchor, {}); @undoBatch - makePushpin = (anchor: Doc) => (anchor.isPushpin = !anchor.isPushpin); + makePushpin = (anchor: Doc) => (anchor.followLinkToggle = !anchor.followLinkToggle); - isPushpin = (anchor: Doc) => BoolCast(anchor.isPushpin); + isPushpin = (anchor: Doc) => BoolCast(anchor.followLinkToggle); specificContextMenu = (e: React.MouseEvent): void => { const cm = ContextMenu.Instance; @@ -896,9 +891,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps }; // TODO: nda -- Look at how link anchors are added - makeLinkAnchor(anchorDoc?: Doc, location?: string, targetHref?: string, title?: string) { + makeLinkAnchor(anchorDoc?: Doc, location?: string, targetHref?: string, title?: string, noPreview?: boolean) { const state = this._editorView?.state; if (state) { + let selectedText = ''; const sel = state.selection; const splitter = state.schema.marks.splitter.create({ id: Utils.GenerateGuid() }); let tr = state.tr.addMark(sel.from, sel.to, splitter); @@ -910,13 +906,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps if (node.firstChild === null && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) { const allAnchors = [{ href, title, anchorId: anchor[Id] }]; allAnchors.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.linkAnchor.name)?.attrs.allAnchors ?? [])); - const link = state.schema.marks.linkAnchor.create({ allAnchors, title, location }); + const link = state.schema.marks.linkAnchor.create({ allAnchors, title, location, noPreview }); tr = tr.addMark(pos, pos + node.nodeSize, link); + selectedText += (node as Node).textContent; } }); this.dataDoc[ForceServerWrite] = this.dataDoc[UpdatingFromServer] = true; // need to allow permissions for adding links to readonly/augment only documents this._editorView!.dispatch(tr.removeMark(sel.from, sel.to, splitter)); this.dataDoc[UpdatingFromServer] = this.dataDoc[ForceServerWrite] = false; + anchor.text = selectedText; return anchor; } return anchorDoc ?? this.rootDoc; @@ -924,10 +922,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps return anchorDoc ?? this.rootDoc; } - scrollFocus = (textAnchor: Doc, smooth: boolean) => { + scrollFocus = (textAnchor: Doc, options: DocFocusOptions) => { let didToggle = false; if (DocListCast(this.Document[this.fieldKey + '-sidebar']).includes(textAnchor) && !this.SidebarShown) { - this.toggleSidebar(!smooth); + this.toggleSidebar(options.instant); didToggle = true; } const textAnchorId = textAnchor[Id]; @@ -968,7 +966,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const content = (ret.frag as any)?.content; if ((ret.frag.size > 2 || (content?.length && content[0].type === this._editorView.state.schema.nodes.audiotag)) && ret.start >= 0) { - smooth && (this._focusSpeed = 500); + !options.instant && (this._focusSpeed = 500); let selection = TextSelection.near(editor.state.doc.resolve(ret.start)); // default to near the start if (ret.frag.firstChild) { selection = TextSelection.between(editor.state.doc.resolve(ret.start), editor.state.doc.resolve(ret.start + ret.frag.firstChild.nodeSize)); // bcz: looks better to not have the target selected @@ -1122,7 +1120,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const durationSecStr = viewTrans.match(/([0-9.]*)s/); const duration = durationMiliStr ? Number(durationMiliStr[1]) : durationSecStr ? Number(durationSecStr[1]) * 1000 : 0; if (duration) { - smoothScroll(duration, this._scrollRef.current, Math.abs(pos || 0)); + this._scrollStopper = smoothScroll(duration, this._scrollRef.current, Math.abs(pos || 0), 'ease', this._scrollStopper); } else { this._scrollRef.current.scrollTo({ top: pos }); } @@ -1132,6 +1130,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps ); quickScroll = undefined; this.tryUpdateScrollHeight(); + setTimeout(this.tryUpdateScrollHeight, 250); } pushToGoogleDoc = async () => { @@ -1233,61 +1232,38 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps }; handlePaste = (view: EditorView, event: Event, slice: Slice): boolean => { - const cbe = event as ClipboardEvent; - const pdfDocId = cbe.clipboardData?.getData('dash/pdfOrigin'); - const pdfRegionId = cbe.clipboardData?.getData('dash/pdfRegion'); - return pdfDocId && pdfRegionId && this.addPdfReference(pdfDocId, pdfRegionId, slice) ? true : false; + const pdfAnchorId = (event as ClipboardEvent).clipboardData?.getData('dash/pdfAnchor'); + return pdfAnchorId && this.addPdfReference(pdfAnchorId) ? true : false; }; - addPdfReference = (pdfDocId: string, pdfRegionId: string, slice?: Slice) => { + addPdfReference = (pdfAnchorId: string) => { const view = this._editorView!; - if (pdfDocId && pdfRegionId) { - DocServer.GetRefField(pdfDocId).then(pdfDoc => { - DocServer.GetRefField(pdfRegionId).then(pdfRegion => { - if (pdfDoc instanceof Doc && pdfRegion instanceof Doc) { - setTimeout(async () => { - const targetField = Doc.LayoutFieldKey(pdfDoc); - const targetAnnotations = await DocListCastAsync(pdfDoc[DataSym][targetField + '-annotations']); // bcz: better to have the PDF's view handle updating its own annotations - if (targetAnnotations) targetAnnotations.push(pdfRegion); - else Doc.AddDocToList(pdfDoc[DataSym], targetField + '-annotations', pdfRegion); - }); - - const link = DocUtils.MakeLink({ doc: this.rootDoc }, { doc: pdfRegion }, 'PDF pasted'); - if (link) { - const linkId = link[Id]; - const quote = view.state.schema.nodes.blockquote.create({ content: addMarkToFrag(slice?.content || view.state.doc.content, (node: Node) => addLinkMark(node, StrCast(pdfDoc.title), linkId)) }); - const newSlice = new Slice(Fragment.from(quote), slice?.openStart || 0, slice?.openEnd || 0); - if (slice) { - view.dispatch(view.state.tr.replaceSelection(newSlice).scrollIntoView().setMeta('paste', true).setMeta('uiEvent', 'paste')); - } else { - selectAll(view.state, (tx: Transaction) => view.dispatch(tx.replaceSelection(newSlice).scrollIntoView())); - } - } + if (pdfAnchorId) { + DocServer.GetRefField(pdfAnchorId).then(pdfAnchor => { + if (pdfAnchor instanceof Doc) { + const dashField = view.state.schema.nodes.paragraph.create({}, [ + view.state.schema.nodes.dashField.create({ fieldKey: 'text', docid: pdfAnchor[Id], hideKey: true, editable: false }, undefined, [ + view.state.schema.marks.linkAnchor.create({ + allAnchors: [{ href: `/doc/${this.rootDoc[Id]}`, title: this.rootDoc.title, anchorId: `${this.rootDoc[Id]}` }], + location: 'add:right', + title: `from: ${DocCast(pdfAnchor.context).title}`, + noPreview: true, + docref: false, + }), + view.state.schema.marks.pFontSize.create({ fontSize: '8px' }), + view.state.schema.marks.em.create({}), + ]), + ]); + + const link = DocUtils.MakeLink({ doc: pdfAnchor }, { doc: this.rootDoc }, 'PDF pasted'); + if (link) { + view.dispatch(view.state.tr.replaceSelectionWith(dashField, false).scrollIntoView().setMeta('paste', true).setMeta('uiEvent', 'paste')); } - }); + } }); return true; } return false; - - function addMarkToFrag(frag: Fragment, marker: (node: Node) => Node) { - const nodes: Node[] = []; - frag.forEach(node => nodes.push(marker(node))); - return Fragment.fromArray(nodes); - } - - function addLinkMark(node: Node, title: string, linkId: string) { - if (!node.isText) { - const content = addMarkToFrag(node.content, (node: Node) => addLinkMark(node, title, linkId)); - return node.copy(content); - } - const marks = [...node.marks]; - const linkIndex = marks.findIndex(mark => mark.type.name === 'link'); - const allLinks = [{ href: Doc.globalServerPath(linkId), title, linkId }]; - 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); - } }; isActiveTab(el: Element | null | undefined) { @@ -1308,6 +1284,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps }); } _didScroll = false; + _scrollStopper: undefined | (() => void); setupEditor(config: any, fieldKey: string) { const curText = Cast(this.dataDoc[this.fieldKey], RichTextField, null) || StrCast(this.dataDoc[this.fieldKey]); const rtfField = Cast((!curText && this.layoutDoc[this.fieldKey]) || this.dataDoc[fieldKey], RichTextField); @@ -1326,7 +1303,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const shift = Math.min(topOff ?? Number.MAX_VALUE, botOff ?? Number.MAX_VALUE); const scrollPos = scrollRef.scrollTop + shift * self.props.ScreenToLocalTransform().Scale; if (this._focusSpeed !== undefined) { - scrollPos && smoothScroll(this._focusSpeed, scrollRef, scrollPos); + scrollPos && (this._scrollStopper = smoothScroll(this._focusSpeed, scrollRef, scrollPos, 'ease', this._scrollStopper)); } else { scrollRef.scrollTo({ top: scrollPos }); } @@ -1397,11 +1374,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } else if (this._editorView) { this._editorView.dispatch(this._editorView.state.tr.addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) }))); } - FormattedTextBox.DontSelectInitialText = false; } selectOnLoad && this._editorView!.focus(); // add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet. if (this._editorView) { + const tr = this._editorView.state.tr; + const { from, to } = tr.selection; + // for some reason, the selection is sometimes lost in the sidebar view when prosemirror syncs the seledtion with the dom, so reset the selectoin after the document has ben fully instantiated. + if (FormattedTextBox.DontSelectInitialText) setTimeout(() => this._editorView?.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(from), tr.doc.resolve(to)))), 250); this._editorView.state.storedMarks = [ ...(this._editorView.state.storedMarks ?? []), ...(!this._editorView.state.storedMarks?.some(mark => mark.type === schema.marks.user_mark) ? [schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })] : []), @@ -1412,7 +1392,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps ...(Doc.UserDoc().fontSize ? [schema.mark(schema.marks.pFontSize, { fontSize: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.FontSize) })] : []), ...(Doc.UserDoc().fontWeight === 'bold' ? [schema.mark(schema.marks.strong)] : []), ]; + if (FormattedTextBox.PasteOnLoad) { + const pdfAnchorId = FormattedTextBox.PasteOnLoad.clipboardData?.getData('dash/pdfAnchor'); + FormattedTextBox.PasteOnLoad = undefined; + pdfAnchorId && this.addPdfReference(pdfAnchorId); + } } + FormattedTextBox.DontSelectInitialText = false; } componentWillUnmount() { @@ -1443,7 +1429,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const func = () => { const docView = DocumentManager.Instance.getDocumentView(audiodoc); if (!docView) { - this.props.addDocTab(audiodoc, 'add:bottom'); + this.props.addDocTab(audiodoc, OpenWhere.addBottom); setTimeout(func); } else docView.ComponentView?.playFrom?.(timecode, Cast(anchor.timecodeToHide, 'number', null)); // bcz: would be nice to find the next audio tag in the doc and play until that }; @@ -1480,7 +1466,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps document.removeEventListener('pointermove', this.onSelectMove); }; onPointerUp = (e: React.PointerEvent): void => { - if (!this._editorView?.state.selection.empty && FormattedTextBox._canAnnotate && !(e.nativeEvent as any).dash) this.setupAnchorMenu(); + if (!this._editorView?.state.selection.empty && !(this._editorView?.state.selection instanceof NodeSelection) && FormattedTextBox._canAnnotate && !(e.nativeEvent as any).dash) this.setupAnchorMenu(); if (!this._downEvent) return; this._downEvent = false; if (this.props.isContentActive(true) && !(e.nativeEvent as any).dash) { @@ -1489,7 +1475,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps !this.props.isSelected(true) && editor.dispatch(editor.state.tr.setSelection(new TextSelection(editor.state.doc.resolve(pcords?.pos || 0)))); let target = e.target as any; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span> while (target && !target.dataset?.targethrefs) target = target.parentElement; - FormattedTextBoxComment.update(this, editor, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc); + FormattedTextBoxComment.update(this, editor, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc, target?.dataset.nopreview === 'true'); + if (target) return; } if (e.button === 0 && this.props.isSelected(true) && !e.altKey) { @@ -1524,6 +1511,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps //applyDevTools.applyDevTools(this._editorView); FormattedTextBox.Focused = this; this.ProseRef?.children[0] === e.nativeEvent.target && this._editorView && RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props); + this.startUndoTypingBatch(); }; @observable public static Focused: FormattedTextBox | undefined; diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx index bdf59863b..e7ca26d5c 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx @@ -1,5 +1,5 @@ import { Mark, ResolvedPos } from 'prosemirror-model'; -import { EditorState } from 'prosemirror-state'; +import { EditorState, NodeSelection } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; import { Doc } from '../../../../fields/Doc'; import { DocServer } from '../../../DocServer'; @@ -92,7 +92,7 @@ export class FormattedTextBoxComment { FormattedTextBoxComment.tooltip.style.display = ''; } - static update(textBox: FormattedTextBox, view: EditorView, lastState?: EditorState, hrefs: string = '', linkDoc: string = '') { + static update(textBox: FormattedTextBox, view: EditorView, lastState?: EditorState, hrefs: string = '', linkDoc: string = '', noPreview: boolean = false) { FormattedTextBoxComment.textBox = textBox; if (hrefs || !lastState?.doc.eq(view.state.doc) || !lastState?.selection.eq(view.state.selection)) { FormattedTextBoxComment.setupPreview( @@ -102,12 +102,13 @@ export class FormattedTextBoxComment { ?.trim() .split(' ') .filter(h => h), - linkDoc + linkDoc, + noPreview ); } } - static setupPreview(view: EditorView, textBox: FormattedTextBox, hrefs?: string[], linkDoc?: string) { + static setupPreview(view: EditorView, textBox: FormattedTextBox, hrefs?: string[], linkDoc?: string, noPreview?: boolean) { const state = view.state; // this section checks to see if the insertion point is over text entered by a different user. If so, it sets ths comment text to indicate the user and the modification date if (state.selection.$from) { @@ -130,8 +131,8 @@ export class FormattedTextBoxComment { if (state.selection.$from && hrefs?.length) { const nbef = findStartOfMark(state.selection.$from, view, findLinkMark); const naft = findEndOfMark(state.selection.$from, view, findLinkMark) || nbef; - nbef && - naft && + //nbef && + naft && LinkDocPreview.SetLinkInfo({ docProps: textBox.props, linkSrc: textBox.rootDoc, @@ -139,6 +140,7 @@ export class FormattedTextBoxComment { location: (pos => [pos.left, pos.top + 25])(view.coordsAtPos(state.selection.from - Math.max(0, nbef - 1))), hrefs, showHeader: true, + noPreview, }); } } diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts index 3d9bd6add..68b0488a2 100644 --- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts +++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts @@ -9,6 +9,7 @@ import { GetEffectiveAcl } from '../../../../fields/util'; import { Utils } from '../../../../Utils'; import { Docs } from '../../../documents/Documents'; import { SelectionManager } from '../../../util/SelectionManager'; +import { OpenWhere } from '../DocumentView'; import { liftListItem, sinkListItem } from './prosemirrorPatches.js'; const mac = typeof navigator !== 'undefined' ? /Mac/.test(navigator.platform) : false; @@ -135,7 +136,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey //Command to create a new Tab with a PDF of all the command shortcuts bind('Mod-/', (state: EditorState, dispatch: (tx: Transaction) => void) => { const newDoc = Docs.Create.PdfDocument(Utils.prepend('/assets/cheat-sheet.pdf'), { _width: 300, _height: 300 }); - props.addDocTab(newDoc, 'add:right'); + props.addDocTab(newDoc, OpenWhere.addRight); }); //Commands to modify BlockType diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index 47833dd43..e3c67ad2e 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -248,8 +248,7 @@ export class RichTextRules { // [[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]+)?\]\]$/), (state, match, start, end) => { const fieldKey = match[1]; - const rawdocid = match[3]; - const docid = rawdocid ? normalizeEmail(!rawdocid.includes('@') ? Doc.CurrentUserEmail + rawdocid : rawdocid.substring(1)) : undefined; + const docid = match[3]?.replace(':', ''); const value = match[2]?.substring(1); if (!fieldKey) { if (docid) { @@ -259,7 +258,7 @@ export class RichTextRules { if (rstate) { this.TextBox.EditorView?.dispatch(rstate.tr.setSelection(new TextSelection(rstate.doc.resolve(start), rstate.doc.resolve(end - 3)))); } - const target = (docx instanceof Doc && docx) || Docs.Create.FreeformDocument([], { title: rawdocid.replace(/^:/, ''), _width: 500, _height: 500 }, docid); + const target = (docx instanceof Doc && docx) || Docs.Create.FreeformDocument([], { title: docid, _width: 500, _height: 500 }, docid); DocUtils.MakeLink({ doc: this.TextBox.getAnchor() }, { doc: target }, 'portal to:portal from', undefined); const fstate = this.TextBox.EditorView?.state; @@ -275,7 +274,7 @@ export class RichTextRules { const num = value.match(/^[0-9.]$/); this.Document[DataSym][fieldKey] = value === 'true' ? true : value === 'false' ? false : num ? Number(value) : value; } - const fieldView = state.schema.nodes.dashField.create({ fieldKey, docid }); + const fieldView = state.schema.nodes.dashField.create({ fieldKey, docid, hideKey: false }); return state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).replaceSelectionWith(fieldView, true); }), diff --git a/src/client/views/nodes/formattedText/SummaryView.tsx b/src/client/views/nodes/formattedText/SummaryView.tsx index 01acc3de9..4e75d374c 100644 --- a/src/client/views/nodes/formattedText/SummaryView.tsx +++ b/src/client/views/nodes/formattedText/SummaryView.tsx @@ -1,6 +1,6 @@ import { TextSelection } from 'prosemirror-state'; import { Fragment, Node, Slice } from 'prosemirror-model'; -import * as ReactDOM from 'react-dom'; +import * as ReactDOM from 'react-dom/client'; import React = require('react'); // an elidable textblock that collapses when its '<-' is clicked and expands when its '...' anchor is clicked. @@ -9,6 +9,7 @@ import React = require('react'); // method instead of changing prosemirror's text when the expand/elide buttons are clicked. export class SummaryView { dom: HTMLSpanElement; // container for label and value + root: any; constructor(node: any, view: any, getPos: any) { const self = this; @@ -35,13 +36,14 @@ export class SummaryView { return js.apply(this, arguments); }; - ReactDOM.render(<SummaryViewInternal />, this.dom); - (this as any).dom = this.dom; + this.root = ReactDOM.createRoot(this.dom); + this.root.render(<SummaryViewInternal />); } className = (visible: boolean) => 'formattedTextBox-summarizer' + (visible ? '' : '-collapsed'); destroy() { - ReactDOM.unmountComponentAtNode(this.dom); + this.root.unmount(); + // ReactDOM.unmountComponentAtNode(this.dom); } selectNode() {} diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts index 00c41e187..8d43c33f0 100644 --- a/src/client/views/nodes/formattedText/marks_rts.ts +++ b/src/client/views/nodes/formattedText/marks_rts.ts @@ -75,6 +75,7 @@ export const marks: { [index: string]: MarkSpec } = { allAnchors: { default: [] as { href: string; title: string; anchorId: string }[] }, location: { default: null }, title: { default: null }, + noPreview: { default: false }, docref: { default: false }, // flags whether the linked text comes from a document within Dash. If so, an attribution label is appended after the text }, inclusive: false, @@ -85,6 +86,7 @@ export const marks: { [index: string]: MarkSpec } = { return { location: dom.getAttribute('location'), title: dom.getAttribute('title'), + noPreview: dom.getAttribute('noPreview'), }; }, }, @@ -95,12 +97,9 @@ export const marks: { [index: string]: MarkSpec } = { return node.attrs.docref && node.attrs.title ? [ 'div', - ['span', `"`], ['span', 0], - ['span', `"`], - ['br'], [ - 'a', + 'span', { ...node.attrs, class: 'prosemirror-attribution', @@ -108,19 +107,8 @@ export const marks: { [index: string]: MarkSpec } = { }, node.attrs.title, ], - ['br'], ] - : //node.attrs.allLinks.length === 1 ? - ['a', { class: anchorids, 'data-targethrefs': targethrefs, title: node.attrs.title, location: node.attrs.location, style: `text-decoration: underline` }, 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] - // )] - // ]; + : ['a', { class: anchorids, 'data-targethrefs': targethrefs, title: node.attrs.title, 'data-noPreview': node.attrs.noPreview, location: node.attrs.location, style: `text-decoration: underline` }, 0]; }, }, diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts index 66d747bf7..aa2475dca 100644 --- a/src/client/views/nodes/formattedText/nodes_rts.ts +++ b/src/client/views/nodes/formattedText/nodes_rts.ts @@ -263,6 +263,7 @@ export const nodes: { [index: string]: NodeSpec } = { fieldKey: { default: '' }, docid: { default: '' }, hideKey: { default: false }, + editable: { default: true }, }, group: 'inline', draggable: false, diff --git a/src/client/views/nodes/trails/PresBox.scss b/src/client/views/nodes/trails/PresBox.scss index 4d60a02f1..fd202590e 100644 --- a/src/client/views/nodes/trails/PresBox.scss +++ b/src/client/views/nodes/trails/PresBox.scss @@ -938,14 +938,19 @@ min-width: 15px; max-width: 100px; left: 8px; + margin: auto; + margin-left: unset; + height: 100%; } .presBox-presentPanel { display: flex; justify-self: end; width: 100%; - max-width: 300px; - min-width: 150px; + margin: auto; + margin-right: unset; + height: 100%; + position: relative; } select { @@ -955,7 +960,7 @@ .presBox-button { cursor: pointer; - height: 25px; + //height: 100%; border-radius: 5px; display: none; justify-content: center; @@ -986,6 +991,9 @@ width: max-content; position: absolute; right: 10px; + margin: auto; + margin-right: unset; + height: 100%; .present-icon { margin-right: 7px; @@ -999,9 +1007,16 @@ position: relative; display: inline-block; left: 8px; + margin: auto; + margin-left: unset; } } +.presBox-buttons.inOverlay { + padding-top: unset; + padding-bottom: unset; +} + .presBox-backward, .presBox-forward { width: 25px; @@ -1052,6 +1067,8 @@ font-size: 30px; position: absolute; min-width: 50px; + margin: auto; + margin-left: unset; } } @@ -1087,13 +1104,12 @@ color: $white; border-radius: 5px; grid-template-rows: 100%; - height: 25; + height: 100%; width: max-content; min-width: max-content; justify-content: space-evenly; align-items: center; display: flex; - position: absolute; transition: all 0.2s; .presPanel-button-text { diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index 4e8aed8a6..855a7f171 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -1,18 +1,18 @@ import React = require('react'); import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; -import { action, computed, IReactionDisposer, observable, ObservableSet, reaction, runInAction } from 'mobx'; +import { action, computed, IReactionDisposer, observable, ObservableSet, observe, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { ColorState, SketchPicker } from 'react-color'; -import { Bounce, Fade, Flip, LightSpeed, Roll, Rotate, Zoom } from 'react-reveal'; -import { Doc, DocListCast, FieldResult, StrListCast } from '../../../../fields/Doc'; +import { AnimationSym, Doc, DocListCast, FieldResult, HighlightSym, Opt, StrListCast } from '../../../../fields/Doc'; import { Copy, Id } from '../../../../fields/FieldSymbols'; import { InkTool } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; import { ObjectField } from '../../../../fields/ObjectField'; import { listSpec } from '../../../../fields/Schema'; import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types'; -import { emptyFunction, returnFalse, returnOne, returnTrue, setupMoveUpEvents, StopEvent } from '../../../../Utils'; +import { AudioField } from '../../../../fields/URLField'; +import { emptyFunction, emptyPath, returnFalse, returnOne, setupMoveUpEvents, StopEvent } from '../../../../Utils'; import { DocServer } from '../../../DocServer'; import { Docs } from '../../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; @@ -22,25 +22,40 @@ import { SelectionManager } from '../../../util/SelectionManager'; import { SettingsManager } from '../../../util/SettingsManager'; import { undoBatch, UndoManager } from '../../../util/UndoManager'; import { CollectionDockingView } from '../../collections/CollectionDockingView'; -import { CollectionFreeFormView, MarqueeViewBounds } from '../../collections/collectionFreeForm'; +import { CollectionFreeFormView, computeTimelineLayout, MarqueeViewBounds } from '../../collections/collectionFreeForm'; import { CollectionView } from '../../collections/CollectionView'; import { TabDocView } from '../../collections/TabDocView'; import { ViewBoxBaseComponent } from '../../DocComponent'; import { Colors } from '../../global/globalEnums'; import { LightboxView } from '../../LightboxView'; import { CollectionFreeFormDocumentView } from '../CollectionFreeFormDocumentView'; +import { DocFocusOptions, DocumentView, OpenWhere, OpenWhereMod } from '../DocumentView'; import { FieldView, FieldViewProps } from '../FieldView'; import { ScriptingBox } from '../ScriptingBox'; import './PresBox.scss'; -import { PresEffect, PresMovement, PresStatus } from './PresEnums'; +import { PresEffect, PresEffectDirection, PresMovement, PresStatus } from './PresEnums'; +import { CollectionStackedTimeline } from '../../collections/CollectionStackedTimeline'; +const { Howl } = require('howler'); export interface PinProps { audioRange?: boolean; activeFrame?: number; + currentFrame?: number; hidePresBox?: boolean; pinViewport?: MarqueeViewBounds; // pin a specific viewport on a freeform view (use MarqueeView.CurViewBounds to compute if no region has been selected) pinDocLayout?: boolean; // pin layout info (width/height/x/y) pinDocContent?: boolean; // pin data info (scroll/pan/zoom/text) + pinAudioPlay?: boolean; // pin audio annotation + pinData?: { + scrollable?: boolean | undefined; + pannable?: boolean | undefined; + temporal?: boolean | undefined; + clippable?: boolean | undefined; + dataview?: boolean | undefined; + textview?: boolean | undefined; + poslayoutview?: boolean | undefined; + dataannos?: boolean | undefined; + }; } @observer @@ -49,39 +64,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { return FieldView.LayoutString(PresBox, fieldKey); } - /** - * returns an entrance animation effect function to wrap a JSX element - * @param presEffectDoc presentation effects document that specifies the animation effect parameters - * @returns a function that will wrap a JSX animation element wrapping any JSX element - */ - public static AnimationEffect(renderDoc: JSX.Element, presEffectDoc: Doc) { - const effectProps = { - left: presEffectDoc.presEffectDirection === PresEffect.Left, - right: presEffectDoc.presEffectDirection === PresEffect.Right, - top: presEffectDoc.presEffectDirection === PresEffect.Top, - bottom: presEffectDoc.presEffectDirection === PresEffect.Bottom, - opposite: true, - delay: NumCast(presEffectDoc.presTransition), - }; - //prettier-ignore - switch (StrCast(presEffectDoc.presEffect)) { - default: - case PresEffect.None: return renderDoc; - case PresEffect.Zoom: return <Zoom {...effectProps}>{renderDoc}</Zoom>; - case PresEffect.Fade: return <Fade {...effectProps}>{renderDoc}</Fade>; - case PresEffect.Flip: return <Flip {...effectProps}>{renderDoc}</Flip>; - case PresEffect.Rotate: return <Rotate {...effectProps}>{renderDoc}</Rotate>; - case PresEffect.Bounce: return <Bounce {...effectProps}>{renderDoc}</Bounce>; - case PresEffect.Roll: return <Roll {...effectProps}>{renderDoc}</Roll>; - case PresEffect.Lightspeed: return <LightSpeed {...effectProps}>{renderDoc}</LightSpeed>; - } - } - private _disposers: { [name: string]: IReactionDisposer } = {}; public selectedArray = new ObservableSet<Doc>(); @observable public static Instance: PresBox; - @observable static startMarquee: boolean = false; // onclick "+ new slide" in presentation mode, set as true, then when marquee selection finish, onPointerUp automatically triggers PinWithView @observable _isChildActive = false; @observable _moveOnFromAudio: boolean = true; @@ -93,7 +79,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @observable _expandBoolean: boolean = false; @observable _transitionTools: boolean = false; @observable _newDocumentTools: boolean = false; - @observable _progressivizeTools: boolean = false; @observable _openMovementDropdown: boolean = false; @observable _openEffectDropdown: boolean = false; @observable _presentTools: boolean = false; @@ -125,7 +110,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { return Cast(this.activeItem?.presentationTargetDoc, Doc, null); } @computed get scrollable() { - if (this.targetDoc.type === DocumentType.PDF || this.targetDoc.type === DocumentType.WEB || this.targetDoc.type === DocumentType.RTF || this.targetDoc._viewType === CollectionViewType.Stacking) return true; + if ([DocumentType.PDF, DocumentType.WEB, DocumentType.RTF].includes(this.targetDoc.type as DocumentType) || this.targetDoc._viewType === CollectionViewType.Stacking) return true; return false; } @computed get panable() { @@ -151,6 +136,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @action componentWillUnmount() { this._unmounting = true; + if (this._presTimer) clearTimeout(this._presTimer); document.removeEventListener('keydown', PresBox.keyEventsWrapper, true); this.resetPresentation(); // Turn of progressivize editors @@ -158,7 +144,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { Object.values(this._disposers).forEach(disposer => disposer?.()); } - @action componentDidMount() { this._disposers.keyboard = reaction( () => this.selectedDoc, @@ -182,10 +167,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { ); this.props.setContentView?.(this); this._unmounting = false; - this.rootDoc._forceRenderEngine = 'timeline'; - this.layoutDoc.presStatus = PresStatus.Edit; - this.layoutDoc._gridGap = 0; - this.layoutDoc._yMargin = 0; + if (this.props.renderDepth > 0) { + runInAction(() => { + this.rootDoc._forceRenderEngine = computeTimelineLayout.name; + this.layoutDoc._gridGap = 0; + this.layoutDoc._yMargin = 0; + }); + } this.turnOffEdit(true); this._disposers.selection = reaction( () => SelectionManager.Views(), @@ -199,17 +187,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { PresBox.Instance = this; }; - // There are still other internal frames and should go through all frames before going to next slide - nextInternalFrame = (targetDoc: Doc, activeItem: Doc) => { - const currentFrame = Cast(targetDoc?._currentFrame, 'number', null); - const childDocs = DocListCast(targetDoc[Doc.LayoutFieldKey(targetDoc)]); - targetDoc._viewTransition = 'all 1s'; - setTimeout(() => (targetDoc._viewTransition = undefined), 1010); - this.nextKeyframe(targetDoc, activeItem); - if (activeItem.presProgressivize) CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0, targetDoc); - else targetDoc.keyFrameEditing = true; - }; - _mediaTimer!: [NodeJS.Timeout, Doc]; // 'Play on next' for audio or video therefore first navigate to the audio/video before it should be played startTempMedia = (targetDoc: Doc, activeItem: Doc) => { @@ -232,38 +209,53 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { // TODO: to handle child slides (entering into subtrail and exiting), also the next() and back() functions // No more frames in current doc and next slide is defined, therefore move to next slide nextSlide = (slideNum?: number) => { - let nextSelected = slideNum ?? this.itemIndex + 1; - this.gotoDocument(nextSelected, this.activeItem); - for (nextSelected = nextSelected + 1; nextSelected < this.childDocs.length; nextSelected++) { - if (this.childDocs[nextSelected].groupWithUp) { - this.gotoDocument(nextSelected, this.activeItem, true); - } else { - break; + const nextSlideInd = slideNum ?? this.itemIndex + 1; + let curSlideInd = nextSlideInd; + const resetSelection = action(() => { + this.clearSelectedArray(); + for (let i = nextSlideInd; i <= curSlideInd; i++) { + this.addToSelectedArray(this.childDocs[i]); } - } + }); + CollectionStackedTimeline.CurrentlyPlaying?.map((clip, i) => DocumentManager.Instance.getDocumentView(clip)?.ComponentView?.Pause?.()); + this.clearSelectedArray(); + const doGroupWithUp = + (nextSelected: number, force = false) => + () => { + if (nextSelected < this.childDocs.length) { + if (force || this.childDocs[nextSelected].groupWithUp) { + const serial = nextSelected + 1 < this.childDocs.length && NumCast(this.childDocs[nextSelected + 1].groupWithUp) > 1; + if (serial) { + this.gotoDocument(nextSelected, this.activeItem, true, async () => { + const waitTime = NumCast(this.activeItem.presDuration) - NumCast(this.activeItem.presTransition); + await new Promise<void>(res => setTimeout(() => res(), Math.max(0, waitTime))); + doGroupWithUp(nextSelected + 1)(); + }); + } else { + this.gotoDocument(nextSelected, this.activeItem, undefined, resetSelection); + curSlideInd = this.itemIndex; + doGroupWithUp(nextSelected + 1)(); + } + } + } + }; + doGroupWithUp(curSlideInd, true)(); }; // Called when the user activates 'next' - to move to the next part of the pres. trail @action next = () => { - const activeNext = Cast(this.childDocs[this.itemIndex + 1], Doc, null); - const activeItem: Doc = this.activeItem; - const targetDoc: Doc = this.targetDoc; - const lastFrame = Cast(targetDoc?.lastFrame, 'number', null); - const curFrame = NumCast(targetDoc?._currentFrame); - let internalFrames: boolean = false; - if (activeItem.presProgressivize || activeItem.zoomProgressivize || targetDoc.scrollProgressivize) internalFrames = true; - if (internalFrames && lastFrame !== undefined && curFrame < lastFrame) { - // Case 1: There are still other frames and should go through all frames before going to next slide - this.nextInternalFrame(targetDoc, activeItem); - } else if (this.childDocs[this.itemIndex + 1] !== undefined) { - // Case 2: No more frames in current doc and next slide is defined, therefore move to next slide + if (this.childDocs[this.itemIndex + 1] !== undefined) { + // Case 1: No more frames in current doc and next slide is defined, therefore move to next slide const slides = DocListCast(this.rootDoc[StrCast(this.presFieldKey, 'data')]); const curLast = this.selectedArray.size ? Math.max(...Array.from(this.selectedArray).map(d => slides.indexOf(DocCast(d)))) : this.itemIndex; - this.nextSlide(curLast + 1); - } else if (this.childDocs[this.itemIndex + 1] === undefined && (this.layoutDoc.presLoop || this.layoutDoc.presStatus === PresStatus.Edit)) { - // Case 3: Last slide and presLoop is toggled ON or it is in Edit mode - this.nextSlide(0); + this.nextSlide(curLast + 1 === this.childDocs.length ? (this.layoutDoc.presLoop ? 0 : curLast) : curLast + 1); + } else { + if (this.childDocs[this.itemIndex + 1] === undefined && (this.layoutDoc.presLoop || this.layoutDoc.presStatus === PresStatus.Edit)) { + // Case 2: Last slide and presLoop is toggled ON or it is in Edit mode + this.nextSlide(0); + } + return 0; } return this.itemIndex; }; @@ -272,29 +264,21 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @action back = () => { 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); let prevSelected = this.itemIndex; // Functionality for group with up let didZoom = activeItem.presMovement; for (; prevSelected > 0 && this.childDocs[Math.max(0, prevSelected - 1)].groupWithUp; prevSelected--) { didZoom = didZoom === 'none' ? this.childDocs[prevSelected].presMovement : didZoom; } - if (lastFrame !== undefined && curFrame >= 1) { - // Case 1: There are still other frames and should go through all frames before going to previous slide - this.prevKeyframe(targetDoc, activeItem); - } else if (activeItem && this.childDocs[this.itemIndex - 1] !== undefined) { + if (activeItem && this.childDocs[this.itemIndex - 1] !== undefined) { // Case 2: There are no other frames so it should go to the previous slide prevSelected = Math.max(0, prevSelected - 1); this.nextSlide(prevSelected); this.rootDoc._itemIndex = prevSelected; - if (NumCast(prevTargetDoc.lastFrame) > 0) prevTargetDoc._currentFrame = NumCast(prevTargetDoc.lastFrame); } else if (this.childDocs[this.itemIndex - 1] === undefined && this.layoutDoc.presLoop) { // Case 3: Pres loop is on so it should go to the last slide - this.gotoDocument(this.childDocs.length - 1, activeItem); + this.nextSlide(this.childDocs.length - 1); } return this.itemIndex; }; @@ -302,21 +286,22 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { //The function that is called when a document is clicked or reached through next or back. //it'll also execute the necessary actions if presentation is playing. @undoBatch - public gotoDocument = action((index: number, from?: Doc, group?: boolean) => { + public gotoDocument = action((index: number, from?: Doc, group?: boolean, finished?: () => void) => { Doc.UnBrushAllDocs(); - if (index >= 0 && index < this.childDocs.length) { this.rootDoc._itemIndex = index; const activeItem: Doc = this.activeItem; const targetDoc: Doc = this.targetDoc; - if (activeItem.presActiveFrame !== undefined) { + const activeFrame = activeItem.presActiveFrame ?? activeItem.presCurrentFrame; + if (activeFrame !== undefined) { const transTime = NumCast(activeItem.presTransition, 500); - const context = DocCast(DocCast(activeItem.presentationTargetDoc).context); + const acontext = activeItem.presActiveFrame !== undefined ? DocCast(DocCast(activeItem.presentationTargetDoc).context) : DocCast(activeItem.presentationTargetDoc); + const context = DocCast(acontext)?.annotationOn ? DocCast(DocCast(acontext).annotationOn) : acontext; if (context) { - const contextView = DocumentManager.Instance.getFirstDocumentView(context); - if (contextView?.ComponentView) { - CollectionFreeFormDocumentView.gotoKeyframe((contextView.ComponentView as CollectionFreeFormView).childDocs.slice(), transTime); - context._currentFrame = NumCast(activeItem.presActiveFrame); + const ffview = DocumentManager.Instance.getFirstDocumentView(context)?.ComponentView as CollectionFreeFormView; + if (ffview?.childDocs) { + this._keyTimer = CollectionFreeFormDocumentView.gotoKeyframe(this._keyTimer, ffview.childDocs.slice(), transTime); + context._currentFrame = NumCast(activeFrame); } } } @@ -330,56 +315,91 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (this.layoutDoc.presStatus !== PresStatus.Edit && (targetDoc.type === DocumentType.AUDIO || targetDoc.type === DocumentType.VID) && activeItem.mediaStart === 'auto') { this.startTempMedia(targetDoc, activeItem); } - if (targetDoc) { - Doc.linkFollowHighlight(targetDoc.annotationOn instanceof Doc ? [targetDoc, targetDoc.annotationOn] : targetDoc); - targetDoc && runInAction(() => (targetDoc.focusSpeed = activeItem.presMovement === PresMovement.Jump ? 0 : NumCast(activeItem.presTransition, 500))); - setTimeout(() => (targetDoc.focusSpeed = undefined), NumCast(targetDoc.focusSpeed) + 10); - } - if (targetDoc?.lastFrame !== undefined) { - targetDoc._currentFrame = 0; - } if (!group) this.clearSelectedArray(); this.childDocs[index] && this.addToSelectedArray(this.childDocs[index]); //Update selected array this.turnOffEdit(); - this.navigateToActiveItem(); //Handles movement to element only when presTrail is list + this.navigateToActiveItem(finished); //Handles movement to element only when presTrail is list this.onHideDocument(); //Handles hide after/before } }); - static pinDataTypes(target: Doc) { - const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(target.type as any) || target._viewType === CollectionViewType.Stacking; - const pannable = [DocumentType.IMG].includes(target.type as any) || (target.type === DocumentType.COL && target._viewType === CollectionViewType.Freeform); - const temporal = [DocumentType.AUDIO, DocumentType.VID].includes(target.type as any); - const clippable = [DocumentType.COMPARISON].includes(target.type as any); - const dataview = [DocumentType.INK, DocumentType.COL].includes(target.type as any) && target.activeFrame === undefined; - const poslayoutview = [DocumentType.COL].includes(target.type as any) && target.activeFrame === undefined; - const textview = [DocumentType.RTF].includes(target.type as any) && target.activeFrame === undefined; - return { scrollable, pannable, temporal, clippable, dataview, textview, poslayoutview }; + static pinDataTypes(target?: Doc): { scrollable?: boolean; pannable?: boolean; temporal?: boolean; clippable?: boolean; dataview?: boolean; textview?: boolean; poslayoutview?: boolean; dataannos?: boolean } { + const targetType = target?.type as any; + const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(targetType) || target?._viewType === CollectionViewType.Stacking; + const pannable = [DocumentType.IMG, DocumentType.PDF].includes(targetType) || (targetType === DocumentType.COL && target?._viewType === CollectionViewType.Freeform); + const temporal = [DocumentType.AUDIO, DocumentType.VID].includes(targetType); + const clippable = [DocumentType.COMPARISON].includes(targetType); + const dataview = [DocumentType.INK, DocumentType.COL, DocumentType.IMG].includes(targetType) && target?.activeFrame === undefined; + const poslayoutview = [DocumentType.COL].includes(targetType) && target?.activeFrame === undefined; + const textview = [DocumentType.RTF].includes(targetType) && target?.activeFrame === undefined; + const dataannos = false; + return { scrollable, pannable, temporal, clippable, dataview, textview, poslayoutview, dataannos }; } @action - static restoreTargetDocView(bestTarget: Doc, activeItem: Doc) { - const transTime = NumCast(activeItem.presTransition, 500); - const presTransitionTime = `all ${transTime}ms`; - const { scrollable, pannable, temporal, clippable, dataview, textview, poslayoutview } = this.pinDataTypes(bestTarget); - bestTarget._viewTransition = presTransitionTime; - if (clippable) bestTarget._clipWidth = activeItem.presPinClipWidth; - if (temporal) bestTarget._currentTimecode = activeItem.presStartTime; - if (scrollable) { - bestTarget._scrollTop = activeItem.presPinViewScroll; - const contentBounds = Cast(activeItem.presPinViewBounds, listSpec('number')); - if (contentBounds) { - const dv = DocumentManager.Instance.getDocumentView(bestTarget)?.ComponentView; - dv?.brushView?.({ panX: (contentBounds[0] + contentBounds[2]) / 2, panY: (contentBounds[1] + contentBounds[3]) / 2, width: contentBounds[2] - contentBounds[0], height: contentBounds[3] - contentBounds[1] }); + playAnnotation = (anno: AudioField) => {}; + @action + static restoreTargetDocView(bestTargetView: Opt<DocumentView>, pinProps: PinProps | undefined, activeItem: Doc, transTime: number, pinDataTypes = this.pinDataTypes(bestTargetView?.rootDoc)) { + if (!bestTargetView) return; + const bestTarget = bestTargetView.rootDoc; + let changed = false; + if (pinProps?.pinDocLayout) { + if ( + bestTarget.x !== NumCast(activeItem.presX, NumCast(bestTarget.x)) || + bestTarget.y !== NumCast(activeItem.presY, NumCast(bestTarget.y)) || + bestTarget.rotation !== NumCast(activeItem.presRot, NumCast(bestTarget.rotation)) || + bestTarget.width !== NumCast(activeItem.presWidth, NumCast(bestTarget.width)) || + bestTarget.height !== NumCast(activeItem.presHeight, NumCast(bestTarget.height)) + ) { + bestTarget._dataTransition = `all ${transTime}ms`; + bestTarget.x = NumCast(activeItem.presX, NumCast(bestTarget.x)); + bestTarget.y = NumCast(activeItem.presY, NumCast(bestTarget.y)); + bestTarget.rotation = NumCast(activeItem.presRot, NumCast(bestTarget.rotation)); + bestTarget.width = NumCast(activeItem.presWidth, NumCast(bestTarget.width)); + bestTarget.height = NumCast(activeItem.presHeight, NumCast(bestTarget.height)); + setTimeout(() => (bestTarget._dataTransition = undefined), transTime + 10); + changed = true; + } + } + if (pinDataTypes.clippable) { + if (bestTarget._clipWidth !== activeItem.presPinClipWidth) { + bestTarget._clipWidth = activeItem.presPinClipWidth; + changed = true; + } + } + if (pinDataTypes.temporal) { + if (bestTarget._currentTimecode !== activeItem.presStartTime) { + bestTarget._currentTimecode = activeItem.presStartTime; + changed = true; } } - if (dataview) Doc.GetProto(bestTarget)[Doc.LayoutFieldKey(bestTarget)] = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData; - if (textview) Doc.GetProto(bestTarget)[Doc.LayoutFieldKey(bestTarget)] = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData; - if (poslayoutview) { + if (pinDataTypes.scrollable) { + if (bestTarget._scrollTop !== activeItem.presPinViewScroll) { + bestTarget._scrollTop = activeItem.presPinViewScroll; + changed = true; + const contentBounds = Cast(activeItem.presPinViewBounds, listSpec('number')); + if (contentBounds) { + const dv = DocumentManager.Instance.getDocumentView(bestTarget)?.ComponentView; + dv?.brushView?.({ panX: (contentBounds[0] + contentBounds[2]) / 2, panY: (contentBounds[1] + contentBounds[3]) / 2, width: contentBounds[2] - contentBounds[0], height: contentBounds[3] - contentBounds[1] }); + } + } + } + if (pinDataTypes.dataannos) { + const fkey = Doc.LayoutFieldKey(bestTarget); + Doc.GetProto(bestTarget)[fkey + '-annotations'] = new List<Doc>([...DocListCast(bestTarget[fkey + '-annotations']).filter(doc => doc.unrendered), ...DocListCast(activeItem.presAnnotations)]); + } + if (pinDataTypes.dataview && activeItem.presData !== undefined) { + const fkey = Doc.LayoutFieldKey(bestTarget); + Doc.GetProto(bestTarget)[fkey] = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData; + bestTarget[fkey + '-useAlt'] = activeItem.presUseAlt; + } + if (pinDataTypes.textview && activeItem.presData !== undefined) Doc.GetProto(bestTarget)[Doc.LayoutFieldKey(bestTarget)] = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData; + if (pinDataTypes.poslayoutview) { + changed = true; StrListCast(activeItem.presPinLayoutData) .map(str => JSON.parse(str) as { id: string; x: number; y: number; w: number; h: number }) .forEach(data => { const doc = DocServer.GetCachedRefField(data.id) as Doc; - doc._dataTransition = presTransitionTime; + doc._dataTransition = `all ${transTime}ms`; doc.x = data.x; doc.y = data.y; doc._width = data.w; @@ -389,16 +409,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { () => StrListCast(activeItem.presPinLayoutData) .map(str => JSON.parse(str) as { id: string; x: number; y: number; w: number; h: number }) - .forEach( - action(data => { - const doc = DocServer.GetCachedRefField(data.id) as Doc; - doc._dataTransition = undefined; - }) - ), + .forEach(action(data => ((DocServer.GetCachedRefField(data.id) as Doc)._dataTransition = undefined))), transTime + 10 ); } - if (pannable) { + if (pinDataTypes.pannable) { const contentBounds = Cast(activeItem.presPinViewBounds, listSpec('number')); if (contentBounds) { const viewport = { panX: (contentBounds[0] + contentBounds[2]) / 2, panY: (contentBounds[1] + contentBounds[3]) / 2, width: contentBounds[2] - contentBounds[0], height: contentBounds[3] - contentBounds[1] }; @@ -407,25 +422,29 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const dv = DocumentManager.Instance.getDocumentView(bestTarget); if (dv) { const computedScale = NumCast(activeItem.presZoom, 1) * Math.min(dv.props.PanelWidth() / viewport.width, dv.props.PanelHeight() / viewport.height); - activeItem.presMovement === 'zoom' && (bestTarget._viewScale = computedScale); + activeItem.presMovement === PresMovement.Zoom && (bestTarget._viewScale = computedScale); dv.ComponentView?.brushView?.(viewport); } } else { - bestTarget._panX = activeItem.presPinViewX; - bestTarget._panY = activeItem.presPinViewY; - activeItem.presMovement === 'zoom' && (bestTarget._viewScale = activeItem.presPinViewScale); + if (bestTarget._panX !== activeItem.presPinViewX || bestTarget._panY !== activeItem.presPinViewY || bestTarget._viewScale !== activeItem.presPinViewScale) { + bestTarget._panX = activeItem.presPinViewX; + bestTarget._panY = activeItem.presPinViewY; + bestTarget._viewScale = activeItem.presPinViewScale; + changed = true; + } } } - return setTimeout(() => (bestTarget._viewTransition = undefined), transTime + 10); + if (changed) { + return bestTargetView.setViewTransition('all', transTime); + } } /// copies values from the targetDoc (which is the prototype of the pinDoc) to /// reserved fields on the pinDoc so that those values can be restored to the /// target doc when navigating to it. @action - static pinDocView(pinDoc: Doc, pinProps: PinProps | undefined, targetDoc: Doc) { - const { scrollable, pannable, temporal, clippable, dataview, textview, poslayoutview } = this.pinDataTypes(pinDoc); - if (pinProps?.pinDocLayout) { + static pinDocView(pinDoc: Doc, pinProps: PinProps, targetDoc: Doc) { + if (pinProps.pinDocLayout) { pinDoc.presPinLayout = true; pinDoc.presX = NumCast(targetDoc.x); pinDoc.presY = NumCast(targetDoc.y); @@ -433,36 +452,51 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { pinDoc.presWidth = NumCast(targetDoc.width); pinDoc.presHeight = NumCast(targetDoc.height); } - if (pinProps?.pinDocContent) { - pinDoc.presPinData = scrollable || temporal || pannable || clippable || dataview || textview || poslayoutview || pinProps.activeFrame !== undefined; - if (dataview) pinDoc.presData = targetDoc[Doc.LayoutFieldKey(targetDoc)] instanceof ObjectField ? (targetDoc[Doc.LayoutFieldKey(targetDoc)] as ObjectField)[Copy]() : targetDoc.data; - if (textview) pinDoc.presData = targetDoc[Doc.LayoutFieldKey(targetDoc)] instanceof ObjectField ? (targetDoc[Doc.LayoutFieldKey(targetDoc)] as ObjectField)[Copy]() : targetDoc.text; - if (scrollable) pinDoc.presPinViewScroll = pinDoc._scrollTop; - if (clippable) pinDoc.presPinClipWidth = pinDoc._clipWidth; - if (poslayoutview) pinDoc.presPinLayoutData = new List<string>(DocListCast(pinDoc.presData).map(d => JSON.stringify({ id: d[Id], x: NumCast(d.x), y: NumCast(d.y), w: NumCast(d._width), h: NumCast(d._height) }))); - if (pannable) { - pinDoc.presPinViewX = NumCast(pinDoc._panX); - pinDoc.presPinViewY = NumCast(pinDoc._panY); - pinDoc.presPinViewScale = NumCast(pinDoc._viewScale, 1); + if (pinProps.pinAudioPlay) pinDoc.followLinkAudio = true; + if (pinProps.pinData) { + pinDoc.presPinData = + pinProps.pinData.scrollable || + pinProps.pinData.temporal || + pinProps.pinData.pannable || + pinProps.pinData.clippable || + pinProps.pinData.dataview || + pinProps.pinData.textview || + pinProps.pinData.poslayoutview || + pinProps?.activeFrame !== undefined; + if (pinProps.pinData.dataview) { + const fkey = Doc.LayoutFieldKey(targetDoc); + pinDoc.presUseAlt = targetDoc[fkey + '-useAlt']; + pinDoc.presData = targetDoc[fkey] instanceof ObjectField ? (targetDoc[fkey] as ObjectField)[Copy]() : targetDoc.data; + } + if (pinProps.pinData.dataannos) { + const fkey = Doc.LayoutFieldKey(targetDoc); + pinDoc.presAnnotations = new List<Doc>(DocListCast(Doc.GetProto(targetDoc)[fkey + '-annotations']).filter(doc => !doc.unrendered)); } - if (temporal) { - pinDoc.presStartTime = pinDoc._currentTimecode; - const duration = NumCast(pinDoc[`${Doc.LayoutFieldKey(pinDoc)}-duration`], NumCast(pinDoc.presStartTime) + 0.1); - pinDoc.presEndTime = NumCast(pinDoc.clipEnd, duration); + if (pinProps.pinData.textview) pinDoc.presData = targetDoc[Doc.LayoutFieldKey(targetDoc)] instanceof ObjectField ? (targetDoc[Doc.LayoutFieldKey(targetDoc)] as ObjectField)[Copy]() : targetDoc.text; + if (pinProps.pinData.scrollable) pinDoc.presPinViewScroll = targetDoc._scrollTop; + if (pinProps.pinData.clippable) pinDoc.presPinClipWidth = targetDoc._clipWidth; + if (pinProps.pinData.poslayoutview) pinDoc.presPinLayoutData = new List<string>(DocListCast(targetDoc.presData).map(d => JSON.stringify({ id: d[Id], x: NumCast(d.x), y: NumCast(d.y), w: NumCast(d._width), h: NumCast(d._height) }))); + if (pinProps.pinData.pannable) { + pinDoc.presPinViewX = NumCast(targetDoc._panX); + pinDoc.presPinViewY = NumCast(targetDoc._panY); + pinDoc.presPinViewScale = NumCast(targetDoc._viewScale, 1); + } + if (pinProps.pinData.temporal) { + pinDoc.presStartTime = targetDoc._currentTimecode; + const duration = NumCast(pinDoc[`${Doc.LayoutFieldKey(pinDoc)}-duration`], NumCast(targetDoc.presStartTime) + 0.1); + pinDoc.presEndTime = NumCast(targetDoc.clipEnd, duration); } } if (pinProps?.pinViewport) { // If pinWithView option set then update scale and x / y props of slide const bounds = pinProps.pinViewport; pinDoc.presPinView = true; - pinDoc.presPinViewScale = NumCast(pinDoc._viewScale, 1); + pinDoc.presPinViewScale = NumCast(targetDoc._viewScale, 1); pinDoc.presPinViewX = bounds.left + bounds.width / 2; pinDoc.presPinViewY = bounds.top + bounds.height / 2; pinDoc.presPinViewBounds = new List<number>([bounds.left, bounds.top, bounds.left + bounds.width, bounds.top + bounds.height]); } } - - static _navTimer: NodeJS.Timeout; /** * This method makes sure that cursor navigates to the element that * has the option open and last in the group. @@ -471,127 +505,106 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { * a new tab. If presCollection is undefined it will open the document * on the right. */ - navigateToActiveItem = () => { + navigateToActiveItem = (afterNav?: () => void) => { const activeItem: Doc = this.activeItem; const targetDoc: Doc = this.targetDoc; + const finished = () => { + afterNav?.(); + console.log('Finish Slide Nav: ' + targetDoc.title); + targetDoc[AnimationSym] = undefined; + }; const srcContext = Cast(targetDoc.context, Doc, null) ?? Cast(Cast(targetDoc.annotationOn, Doc, null)?.context, Doc, null); const presCollection = Cast(this.layoutDoc.presCollection, Doc, null); const collectionDocView = presCollection ? DocumentManager.Instance.getDocumentView(presCollection) : undefined; - const includesDoc: boolean = DocListCast(presCollection?.data).includes(targetDoc); - const tab = CollectionDockingView.Instance && Array.from(CollectionDockingView.Instance.tabMap).find(tab => tab.DashDoc === srcContext); + const includesDoc = () => (DocumentManager.Instance.getDocumentView(targetDoc) ? true : false); // DocListCast(presCollection?.data).includes(targetDoc); + const tabMap = CollectionDockingView.Instance?.tabMap; + const tab = tabMap && Array.from(tabMap).find(tab => tab.DashDoc === srcContext || tab.DashDoc === targetDoc); // Handles the setting of presCollection - if (includesDoc) { + if (includesDoc()) { //Case 1: Pres collection should not change as it is already the same } else if (tab !== undefined) { // Case 2: Pres collection should update this.layoutDoc.presCollection = srcContext; } - const presStatus = this.rootDoc.presStatus; const selViewCache = Array.from(this.selectedArray); const dragViewCache = Array.from(this._dragArray); const eleViewCache = Array.from(this._eleArray); - const self = this; const resetSelection = action(() => { - const presDocView = DocumentManager.Instance.getDocumentView(self.rootDoc); - if (presDocView) SelectionManager.SelectView(presDocView, false); - self.rootDoc.presStatus = presStatus; - self.clearSelectedArray(); - selViewCache.forEach(doc => self.addToSelectedArray(doc)); - self._dragArray.splice(0, self._dragArray.length, ...dragViewCache); - self._eleArray.splice(0, self._eleArray.length, ...eleViewCache); + if (!includesDoc()) { + const presDocView = DocumentManager.Instance.getDocumentView(this.rootDoc); + if (presDocView) SelectionManager.SelectView(presDocView, false); + this.clearSelectedArray(); + selViewCache.forEach(doc => this.addToSelectedArray(doc)); + this._dragArray.splice(0, this._dragArray.length, ...dragViewCache); + this._eleArray.splice(0, this._eleArray.length, ...eleViewCache); + } + finished(); }); - const openInTab = (doc: Doc, finished?: () => void) => { - (collectionDocView ?? this).props.addDocTab(doc, ''); + const createDocView = (doc: Doc, finished?: () => void) => { + DocumentManager.Instance.AddViewRenderedCb(doc, () => finished?.()); + (collectionDocView ?? this).props.addDocTab(doc, OpenWhere.lightbox); this.layoutDoc.presCollection = targetDoc; - // this still needs some fixing - setTimeout(resetSelection, 500); - if (doc !== targetDoc) { - setTimeout(finished ?? emptyFunction, 100); /// give it some time to create the targetDoc if we're opening up its context - } else { - finished?.(); - } }; - PresBox.NavigateToTarget(targetDoc, activeItem, openInTab, srcContext, includesDoc || tab ? undefined : resetSelection); + PresBox.NavigateToTarget(targetDoc, activeItem, createDocView, srcContext, includesDoc() || tab ? finished : resetSelection); }; - static NavigateToTarget(targetDoc: Doc, activeItem: Doc, openInTab: any, srcContext: Doc, finished?: () => void) { - if ((activeItem.presPinLayout || activeItem.presPinView) && DocCast(targetDoc.context)?._currentFrame === undefined) { - const transTime = NumCast(activeItem.presTransition, 500); - const presTransitionTime = `all ${transTime}ms`; - targetDoc._dataTransition = presTransitionTime; - targetDoc.x = NumCast(activeItem.presX, NumCast(targetDoc.x)); - targetDoc.y = NumCast(activeItem.presY, NumCast(targetDoc.y)); - targetDoc.rotation = NumCast(activeItem.presRot, NumCast(targetDoc.rotation)); - targetDoc.width = NumCast(activeItem.presWidth, NumCast(targetDoc.width)); - targetDoc.height = NumCast(activeItem.presHeight, NumCast(targetDoc.height)); - setTimeout(() => (targetDoc._dataTransition = undefined), transTime + 10); + static NavigateToTarget(targetDoc: Doc, activeItem: Doc, createDocView: any, srcContext: Doc, finished?: () => void) { + if (activeItem.presMovement === PresMovement.None && targetDoc.type === DocumentType.SCRIPTING) { + (DocumentManager.Instance.getFirstDocumentView(targetDoc)?.ComponentView as ScriptingBox)?.onRun?.(); + return; } + const restoreLayout = () => { + // After navigating to the document, if it is added as a presPinView then it will + // adjust the pan and scale to that of the pinView when it was added. + const pinDocLayout = (BoolCast(activeItem.presPinLayout) || BoolCast(activeItem.presPinView)) && DocCast(targetDoc.context)?._currentFrame === undefined; + if (activeItem.presPinData || activeItem.presPinView || pinDocLayout) { + // targetDoc may or may not be displayed. so get the first available document (or alias) view that matches targetDoc and use it + PresBox.restoreTargetDocView(DocumentManager.Instance.getFirstDocumentView(targetDoc), { pinDocLayout }, activeItem, NumCast(activeItem.presTransition, 500)); + } + }; // If openDocument is selected then it should open the document for the user if (activeItem.openDocument) { LightboxView.SetLightboxDoc(targetDoc); // openInTab(targetDoc); - } else if (targetDoc && activeItem.presMovement !== PresMovement.None) { - LightboxView.SetLightboxDoc(undefined); - const zooming = activeItem.presMovement !== PresMovement.Pan; - DocumentManager.Instance.jumpToDocument(targetDoc, zooming, openInTab, srcContext ? [srcContext] : [], undefined, undefined, undefined, finished, undefined, true, NumCast(activeItem.presZoom, 1)); - } else if (activeItem.presMovement === PresMovement.None && targetDoc.type === DocumentType.SCRIPTING) { - (DocumentManager.Instance.getFirstDocumentView(targetDoc)?.ComponentView as ScriptingBox)?.onRun?.(); - } - // After navigating to the document, if it is added as a presPinView then it will - // adjust the pan and scale to that of the pinView when it was added. - if (activeItem.presPinData || activeItem.presPinView) { - clearTimeout(PresBox._navTimer); - // targetDoc may or may not be displayed. this gets the first available document (or alias) view that matches targetDoc - const bestTarget = DocumentManager.Instance.getFirstDocumentView(targetDoc)?.props.Document; - if (bestTarget) PresBox._navTimer = PresBox.restoreTargetDocView(bestTarget, activeItem); + setTimeout(restoreLayout); + } else { + if (targetDoc) { + const options: DocFocusOptions = { + willPan: activeItem.presMovement !== PresMovement.None, + willPanZoom: activeItem.presMovement === PresMovement.Zoom || activeItem.presMovement === PresMovement.Jump || activeItem.presMovement === PresMovement.Center, + zoomScale: activeItem.presMovement === PresMovement.Center ? 0 : NumCast(activeItem.presZoom, 1), + zoomTime: activeItem.presMovement === PresMovement.Jump ? 0 : NumCast(activeItem.presTransition, 500), + effect: activeItem, + noSelect: true, + originatingDoc: activeItem, + easeFunc: StrCast(activeItem.presEaseFunc, 'ease') as any, + zoomTextSelections: true, + }; + if (activeItem.presentationTargetDoc instanceof Doc) activeItem.presentationTargetDoc[AnimationSym] = undefined; + var containerDocContext = srcContext ? [srcContext] : []; + while (containerDocContext.length && !DocumentManager.Instance.getDocumentView(containerDocContext[0]) && containerDocContext[0].context) { + containerDocContext = [Cast(containerDocContext[0].context, Doc, null), ...containerDocContext]; + } + const testTarget = containerDocContext.length ? containerDocContext[0] : targetDoc; + if (LightboxView.LightboxDoc && !DocumentManager.Instance.getLightboxDocumentView(testTarget)) { + DocumentManager.Instance.AddViewRenderedCb(LightboxView.LightboxDoc, dv => { + if (LightboxView.LightboxDoc && !DocumentManager.Instance.getLightboxDocumentView(LightboxView.LightboxDoc)) { + DocumentManager.Instance.jumpToDocument(targetDoc, options, createDocView, containerDocContext, finished); + restoreLayout(); + } else { + LightboxView.SetLightboxDoc(undefined); + DocumentManager.Instance.jumpToDocument(targetDoc, options, createDocView, containerDocContext, finished); + restoreLayout(); + } + }); + return; + } + DocumentManager.Instance.jumpToDocument(targetDoc, options, createDocView, containerDocContext, finished); + restoreLayout(); + } else restoreLayout(); } } /** - * Uses the viewfinder to progressivize through the different views of a single collection. - * @param activeItem: document for which internal zoom is used - */ - zoomProgressivizeNext = (activeItem: Doc) => { - 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']); - const vfWidth = this.checkList(targetDoc, activeItem['viewfinder-width-indexed']); - const vfTop = this.checkList(targetDoc, activeItem['viewfinder-top-indexed']); - const vfHeight = this.checkList(targetDoc, activeItem['viewfinder-height-indexed']); - // Case 1: document that is not a Golden Layout tab - if (srcContext) { - const srcDocView = DocumentManager.Instance.getDocumentView(srcContext); - if (srcDocView) { - const layoutdoc = Doc.Layout(targetDoc); - const panelWidth: number = srcDocView.props.PanelWidth(); - const panelHeight: number = srcDocView.props.PanelHeight(); - const newPanX = NumCast(targetDoc.x) + NumCast(layoutdoc._width) / 2; - const newPanY = NumCast(targetDoc.y) + NumCast(layoutdoc._height) / 2; - const newScale = 0.9 * Math.min(Number(panelWidth) / vfWidth, Number(panelHeight) / vfHeight); - srcContext._panX = newPanX + (vfLeft + vfWidth / 2); - srcContext._panY = newPanY + (vfTop + vfHeight / 2); - srcContext._viewScale = newScale; - } - } - // Case 2: document is the containing collection - if (docView && !srcContext) { - const panelWidth: number = docView.props.PanelWidth(); - const panelHeight: number = docView.props.PanelHeight(); - const newScale = 0.9 * Math.min(Number(panelWidth) / vfWidth, Number(panelHeight) / vfHeight); - targetDoc._panX = vfLeft + vfWidth / 2; - targetDoc._panY = vfTop + vfWidth / 2; - targetDoc._viewScale = newScale; - } - const resize = document.getElementById('resizable'); - if (resize) { - resize.style.width = vfWidth + 'px'; - resize.style.height = vfHeight + 'px'; - resize.style.top = vfTop + 'px'; - resize.style.left = vfLeft + 'px'; - } - }; - - /** * For 'Hide Before' and 'Hide After' buttons making sure that * they are hidden each time the presentation is updated. */ @@ -600,54 +613,57 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.childDocs.forEach((doc, index) => { const curDoc = Cast(doc, Doc, null); const tagDoc = Cast(curDoc.presentationTargetDoc, Doc, null); - //if (tagDoc) tagDoc.opacity = 1; const itemIndexes: number[] = this.getAllIndexes(this.tagDocs, tagDoc); - const curInd: number = itemIndexes.indexOf(index); if (tagDoc === this.layoutDoc.presCollection) { tagDoc.opacity = 1; } else { - if (curDoc.presHideBefore) { - if (itemIndexes.length > 1 && curInd !== 0) { + if (curDoc.presHide) { + if (index !== this.itemIndex) { tagDoc.opacity = 1; - } else { - if (index > this.itemIndex) { - tagDoc.opacity = 0; - } else if (index === this.itemIndex || !curDoc.presHideAfter) { - tagDoc.opacity = 1; - } } } - if (curDoc.presHideAfter) { - if (itemIndexes.length > 1 && curInd !== itemIndexes.length - 1) { + const hidingIndBef = itemIndexes.find(item => item >= this.itemIndex); + if (curDoc.presHideBefore && index === hidingIndBef) { + if (index > this.itemIndex) { + tagDoc.opacity = 0; + } else if (index === this.itemIndex || !curDoc.presHideAfter) { tagDoc.opacity = 1; - } else { - if (index < this.itemIndex) { - tagDoc.opacity = 0; - } else if (index === this.itemIndex || !curDoc.presHideBefore) { - tagDoc.opacity = 1; - } + } + } + const hidingIndAft = itemIndexes + .slice() + .reverse() + .find(item => item < this.itemIndex); + if (curDoc.presHideAfter && index === hidingIndAft) { + if (index < this.itemIndex) { + tagDoc.opacity = 0; + } else if (index === this.itemIndex || !curDoc.presHideBefore) { + tagDoc.opacity = 1; + } + } + const hidingInd = itemIndexes.find(item => item === this.itemIndex); + if (curDoc.presHide && index === hidingInd) { + if (index === this.itemIndex) { + tagDoc.opacity = 0; } } } }); }; - //The function that starts or resets presentaton functionally, depending on presStatus of the layoutDoc - @action - startAutoPres = (startSlide: number) => { - this.layoutDoc.presStatus = PresStatus.Autoplay; - this.startPresentation(startSlide); - clearTimeout(this._presTimer); - const func = (itemIndex: number) => { - if (itemIndex === this.next()) this.layoutDoc.presStatus = PresStatus.Manual; - else - this._presTimer = setTimeout( - () => this.layoutDoc.presStatus !== PresStatus.Manual && func(this.itemIndex), - NumCast(this.activeItem.presDuration, this.activeItem.type === DocumentType.SCRIPTING ? 0 : 2500) + NumCast(this.activeItem.presTransition) - ); + _exitTrail: Opt<() => void>; + PlayTrail = (doc: Doc) => { + const savedState = { c: doc, x: NumCast(doc.panX), y: NumCast(doc.panY), s: NumCast(doc.viewScale) }; + this.startPresentation(0); + this._exitTrail = () => { + const { x, y, s, c } = savedState; + c._panX = x; + c._panY = y; + c._viewScale = s; + LightboxView.SetLightboxDoc(undefined); + Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, this.rootDoc); + return PresStatus.Edit; }; - - func(this.itemIndex); }; // The function pauses the auto presentation @@ -676,14 +692,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }; // The function allows for viewing the pres path on toggle - @action togglePath = (srcContext: Doc, off?: boolean) => { - if (off) { - this._pathBoolean = false; - srcContext.presPathView = false; - } else { - runInAction(() => (this._pathBoolean = !this._pathBoolean)); - srcContext.presPathView = this._pathBoolean; - } + @action togglePath = (off?: boolean) => { + this._pathBoolean = off ? false : !this._pathBoolean; + CollectionFreeFormView.ShowPresPaths = this._pathBoolean; }; // The function allows for expanding the view of pres on toggle @@ -701,40 +712,54 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { * @param startIndex: index that the presentation will start at */ startPresentation = (startIndex: number) => { - this.childDocs.forEach(doc => { - const tagDoc = doc.presentationTargetDoc as Doc; - if (doc.presHideBefore && this.childDocs.indexOf(doc) > startIndex) { - tagDoc.opacity = 0; - } - if (doc.presHideAfter && this.childDocs.indexOf(doc) < startIndex) { - tagDoc.opacity = 0; - } - }); - this.gotoDocument(startIndex, this.activeItem); + clearTimeout(this._presTimer); + if (this.childDocs.length) { + this.layoutDoc.presStatus = PresStatus.Autoplay; + this.childDocs.forEach(doc => { + const tagDoc = doc.presentationTargetDoc as Doc; + if (doc.presHideBefore && this.childDocs.indexOf(doc) > startIndex) tagDoc.opacity = 0; + if (doc.presHideAfter && this.childDocs.indexOf(doc) < startIndex) tagDoc.opacity = 0; + // if (doc.presHide && this.childDocs.indexOf(doc) === startIndex) tagDoc.opacity = 0; + }); + const func = () => { + const delay = NumCast(this.activeItem.presDuration, this.activeItem.type === DocumentType.SCRIPTING ? 0 : 2500) + NumCast(this.activeItem.presTransition); + this._presTimer = setTimeout(() => { + if (!this.next()) this.layoutDoc.presStatus = this._exitTrail?.() ?? PresStatus.Manual; + this.layoutDoc.presStatus === PresStatus.Autoplay && func(); + }, delay); + }; + this.gotoDocument(startIndex, this.activeItem, undefined, func); + } }; /** * The method called to open the presentation as a minimized view */ @action - updateMinimize = async () => { + enterMinimize = () => { + clearTimeout(this._presTimer); + const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); + this.props.removeDocument?.(this.layoutDoc); + return PresBox.OpenPresMinimized(this.rootDoc, [pt[0] + (this.props.PanelWidth() - 250), pt[1] + 10]); + }; + exitMinimize = () => { if (DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) { - this.layoutDoc.presStatus = PresStatus.Edit; Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, this.rootDoc); - CollectionDockingView.AddSplit(this.rootDoc, 'right'); - } else { - this.layoutDoc.presStatus = PresStatus.Edit; - clearTimeout(this._presTimer); - const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); - this.rootDoc.overlayX = pt[0] + (this.props.PanelWidth() - 250); - this.rootDoc.overlayY = pt[1] + 10; - this.rootDoc._height = 30; - this.rootDoc._width = 248; - Doc.AddDocToList(Doc.MyOverlayDocs, undefined, this.rootDoc); - this.props.removeDocument?.(this.layoutDoc); + CollectionDockingView.AddSplit(this.rootDoc, OpenWhereMod.right); } + return PresStatus.Edit; }; + public static minimizedWidth = 198; + public static OpenPresMinimized(doc: Doc, pt: number[]) { + doc.overlayX = pt[0]; + doc.overlayY = pt[1]; + doc._height = 30; + doc._width = PresBox.minimizedWidth; + Doc.AddDocToList(Doc.MyOverlayDocs, undefined, doc); + return (doc.presStatus = PresStatus.Manual); + } + /** * Called when the user changes the view type * Either 'List' (stacking) or 'Slides' (carousel) @@ -766,20 +791,18 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (this.childDocs[stopDocIndex - 1].mediaStopTriggerList) { const list = DocListCast(this.childDocs[stopDocIndex - 1].mediaStopTriggerList); list.push(activeItem); - // this.childDocs[stopDocIndex - 1].mediaStopTriggerList = list; - console.log(list); + // this.childDocs[stopDocIndex - 1].mediaStopTriggerList = list;\ } else { this.childDocs[stopDocIndex - 1].mediaStopTriggerList = new List<Doc>(); const list = DocListCast(this.childDocs[stopDocIndex - 1].mediaStopTriggerList); list.push(activeItem); // this.childDocs[stopDocIndex - 1].mediaStopTriggerList = list; - console.log(list); } }); movementName = action((activeItem: Doc) => { - if (![PresMovement.Zoom, PresMovement.Pan, PresMovement.Jump, PresMovement.None].includes(StrCast(activeItem.presMovement) as any)) { - activeItem.presMovement = 'zoom'; + if (![PresMovement.Zoom, PresMovement.Pan, PresMovement.Center, PresMovement.Jump, PresMovement.None].includes(StrCast(activeItem.presMovement) as any)) { + return PresMovement.Zoom; } return StrCast(activeItem.presMovement); }); @@ -804,7 +827,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } else { if (!doc.aliasOf) { const original = Doc.MakeAlias(doc); - TabDocView.PinDoc(original); + TabDocView.PinDoc(original, {}); setTimeout(() => this.removeDocument(doc), 0); return false; } else { @@ -868,6 +891,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { //Regular click @action selectElement = async (doc: Doc) => { + CollectionStackedTimeline.CurrentlyPlaying?.map((clip, i) => DocumentManager.Instance.getDocumentView(clip)?.ComponentView?.Pause?.()); this.gotoDocument(this.childDocs.indexOf(doc), this.activeItem); if (doc.presPinView || doc.presentationTargetDoc === this.layoutDoc.presCollection) setTimeout(() => this.updateCurrentPresentation(DocCast(doc.context)), 0); else this.updateCurrentPresentation(DocCast(doc.context)); @@ -920,7 +944,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { else this.regularSelect(doc, ref, drag, focus); }; - static keyEventsWrapper = (e: KeyboardEvent) => PresBox.Instance.keyEvents(e); + static keyEventsWrapper = (e: KeyboardEvent) => PresBox.Instance?.keyEvents(e); // Key for when the presentaiton is active @action @@ -947,11 +971,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { break; case 'Escape': if (DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) { - this.updateMinimize(); - } else if (this.layoutDoc.presStatus === 'edit') { + this.exitClicked(); + } else if (this.layoutDoc.presStatus === PresStatus.Edit) { this.clearSelectedArray(); this._eleArray.length = this._dragArray.length = 0; - } else this.layoutDoc.presStatus = 'edit'; + } else { + this.layoutDoc.presStatus = PresStatus.Edit; + } if (this._presTimer) clearTimeout(this._presTimer); handled = true; break; @@ -1010,17 +1036,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } }; - /** - * - */ - @action - viewPaths = () => { - const srcContext = Cast(this.rootDoc.presCollection, Doc, null); - if (srcContext) { - this.togglePath(srcContext); - } - }; - getAllIndexes = (arr: Doc[], val: Doc) => arr.map((doc, i) => (doc === val ? i : -1)).filter(i => i !== -1); // Adds the index in the pres path graphically @@ -1100,16 +1115,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { */ @computed get paths() { let pathPoints = ''; - const presCollection = Cast(this.rootDoc.presCollection, Doc, null); this.childDocs.forEach((doc, index) => { const tagDoc = Cast(doc.presentationTargetDoc, Doc, null); - const srcContext = Cast(tagDoc?.context, Doc, null); - if (tagDoc && presCollection === srcContext) { + if (tagDoc) { const n1x = NumCast(tagDoc.x) + NumCast(tagDoc._width) / 2; const n1y = NumCast(tagDoc.y) + NumCast(tagDoc._height) / 2; if ((index = 0)) pathPoints = n1x + ',' + n1y; else pathPoints = pathPoints + ' ' + n1x + ',' + n1y; - } else if (doc.presPinView && presCollection === tagDoc) { + } else if (doc.presPinView) { const n1x = NumCast(doc.presPinViewX); const n1y = NumCast(doc.presPinViewY); if ((index = 0)) pathPoints = n1x + ',' + n1y; @@ -1133,14 +1146,18 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { /> ); } + getPaths = (collection: Doc) => this.paths; // needs to be smarter and figure out the paths to draw for this specific collection. or better yet, draw everything in an overlay layer instad of within a collection // Converts seconds to ms and updates presTransition - setTransitionTime = (number: String, change?: number) => { + public static SetTransitionTime = (number: String, setter: (timeInMS: number) => void, change?: number) => { let timeInMS = Number(number) * 1000; if (change) timeInMS += change; if (timeInMS < 100) timeInMS = 100; - if (timeInMS > 10000) timeInMS = 10000; - this.selectedArray.forEach(doc => (doc.presTransition = timeInMS)); + if (timeInMS > 100000) timeInMS = 100000; + setter(timeInMS); + }; + setTransitionTime = (number: String, change?: number) => { + PresBox.SetTransitionTime(number, (timeInMS: number) => this.selectedArray.forEach(doc => (doc.presTransition = timeInMS)), change); }; // Converts seconds to ms and updates presTransition @@ -1176,6 +1193,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @undoBatch @action + updateHide = (activeItem: Doc) => { + activeItem.presHide = !activeItem.presHide; + this.selectedArray.forEach(doc => (doc.presHide = activeItem.presHide)); + }; + + @undoBatch + @action updateHideAfter = (activeItem: Doc) => { activeItem.presHideAfter = !activeItem.presHideAfter; this.selectedArray.forEach(doc => (doc.presHideAfter = activeItem.presHideAfter)); @@ -1187,10 +1211,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { activeItem.openDocument = !activeItem.openDocument; this.selectedArray.forEach(doc => (doc.openDocument = activeItem.openDocument)); }; + @undoBatch + @action + updateEaseFunc = (activeItem: Doc) => { + activeItem.presEaseFunc = activeItem.presEaseFunc === 'linear' ? 'ease' : 'linear'; + this.selectedArray.forEach(doc => (doc.presEaseFunc = activeItem.presEaseFunc)); + }; @undoBatch @action - updateEffectDirection = (effect: PresEffect, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (doc.presEffectDirection = effect)); + updateEffectDirection = (effect: PresEffectDirection, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (doc.presEffectDirection = effect)); @undoBatch @action @@ -1198,13 +1228,36 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { _batch: UndoManager.Batch | undefined = undefined; + public static inputter = (min: string, step: string, max: string, value: number, active: boolean, change: (val: string) => void, hmargin?: number) => { + let batch: any; + return ( + <input + type="range" + step={step} + min={min} + max={max} + value={value} + style={{ marginLeft: hmargin, marginRight: hmargin, width: `calc(100% - ${2 * (hmargin ?? 0)}px)` }} + className={`toolbar-slider ${active ? '' : 'none'}`} + onPointerDown={e => { + batch = UndoManager.StartBatch('pres slider'); + e.stopPropagation(); + }} + onPointerUp={() => batch?.end()} + onChange={e => { + e.stopPropagation(); + change(e.target.value); + }} + /> + ); + }; @computed get transitionDropdown() { const activeItem: Doc = this.activeItem; const targetDoc: Doc = this.targetDoc; const isPresCollection: boolean = targetDoc === this.layoutDoc.presCollection; const isPinWithView: boolean = BoolCast(activeItem.presPinView); const presEffect = (effect: PresEffect) => ( - <div className={`presBox-dropdownOption ${this.activeItem.presEffect === effect || (effect === PresEffect.None && !this.activeItem.presEffect) ? 'active' : ''}`} onPointerDown={StopEvent} onClick={() => this.updateEffect(effect)}> + <div className={`presBox-dropdownOption ${activeItem.presEffect === effect || (effect === PresEffect.None && !activeItem.presEffect) ? 'active' : ''}`} onPointerDown={StopEvent} onClick={() => this.updateEffect(effect)}> {effect} </div> ); @@ -1213,47 +1266,26 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { {movement} </div> ); - const presDirection = (diretion: PresEffect, icon: string, gridColumn: number, gridRow: number, opts: object) => { - const color = this.activeItem.presEffectDirection === diretion || (diretion === PresEffect.Center && !this.activeItem.presEffectDirection) ? Colors.LIGHT_BLUE : 'black'; + const presDirection = (direction: PresEffectDirection, icon: string, gridColumn: number, gridRow: number, opts: object) => { + const color = activeItem.presEffectDirection === direction || (direction === PresEffectDirection.Center && !activeItem.presEffectDirection) ? Colors.LIGHT_BLUE : 'black'; return ( - <Tooltip title={<div className="dash-tooltip">{diretion}</div>}> + <Tooltip title={<div className="dash-tooltip">{direction}</div>}> <div - style={{ ...opts, border: diretion === PresEffect.Center ? `solid 2px ${color}` : undefined, borderRadius: '100%', cursor: 'pointer', gridColumn, gridRow, justifySelf: 'center', color }} - onClick={() => this.updateEffectDirection(diretion)}> + style={{ ...opts, border: direction === PresEffectDirection.Center ? `solid 2px ${color}` : undefined, borderRadius: '100%', cursor: 'pointer', gridColumn, gridRow, justifySelf: 'center', color }} + onClick={() => this.updateEffectDirection(direction)}> {icon ? <FontAwesomeIcon icon={icon as any} /> : null} </div> </Tooltip> ); }; - const inputter = (min: string, step: string, max: string, value: number, active: boolean, change: (val: string) => void) => { - return ( - <input - type="range" - step={step} - min={min} - max={max} - value={value} - className={`toolbar-slider ${active ? '' : 'none'}`} - onPointerDown={e => { - this._batch = UndoManager.StartBatch('pres slider'); - e.stopPropagation(); - }} - onPointerUp={() => this._batch?.end()} - onChange={e => { - e.stopPropagation(); - change(e.target.value); - }} - /> - ); - }; if (activeItem && targetDoc) { const type = targetDoc.type; const transitionSpeed = activeItem.presTransition ? NumCast(activeItem.presTransition) / 1000 : 0.5; const zoom = NumCast(activeItem.presZoom, 1) * 100; - let duration = activeItem.presDuration ? NumCast(activeItem.presDuration) / 1000 : 2; + let duration = activeItem.presDuration ? NumCast(activeItem.presDuration) / 1000 : 0; if (activeItem.type === DocumentType.AUDIO) duration = NumCast(activeItem.duration); - const effect = this.activeItem.presEffect ? this.activeItem.presEffect : 'None'; - activeItem.presMovement = activeItem.presMovement ? activeItem.presMovement : 'Zoom'; + const effect = activeItem.presEffect ? activeItem.presEffect : PresMovement.None; + // activeItem.presMovement = activeItem.presMovement ? activeItem.presMovement : PresMovement.Zoom; return ( <div className={`presBox-ribbon ${this._transitionTools && this.layoutDoc.presStatus === PresStatus.Edit ? 'active' : ''}`} @@ -1277,6 +1309,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openMovementDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} /> <div className={'presBox-dropdownOptions'} id={'presBoxMovementDropdown'} onPointerDown={StopEvent} style={{ display: this._openMovementDropdown ? 'grid' : 'none' }}> {isPresCollection || (isPresCollection && isPinWithView) ? null : presMovement(PresMovement.None)} + {presMovement(PresMovement.Center)} {presMovement(PresMovement.Zoom)} {presMovement(PresMovement.Pan)} {isPresCollection || (isPresCollection && isPinWithView) ? null : presMovement(PresMovement.Jump)} @@ -1296,9 +1329,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> </div> </div> - {inputter('0', '1', '100', zoom, activeItem.presMovement === PresMovement.Zoom, this.setZoom)} - <div className="ribbon-doubleButton" style={{ display: activeItem.presMovement === PresMovement.Pan || activeItem.presMovement === PresMovement.Zoom ? 'inline-flex' : 'none' }}> - <div className="presBox-subheading">Movement Speed</div> + {PresBox.inputter('0', '1', '100', zoom, activeItem.presMovement === PresMovement.Zoom, this.setZoom)} + <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}> + <div className="presBox-subheading">Transition Speed</div> <div className="ribbon-property"> <input className="presBox-input" type="number" value={transitionSpeed} onKeyDown={e => e.stopPropagation()} onChange={action(e => this.setTransitionTime(e.target.value))} /> s </div> @@ -1311,8 +1344,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> </div> </div> - {inputter('0.1', '0.1', '10', transitionSpeed, [PresMovement.Pan, PresMovement.Zoom].includes(activeItem.presMovement as any), this.setTransitionTime)} - <div className={`slider-headers ${activeItem.presMovement === PresMovement.Pan || activeItem.presMovement === PresMovement.Zoom ? '' : 'none'}`}> + {PresBox.inputter('0.1', '0.1', '100', transitionSpeed, true, this.setTransitionTime)} + <div className={'slider-headers'}> <div className="slider-text">Fast</div> <div className="slider-text">Medium</div> <div className="slider-text">Slow</div> @@ -1329,6 +1362,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </Tooltip> )} {isPresCollection ? null : ( + <Tooltip title={<div className="dash-tooltip">{'Hide while presented'}</div>}> + <div className={`ribbon-toggle ${activeItem.presHide ? 'active' : ''}`} onClick={() => this.updateHide(activeItem)}> + Hide + </div> + </Tooltip> + )} + {isPresCollection ? null : ( <Tooltip title={<div className="dash-tooltip">{'Hide after presented'}</div>}> <div className={`ribbon-toggle ${activeItem.presHideAfter ? 'active' : ''}`} onClick={() => this.updateHideAfter(activeItem)}> Hide after @@ -1340,6 +1380,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { Lightbox </div> </Tooltip> + <Tooltip title={<div className="dash-tooltip">{'Transition movement style'}</div>}> + <div className="ribbon-toggle" onClick={() => this.updateEaseFunc(activeItem)}> + {`${StrCast(activeItem.presEaseFunc, 'ease')}`} + </div> + </Tooltip> </div> {type === DocumentType.AUDIO || type === DocumentType.VID ? null : ( <> @@ -1357,7 +1402,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> </div> </div> - {inputter('0.1', '0.1', '20', duration, targetDoc.type !== DocumentType.AUDIO, this.setDurationTime)} + {PresBox.inputter('0.1', '0.1', '20', duration, targetDoc.type !== DocumentType.AUDIO, this.setDurationTime)} <div className={'slider-headers'} style={{ display: targetDoc.type === DocumentType.AUDIO ? 'none' : 'grid' }}> <div className="slider-text">Short</div> <div className="slider-text">Medium</div> @@ -1369,6 +1414,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { {isPresCollection ? null : ( <div className="ribbon-box"> Effects + <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}> + <div className="presBox-subheading">Play Audio Annotation</div> + <input className="presBox-checkbox" style={{ margin: 10 }} type="checkbox" onChange={() => (activeItem.followLinkAudio = !BoolCast(activeItem.followLinkAudio))} checked={BoolCast(activeItem.followLinkAudio)} /> + </div> <div className="presBox-dropdown" onClick={action(e => { @@ -1376,7 +1425,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this._openEffectDropdown = !this._openEffectDropdown; })} style={{ borderBottomLeftRadius: this._openEffectDropdown ? 0 : 5, border: this._openEffectDropdown ? `solid 2px ${Colors.MEDIUM_BLUE}` : 'solid 1px black' }}> - {effect.toString()} + {effect?.toString()} <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} /> <div className={'presBox-dropdownOptions'} id={'presBoxMovementDropdown'} style={{ display: this._openEffectDropdown ? 'grid' : 'none' }} onPointerDown={e => e.stopPropagation()}> {presEffect(PresEffect.None)} @@ -1387,16 +1436,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { {presEffect(PresEffect.Roll)} </div> </div> - <div className="ribbon-doubleButton" style={{ display: effect === 'None' ? 'none' : 'inline-flex' }}> + <div className="ribbon-doubleButton" style={{ display: effect === PresEffectDirection.None ? 'none' : 'inline-flex' }}> <div className="presBox-subheading">Effect direction</div> <div className="ribbon-property">{StrCast(this.activeItem.presEffectDirection)}</div> </div> - <div className="effectDirection" style={{ display: effect === 'None' ? 'none' : 'grid', width: 40 }}> - {presDirection(PresEffect.Left, 'angle-right', 1, 2, {})} - {presDirection(PresEffect.Right, 'angle-left', 3, 2, {})} - {presDirection(PresEffect.Top, 'angle-down', 2, 1, {})} - {presDirection(PresEffect.Bottom, 'angle-up', 2, 3, {})} - {presDirection(PresEffect.Center, '', 2, 2, { width: 10, height: 10, alignSelf: 'center' })} + <div className="effectDirection" style={{ display: effect === PresEffectDirection.None ? 'none' : 'grid', width: 40 }}> + {presDirection(PresEffectDirection.Left, 'angle-right', 1, 2, {})} + {presDirection(PresEffectDirection.Right, 'angle-left', 3, 2, {})} + {presDirection(PresEffectDirection.Top, 'angle-down', 2, 1, {})} + {presDirection(PresEffectDirection.Bottom, 'angle-up', 2, 3, {})} + {presDirection(PresEffectDirection.Center, '', 2, 2, { width: 10, height: 10, alignSelf: 'center' })} </div> </div> )} @@ -1415,7 +1464,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { applyTo = (array: Doc[]) => { this.updateMovement(this.activeItem.presMovement as PresMovement, true); this.updateEffect(this.activeItem.presEffect as PresEffect, true); - this.updateEffectDirection(this.activeItem.presEffectDirection as PresEffect, true); + this.updateEffectDirection(this.activeItem.presEffectDirection as PresEffectDirection, true); const { presTransition, presDuration, presHideBefore, presHideAfter } = this.activeItem; array.forEach(curDoc => { curDoc.presTransition = presTransition; @@ -1745,10 +1794,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const presData = Cast(this.rootDoc.data, listSpec(Doc)); if (data && presData) { data.push(doc); - TabDocView.PinDoc(doc); + TabDocView.PinDoc(doc, {}); this.gotoDocument(this.childDocs.length, this.activeItem); } else { - this.props.addDocTab(doc, 'add:right'); + this.props.addDocTab(doc, OpenWhere.addRight); } } }; @@ -1781,7 +1830,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { className="dropdown-play-button" onClick={undoBatch( action(() => { - this.updateMinimize(); + this.enterMinimize(); this.turnOffEdit(true); this.gotoDocument(this.itemIndex, this.activeItem); }) @@ -1804,37 +1853,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } scrollFocus = () => { - this.startOrPause(false); + // this.gotoDocument(0); + // this.startOrPause(false); return undefined; }; - // Case in which the document has keyframes to navigate to next key frame - @action - nextKeyframe = (tagDoc: Doc, curDoc: Doc): void => { - const childDocs = DocListCast(tagDoc[Doc.LayoutFieldKey(tagDoc)]); - const currentFrame = Cast(tagDoc._currentFrame, 'number', null); - if (currentFrame === undefined) { - tagDoc._currentFrame = 0; - // CollectionFreeFormDocumentView.setupScroll(tagDoc, 0); - // CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0); - } - // if (tagDoc.editScrollProgressivize) 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)); - }; - - @action - prevKeyframe = (tagDoc: Doc, actItem: Doc): void => { - const childDocs = DocListCast(tagDoc[Doc.LayoutFieldKey(tagDoc)]); - const currentFrame = Cast(tagDoc._currentFrame, 'number', null); - if (currentFrame === undefined) { - tagDoc._currentFrame = 0; - // CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0); - } - CollectionFreeFormDocumentView.gotoKeyframe(childDocs.slice()); - tagDoc._currentFrame = Math.max(0, (currentFrame || 0) - 1); - }; + _keyTimer: NodeJS.Timeout | undefined; /** * Returns the collection type as a string for headers @@ -1860,123 +1884,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @observable private openActiveColorPicker: boolean = false; @observable private openViewedColorPicker: boolean = false; - @computed get progressivizeDropdown() { - 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 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') || targetDoc.type === DocumentType.IMG || targetDoc.type === DocumentType.RTF ? 'inline-flex' : 'none', - }}> - <div className="ribbon-toggle" style={{ backgroundColor: activeItem.presProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.progressivizeChild}> - Contents - </div> - <div className="ribbon-toggle" style={{ opacity: activeItem.presProgressivize ? 1 : 0.4, backgroundColor: targetDoc.editProgressivize ? Colors.LIGHT_BLUE : '' }} 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-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-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-toggle" style={{ backgroundColor: activeItem.zoomProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.progressivizeZoom}> - Zoom - </div> - <div className="ribbon-toggle" style={{ opacity: activeItem.zoomProgressivize ? 1 : 0.4, backgroundColor: activeItem.editZoomProgressivize ? Colors.LIGHT_BLUE : '' }} 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-toggle" style={{ backgroundColor: activeItem.scrollProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.progressivizeScroll}> - Scroll - </div> - <div className="ribbon-toggle" style={{ opacity: activeItem.scrollProgressivize ? 1 : 0.4, backgroundColor: targetDoc.editScrollProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.editScrollProgressivize}> - Edit - </div> - </div> - </div> - <div className="ribbon-final-box"> - Frames - <div className="ribbon-doubleButton"> - <div className="ribbon-frameSelector"> - <div - key="back" - title="back frame" - className="backKeyframe" - onClick={e => { - e.stopPropagation(); - this.prevKeyframe(targetDoc, activeItem); - }}> - <FontAwesomeIcon icon={'caret-left'} size={'lg'} /> - </div> - <div - key="num" - title="toggle view all" - className="numKeyframe" - style={{ color: targetDoc.keyFrameEditing ? 'white' : 'black', backgroundColor: targetDoc.keyFrameEditing ? Colors.MEDIUM_BLUE : Colors.LIGHT_BLUE }} - onClick={action(() => (targetDoc.keyFrameEditing = !targetDoc.keyFrameEditing))}> - {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'} /> - </div> - </div> - <Tooltip - title={ - <> - <div className="dash-tooltip">{'Last frame'}</div> - </> - }> - <div className="ribbon-property">{NumCast(targetDoc.lastFrame)}</div> - </Tooltip> - </div> - <div className="ribbon-frameList"> - {this.frameListHeader} - {this.frameList} - </div> - <div className="ribbon-toggle" style={{ height: 20, backgroundColor: Colors.LIGHT_BLUE }} onClick={() => console.log(' TODO: play frames')}> - Play - </div> - </div> - </div> - ); - } - } - @undoBatch @action switchActive = (color: ColorState) => { @@ -2011,262 +1918,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } @action - turnOffEdit = (paths?: boolean) => { - // Turn off paths - if (paths) { - const srcContext = Cast(this.rootDoc.presCollection, Doc, null); - if (srcContext) this.togglePath(srcContext, true); - } - // Turn off the progressivize editors for each document - this.childDocs.forEach(doc => { - doc.editSnapZoomProgressivize = false; - doc.editZoomProgressivize = false; - const targetDoc = Cast(doc.presentationTargetDoc, Doc, null); - if (targetDoc) { - targetDoc.editZoomProgressivize = false; - // targetDoc.editScrollProgressivize = false; - } - }); - }; - - //Toggle whether the user edits or not - @action - editZoomProgressivize = (e: React.MouseEvent) => { - 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; - activeItem.editZoomProgressivize = true; - } else { - targetDoc.editZoomProgressivize = false; - activeItem.editZoomProgressivize = false; - } - }; - - //Toggle whether the user edits or not - @action - editScrollProgressivize = (e: React.MouseEvent) => { - const targetDoc: Doc = this.targetDoc; - if (!targetDoc.editScrollProgressivize) { - if (!targetDoc.scrollProgressivize) { - targetDoc.scrollProgressivize = true; - this.activeItem.scrollProgressivize = true; - } - targetDoc.editScrollProgressivize = true; - } else { - targetDoc.editScrollProgressivize = false; - } - }; - - //Progressivize Zoom - @action - progressivizeScroll = (e: React.MouseEvent) => { - e.stopPropagation(); - this.activeItem.scrollProgressivize = !this.activeItem.scrollProgressivize; - const targetDoc: Doc = this.targetDoc; - targetDoc.scrollProgressivize = !targetDoc.scrollProgressivize; - // CollectionFreeFormDocumentView.setupScroll(targetDoc, NumCast(targetDoc._currentFrame)); - if (targetDoc.editScrollProgressivize) { - targetDoc.editScrollProgressivize = false; - targetDoc._currentFrame = 0; - targetDoc.lastFrame = 0; - } - }; - - //Progressivize Zoom - @action - progressivizeZoom = (e: React.MouseEvent) => { - e.stopPropagation(); - const activeItem: Doc = this.activeItem; - activeItem.zoomProgressivize = !activeItem.zoomProgressivize; - const targetDoc: Doc = this.targetDoc; - targetDoc.zoomProgressivize = !targetDoc.zoomProgressivize; - CollectionFreeFormDocumentView.setupZoom(activeItem, targetDoc); - if (activeItem.editZoomProgressivize) { - activeItem.editZoomProgressivize = false; - targetDoc._currentFrame = 0; - targetDoc.lastFrame = 0; - } - }; - - //Progressivize Child Docs - @action - editProgressivize = (e: React.MouseEvent) => { - 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; - } else { - targetDoc.editProgressivize = false; - } - }; - - @action - progressivizeChild = (e: React.MouseEvent) => { - e.stopPropagation(); - const activeItem: Doc = this.activeItem; - const targetDoc: Doc = this.targetDoc; - const docs = DocListCast(targetDoc[Doc.LayoutFieldKey(targetDoc)]); - if (!activeItem.presProgressivize) { - targetDoc.keyFrameEditing = false; - activeItem.presProgressivize = true; - targetDoc.presProgressivize = true; - 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.keyFrameEditing = true; - } - }; - - @action - checkMovementLists = (doc: Doc, xlist: any, ylist: any) => { - const x: List<number> = xlist; - const y: List<number> = ylist; - const tags: JSX.Element[] = []; - let pathPoints = ''; //List of all of the pathpoints that need to be added - for (let i = 0; i < x.length - 1; i++) { - if (y[i] || x[i]) { - if (i === 0) pathPoints = x[i] - 11 + ',' + (y[i] + 33); - else pathPoints = pathPoints + ' ' + (x[i] - 11) + ',' + (y[i] + 33); - tags.push( - <div className="progressivizeMove-frame" style={{ position: 'absolute', top: y[i], left: x[i] }}> - {i} - </div> - ); - } - } - tags.push( - <svg style={{ overflow: 'visible', position: 'absolute' }}> - <polyline - points={pathPoints} - style={{ - position: 'absolute', - opacity: 1, - stroke: '#000000', - strokeWidth: 2, - strokeDasharray: '10 5', - }} - fill="none" - /> - </svg> - ); - return tags; - }; - - @observable - toggleDisplayMovement = (doc: Doc) => (doc.displayMovement = !doc.displayMovement); - - @action - checkList = (doc: Doc, list: any): number => { - const x: List<number> = list; - if (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)]; - } - return 100; - }; - - @computed get progressivizeChildDocs() { - const targetDoc: Doc = this.targetDoc; - const docs = DocListCast(targetDoc[Doc.LayoutFieldKey(targetDoc)]); - const tags: JSX.Element[] = []; - docs.forEach((doc, index) => { - if (doc['x-indexed'] && doc['y-indexed']) { - 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" - 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 ? Colors.LIGHT_BLUE : '#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">{NumCast(doc.appearFrame)}</div> - <div className="progressivizeButton-next"> - <FontAwesomeIcon - icon={'caret-right'} - size={'lg'} - onClick={e => { - e.stopPropagation(); - this.nextAppearFrame(doc, index); - }} - /> - </div> - </div> - ); - }); - return tags; - } - - @action - nextAppearFrame = (doc: Doc, i: number) => { - doc.appearFrame = (Cast(doc.appearFrame, 'number', null) ?? 0) + 1; - this.updateOpacityList(doc['opacity-indexed'], NumCast(doc.appearFrame)); - }; - - @action - prevAppearFrame = (doc: Doc, i: number) => { - doc.appearFrame = Math.max(0, (Cast(doc.appearFrame, 'number', null) ?? 0) - 1); - this.updateOpacityList(doc['opacity-indexed'], NumCast(doc.appearFrame)); - }; - - @action - updateOpacityList = (list: any, frame: number) => { - const x: List<number> = list; - if (x && x.length >= frame) { - for (let i = 0; i < x.length; i++) { - if (i < frame) { - x[i] = 0; - } else if (i >= frame) { - x[i] = 1; - } - } - list = x; - } else { - x.length = frame + 1; - for (let i = 0; i < x.length; i++) { - if (i < frame) { - x[i] = 0; - } else if (i >= frame) { - x[i] = 1; - } - } - list = x; - } - }; + turnOffEdit = (paths?: boolean) => paths && this.togglePath(true); // Turn off paths @computed get toolbarWidth(): number { @@ -2289,11 +1941,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <FontAwesomeIcon icon={"plus"} /> <FontAwesomeIcon className={`dropdown ${this.newDocumentTools ? "active" : ""}`} icon={"angle-down"} /> </div></Tooltip> */} - <Tooltip title={<div className="dash-tooltip">{'View paths'}</div>}> + <Tooltip title={<div className="dash-tooltip">View paths</div>}> <div - style={{ opacity: this.childDocs.length > 1 && this.layoutDoc.presCollection ? 1 : 0.3, color: this._pathBoolean ? Colors.MEDIUM_BLUE : 'white', width: isMini ? '100%' : undefined }} + style={{ opacity: this.childDocs.length > 1 ? 1 : 0.3, color: this._pathBoolean ? Colors.MEDIUM_BLUE : 'white', width: isMini ? '100%' : undefined }} className={'toolbar-button'} - onClick={this.childDocs.length > 1 && this.layoutDoc.presCollection ? this.viewPaths : undefined}> + onClick={this.childDocs.length > 1 ? () => this.togglePath() : undefined}> <FontAwesomeIcon icon={'exchange-alt'} /> </div> </Tooltip> @@ -2332,8 +1984,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @computed get topPanel() { const mode = StrCast(this.rootDoc._viewType) as CollectionViewType; const isMini: boolean = this.toolbarWidth <= 100; + const inOverlay = DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc); return ( - <div className="presBox-buttons" style={{ background: Doc.ActivePresentation === this.rootDoc ? Colors.LIGHT_BLUE : undefined, display: !this.rootDoc._chromeHidden ? 'none' : undefined }}> + <div className={`presBox-buttons${inOverlay ? ' inOverlay' : ''}`} style={{ background: Doc.ActivePresentation === this.rootDoc ? Colors.LIGHT_BLUE : undefined, display: !this.rootDoc._chromeHidden ? 'none' : undefined }}> {isMini ? null : ( <select className="presBox-viewPicker" style={{ display: this.layoutDoc.presStatus === 'edit' ? 'block' : 'none' }} onPointerDown={e => e.stopPropagation()} onChange={this.viewChanged} value={mode}> <option onPointerDown={StopEvent} value={CollectionViewType.Stacking}> @@ -2350,7 +2003,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </select> )} <div className="presBox-presentPanel" style={{ opacity: this.childDocs.length ? 1 : 0.3 }}> - <span className={`presBox-button ${this.layoutDoc.presStatus === 'edit' ? 'present' : ''}`}> + <span className={`presBox-button ${this.layoutDoc.presStatus === PresStatus.Edit ? 'present' : ''}`}> <div className="presBox-button-left" onClick={undoBatch(() => { @@ -2379,89 +2032,18 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { ); } - @action - getList = (list: any): List<number> => list; - - @action - updateList = (list: any): List<number> => { - const targetDoc: Doc = this.targetDoc; - const x: List<number> = list; - x[x.length - 1] = NumCast(targetDoc._scrollY); - return x; - }; - - @action - newFrame = () => { - const activeItem: Doc = this.activeItem; - const type: string = StrCast(this.targetDoc.type); - if (!activeItem.frameList) activeItem.frameList = new List<number>(); - switch (type) { - case DocumentType.PDF || DocumentType.RTF || DocumentType.WEB: - this.updateList(activeItem.frameList); - break; - } - }; - - @computed get frameListHeader() { - return ( - <div className="frameList-header"> - Frames {this.panable ? <i>Panable</i> : this.scrollable ? <i>Scrollable</i> : null} - <div className={'frameList-headerButtons'}> - <Tooltip title={<div className="dash-tooltip">{'Add frame by example'}</div>}> - <div - className={'headerButton'} - onClick={e => { - e.stopPropagation(); - this.newFrame(); - }}> - <FontAwesomeIcon icon={'plus'} onPointerDown={e => e.stopPropagation()} /> - </div> - </Tooltip> - <Tooltip title={<div className="dash-tooltip">{'Edit in collection'}</div>}> - <div - className={'headerButton'} - onClick={e => { - e.stopPropagation(); - console.log('New frame'); - }}> - <FontAwesomeIcon icon={'edit'} onPointerDown={e => e.stopPropagation()} /> - </div> - </Tooltip> - </div> - </div> - ); - } - - @computed get frameList() { - const frameList: List<number> = this.getList(this.activeItem.frameList); - return !frameList ? null : ( - <div className="frameList-container"> - {frameList.map(value => ( - <div className="framList-item" /> - ))} - </div> - ); - } - - @computed get playButtonFrames() { - const targetDoc = this.targetDoc; - return !this.targetDoc ? null : ( - <div className="presPanel-button-frame" style={{ display: targetDoc.lastFrame !== undefined && targetDoc.lastFrame >= 0 ? 'inline-flex' : 'none' }}> - <div>{NumCast(targetDoc._currentFrame)}</div> - <div className="presPanel-divider" style={{ border: 'solid 0.5px white', height: '60%' }}></div> - <div>{NumCast(targetDoc.lastFrame)}</div> - </div> - ); - } - @computed get playButtons() { const presEnd: boolean = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1; const presStart: boolean = !this.layoutDoc.presLoop && this.itemIndex === 0; + const inOverlay = DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc); // 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 ? Colors.MEDIUM_BLUE : 'white' }} onClick={() => (this.layoutDoc.presLoop = !this.layoutDoc.presLoop)}> + <div + className="presPanel-button" + style={{ color: this.layoutDoc.presLoop ? Colors.MEDIUM_BLUE : 'white' }} + onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => (this.layoutDoc.presLoop = !this.layoutDoc.presLoop), false, false)}> <FontAwesomeIcon icon={'redo-alt'} /> </div> </Tooltip> @@ -2469,43 +2051,77 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="presPanel-button" style={{ opacity: presStart ? 0.4 : 1 }} - onClick={e => { - this.back(); - if (this._presTimer) { - clearTimeout(this._presTimer); - this.layoutDoc.presStatus = PresStatus.Manual; - } - e.stopPropagation(); - }}> + onPointerDown={e => + setupMoveUpEvents( + this, + e, + returnFalse, + emptyFunction, + () => { + this.back(); + if (this._presTimer) { + clearTimeout(this._presTimer); + this.layoutDoc.presStatus = PresStatus.Manual; + } + e.stopPropagation(); + }, + false, + false + ) + }> <FontAwesomeIcon icon={'arrow-left'} /> </div> <Tooltip title={<div className="dash-tooltip">{this.layoutDoc.presStatus === PresStatus.Autoplay ? 'Pause' : 'Autoplay'}</div>}> - <div className="presPanel-button" onClick={e => this.startOrPause(true)}> + <div className="presPanel-button" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => this.startOrPause(true), false, false)}> <FontAwesomeIcon icon={this.layoutDoc.presStatus === PresStatus.Autoplay ? 'pause' : 'play'} /> </div> </Tooltip> <div className="presPanel-button" style={{ opacity: presEnd ? 0.4 : 1 }} - onClick={e => { - this.next(); - if (this._presTimer) { - clearTimeout(this._presTimer); - this.layoutDoc.presStatus = PresStatus.Manual; - } - e.stopPropagation(); - }}> + onPointerDown={e => + setupMoveUpEvents( + this, + e, + returnFalse, + emptyFunction, + () => { + this.next(); + if (this._presTimer) { + clearTimeout(this._presTimer); + this.layoutDoc.presStatus = PresStatus.Manual; + } + e.stopPropagation(); + }, + false, + false + ) + }> <FontAwesomeIcon icon={'arrow-right'} /> </div> <div className="presPanel-divider"></div> <Tooltip title={<div className="dash-tooltip">{'Click to return to 1st slide'}</div>}> - <div className="presPanel-button" style={{ border: 'solid 1px white' }} onClick={() => this.nextSlide(0)}> + <div + className="presPanel-button" + style={{ border: 'solid 1px white' }} + onPointerDown={e => + setupMoveUpEvents( + this, + e, + returnFalse, + emptyFunction, + () => { + this.nextSlide(0); + }, + false, + false + ) + }> <b>1</b> </div> </Tooltip> - <div className="presPanel-button-text" onClick={() => this.gotoDocument(0, this.activeItem)} style={{ display: this.props.PanelWidth() > 250 ? 'inline-flex' : 'none' }}> - Slide {this.itemIndex + 1} / {this.childDocs.length} - {this.playButtonFrames} + <div className="presPanel-button-text" onClick={() => this.gotoDocument(0, this.activeItem)} style={{ display: inOverlay || this.props.PanelWidth() > 250 ? 'inline-flex' : 'none' }}> + {`${inOverlay ? '' : 'Slide'} ${this.itemIndex + 1} / ${this.childDocs.length}`} </div> <div className="presPanel-divider"></div> {this.props.PanelWidth() > 250 ? ( @@ -2513,14 +2129,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { className="presPanel-button-text" onClick={undoBatch( action(() => { - this.layoutDoc.presStatus = 'edit'; + this.layoutDoc.presStatus = PresStatus.Edit; clearTimeout(this._presTimer); }) )}> EXIT </div> ) : ( - <div className="presPanel-button" onClick={undoBatch(action(() => (this.layoutDoc.presStatus = 'edit')))}> + <div className="presPanel-button" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, this.exitClicked, false, false)}> <FontAwesomeIcon icon={'times'} /> </div> )} @@ -2531,7 +2147,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @action startOrPause = (makeActive = true) => { makeActive && this.updateCurrentPresentation(); - if (this.layoutDoc.presStatus === PresStatus.Manual || this.layoutDoc.presStatus === PresStatus.Edit) this.startAutoPres(this.itemIndex); + if (this.layoutDoc.presStatus === PresStatus.Manual || this.layoutDoc.presStatus === PresStatus.Edit) this.startPresentation(this.itemIndex); else this.pauseAutoPres(); }; @@ -2554,15 +2170,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }; @undoBatch @action - exitClicked = (e: PointerEvent) => { - this.updateMinimize(); - this.layoutDoc.presStatus = PresStatus.Edit; + exitClicked = () => { + this.layoutDoc.presStatus = this._exitTrail?.() ?? this.exitMinimize(); clearTimeout(this._presTimer); }; - @action - startMarqueeCreateSlide = () => (PresBox.startMarquee = true); - AddToMap = (treeViewDoc: Doc, index: number[]): Doc[] => { var indexNum = 0; for (let i = 0; i < index.length; i++) { @@ -2594,8 +2206,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { // needed to ensure that the childDocs are loaded for looking up fields this.childDocs.slice(); const mode = StrCast(this.rootDoc._viewType) as CollectionViewType; - const presEnd: boolean = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1; - const presStart: boolean = !this.layoutDoc.presLoop && this.itemIndex === 0; + const presEnd = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1; + const presStart = !this.layoutDoc.presLoop && this.itemIndex === 0; + const inOverlay = DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc); return this.props.addDocTab === returnFalse ? ( // bcz: hack!! - addDocTab === returnFalse only when this is being rendered by the OverlayView which means the doc is a mini player <div className="miniPres" onClick={e => e.stopPropagation()} onPointerEnter={action(e => (this._forceKeyEvents = true))}> <div @@ -2629,7 +2242,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </Tooltip> <div className="presPanel-button-text"> Slide {this.itemIndex + 1} / {this.childDocs.length} - {this.playButtonFrames} </div> <div className="presPanel-divider" /> <div className="presPanel-button-text" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, this.exitClicked, false, false)}> @@ -2638,7 +2250,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> </div> ) : ( - <div className="presBox-cont" style={{ minWidth: DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc) ? 240 : undefined }}> + <div className="presBox-cont" style={{ minWidth: inOverlay ? PresBox.minimizedWidth : undefined }}> {this.topPanel} {this.toolbar} {this.newDocumentToolbarDropdown} @@ -2652,6 +2264,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { PanelHeight={this.panelHeight} childIgnoreNativeSize={true} moveDocument={returnFalse} + ignoreUnrendered={true} //childFitWidth={returnTrue} childOpacity={returnOne} childLayoutTemplate={this.childLayoutTemplate} @@ -2664,7 +2277,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { ScreenToLocalTransform={this.getTransform} AddToMap={this.AddToMap} RemFromMap={this.RemFromMap} - hierarchyIndex={[]} + hierarchyIndex={emptyPath} /> ) : null} </div> @@ -2684,7 +2297,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { static NavigateToDoc(bestTarget: Doc, activeItem: Doc) { const srcContext = Cast(bestTarget.context, Doc, null) ?? Cast(Cast(bestTarget.annotationOn, Doc, null)?.context, Doc, null); const openInTab = (doc: Doc, finished?: () => void) => { - CollectionDockingView.AddSplit(doc, 'right'); + CollectionDockingView.AddSplit(doc, OpenWhereMod.right); finished?.(); }; PresBox.NavigateToTarget(bestTarget, activeItem, openInTab, srcContext); diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx index 5d14a0e9a..f1c97d26a 100644 --- a/src/client/views/nodes/trails/PresElementBox.tsx +++ b/src/client/views/nodes/trails/PresElementBox.tsx @@ -50,9 +50,6 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { @computed get collapsedHeight() { return 35; } // the collapsed height changes depending on the state of the presBox. We could store this on the presentation element template if it's used by only one presentation - but if it's shared by multiple, then this value must be looked up - @computed get presStatus() { - return this.presBox.presStatus; - } @computed get selectedArray() { return this.presBoxView?.selectedArray; } @@ -364,14 +361,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { }; @computed get recordingIsInOverlay() { - let isInOverlay = false; - DocListCast(Doc.MyOverlayDocs.data).forEach(doc => { - if (doc.slides === this.rootDoc) { - isInOverlay = true; - // return - } - }); - return isInOverlay; + return DocListCast(Doc.MyOverlayDocs.data).some(doc => doc.slides === this.rootDoc); } // a previously recorded video will have timecode defined @@ -502,16 +492,17 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { </Tooltip> ); } - if (this.indexInPres === 0) { + if (this.indexInPres !== 0) { items.push( <Tooltip key="arrow" title={<div className="dash-tooltip">{activeItem.groupWithUp ? 'Ungroup' : 'Group with up'}</div>}> <div className="slideButton" - onClick={() => (activeItem.groupWithUp = !activeItem.groupWithUp)} + onClick={() => (activeItem.groupWithUp = (NumCast(activeItem.groupWithUp) + 1) % 3)} style={{ zIndex: 1000 - this.indexInPres, fontWeight: 700, backgroundColor: activeItem.groupWithUp ? (presColorBool ? presBoxColor : Colors.MEDIUM_BLUE) : undefined, + outline: NumCast(activeItem.groupWithUp) > 1 ? 'solid black 1px' : undefined, height: activeItem.groupWithUp ? 53 : 18, transform: activeItem.groupWithUp ? 'translate(0, -17px)' : undefined, }}> diff --git a/src/client/views/nodes/trails/PresEnums.ts b/src/client/views/nodes/trails/PresEnums.ts index c6a222c3a..564829d54 100644 --- a/src/client/views/nodes/trails/PresEnums.ts +++ b/src/client/views/nodes/trails/PresEnums.ts @@ -1,6 +1,7 @@ export enum PresMovement { Zoom = 'zoom', Pan = 'pan', + Center = 'center', Jump = 'jump', None = 'none', } @@ -14,11 +15,15 @@ export enum PresEffect { Bounce = 'Bounce', Roll = 'Roll', None = 'None', +} + +export enum PresEffectDirection { Left = 'Enter from left', Right = 'Enter from right', Center = 'Enter from center', Top = 'Enter from Top', Bottom = 'Enter from bottom', + None = 'None', } export enum PresStatus { diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx index ee2ae10a7..265328036 100644 --- a/src/client/views/pdf/AnchorMenu.tsx +++ b/src/client/views/pdf/AnchorMenu.tsx @@ -210,14 +210,13 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { </button> </Tooltip> ), - //NOTE: link popup is currently in progress <Tooltip key="link" title={<div className="dash-tooltip">{'Find document to link to selected text'}</div>}> <button className="antimodeMenu-button link" onPointerDown={this.toggleLinkPopup} style={{}}> <FontAwesomeIcon style={{ position: 'absolute', transform: 'scale(1.5)' }} icon={'search'} size="lg" /> <FontAwesomeIcon style={{ position: 'absolute', transform: 'scale(0.5)', transformOrigin: 'top left', top: 12, left: 12 }} icon={'link'} size="lg" /> </button> </Tooltip>, - <LinkPopup key="popup" showPopup={this._showLinkPopup} linkFrom={this.onMakeAnchor} />, + <LinkPopup key="popup" showPopup={this._showLinkPopup} linkCreateAnchor={this.onMakeAnchor} />, AnchorMenu.Instance.StartCropDrag === unimplementedFunction ? ( <></> ) : ( diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx index ee418a02f..7069ff399 100644 --- a/src/client/views/pdf/Annotation.tsx +++ b/src/client/views/pdf/Annotation.tsx @@ -22,7 +22,7 @@ interface IAnnotationProps extends FieldViewProps { export class Annotation extends React.Component<IAnnotationProps> { render() { return ( - <div> + <div style={{ display: this.props.anno.textCopied && !Doc.isBrushedHighlightedDegree(this.props.anno) ? 'none' : undefined }}> {DocListCast(this.props.anno.textInlineAnnotations).map(a => ( <RegionAnnotation pointerEvents={this.props.pointerEvents} {...this.props} document={a} key={a[Id]} /> ))} @@ -52,12 +52,12 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> { }; @undoBatch - pinToPres = () => this.props.pinToPres(this.annoTextRegion); + pinToPres = () => this.props.pinToPres(this.annoTextRegion, {}); @undoBatch - makePushpin = () => (this.annoTextRegion.isPushpin = !this.annoTextRegion.isPushpin); + makePushpin = () => (this.annoTextRegion.followLinkToggle = !this.annoTextRegion.followLinkToggle); - isPushpin = () => BoolCast(this.annoTextRegion.isPushpin); + isPushpin = () => BoolCast(this.annoTextRegion.followLinkToggle); @action onPointerDown = (e: React.PointerEvent) => { diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss index 822af6a68..470aa3eb1 100644 --- a/src/client/views/pdf/PDFViewer.scss +++ b/src/client/views/pdf/PDFViewer.scss @@ -27,14 +27,14 @@ opacity: unset; mix-blend-mode: multiply; // bcz: makes text fuzzy! - span { - padding-right: 5px; - padding-bottom: 4px; - } + // span { + // padding-right: 5px; + // padding-bottom: 4px; + // } } .textLayer ::selection { - background: #ACCEF7; + background: #accef7; } // should match the backgroundColor in createAnnotation() @@ -111,4 +111,4 @@ .pdfViewerDash-interactive { pointer-events: all; -}
\ No newline at end of file +} diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 5c10c7cef..f95d5ac2e 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -16,7 +16,7 @@ import { SnappingManager } from '../../util/SnappingManager'; import { MarqueeOptionsMenu } from '../collections/collectionFreeForm'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { MarqueeAnnotator } from '../MarqueeAnnotator'; -import { DocumentViewProps } from '../nodes/DocumentView'; +import { DocFocusOptions, DocumentViewProps } from '../nodes/DocumentView'; import { FieldViewProps } from '../nodes/FieldView'; import { LinkDocPreview } from '../nodes/LinkDocPreview'; import { StyleProp } from '../StyleProvider'; @@ -68,16 +68,22 @@ export class PDFViewer extends React.Component<IViewerProps> { private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); private _disposers: { [name: string]: IReactionDisposer } = {}; private _viewer: React.RefObject<HTMLDivElement> = React.createRef(); - public _getAnchor: (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => Opt<Doc> = () => undefined; _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); private _selectionText: string = ''; + private _selectionContent: DocumentFragment | undefined; private _downX: number = 0; private _downY: number = 0; private _lastSearch = false; private _viewerIsSetup = false; private _ignoreScroll = false; - private _initialScroll: Opt<number>; + private _initialScroll: { loc: Opt<number>; easeFunc: 'linear' | 'ease' | undefined } | undefined; private _forcedScroll = true; + get _getAnchor() { + return AnchorMenu.Instance?.GetAnchor; + } + + selectionText = () => this._selectionText; + selectionContent = () => this._selectionContent; @observable isAnnotating = false; // key where data is stored @@ -129,6 +135,11 @@ export class PDFViewer extends React.Component<IViewerProps> { copy = (e: ClipboardEvent) => { if (this.props.isContentActive() && e.clipboardData) { e.clipboardData.setData('text/plain', this._selectionText); + const anchor = this._getAnchor(); + if (anchor) { + anchor.textCopied = true; + e.clipboardData.setData('dash/pdfAnchor', anchor[Id]); + } e.preventDefault(); } }; @@ -157,21 +168,23 @@ export class PDFViewer extends React.Component<IViewerProps> { } }; + _scrollStopper: undefined | (() => void); + // scrolls to focus on a nested annotation document. if this is part a link preview then it will jump to the scroll location, // otherwise it will scroll smoothly. - scrollFocus = (doc: Doc, smooth: boolean) => { + scrollFocus = (doc: Doc, scrollTop: number, options: DocFocusOptions) => { const mainCont = this._mainCont.current; let focusSpeed: Opt<number>; if (doc !== this.props.rootDoc && mainCont) { const windowHeight = this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); - const scrollTo = doc.unrendered ? NumCast(doc.y) : Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.props.layoutDoc._scrollTop), windowHeight, 0.1 * windowHeight, NumCast(this.props.Document.scrollHeight)); + const scrollTo = doc.unrendered ? scrollTop : Utils.scrollIntoView(scrollTop, doc[HeightSym](), NumCast(this.props.layoutDoc._scrollTop), windowHeight, 0.1 * windowHeight, NumCast(this.props.Document.scrollHeight)); if (scrollTo !== undefined && scrollTo !== this.props.layoutDoc._scrollTop) { - if (!this._pdfViewer) this._initialScroll = scrollTo; - else if (smooth) smoothScroll((focusSpeed = NumCast(doc.focusSpeed, 500)), mainCont, scrollTo); + if (!this._pdfViewer) this._initialScroll = { loc: scrollTo, easeFunc: options.easeFunc }; + else if (!options.instant) this._scrollStopper = smoothScroll((focusSpeed = options.zoomTime ?? 500), mainCont, scrollTo, options.easeFunc, this._scrollStopper); else this._mainCont.current?.scrollTo({ top: Math.abs(scrollTo || 0) }); } } else { - this._initialScroll = NumCast(this.props.layoutDoc._scrollTop); + this._initialScroll = { loc: NumCast(this.props.layoutDoc._scrollTop), easeFunc: options.easeFunc }; } return focusSpeed; }; @@ -195,13 +208,18 @@ export class PDFViewer extends React.Component<IViewerProps> { this.gotoPage(NumCast(this.props.Document._curPage, 1)); } document.removeEventListener('pagesinit', this.pagesinit); - var quickScroll: string | undefined = this._initialScroll ? this._initialScroll.toString() : ''; + var quickScroll: { loc?: string; easeFunc?: 'ease' | 'linear' } | undefined = { loc: this._initialScroll ? this._initialScroll.loc?.toString() : '', easeFunc: this._initialScroll ? this._initialScroll.easeFunc : undefined }; + this._disposers.scale = reaction( + () => NumCast(this.props.layoutDoc._viewScale, 1), + scale => (this._pdfViewer.currentScaleValue = scale), + { fireImmediately: true } + ); this._disposers.scroll = reaction( () => Math.abs(NumCast(this.props.Document._scrollTop)), pos => { if (!this._ignoreScroll) { this._showWaiting && this.setupPdfJsViewer(); - const viewTrans = quickScroll ?? StrCast(this.props.Document._viewTransition); + const viewTrans = quickScroll?.loc ?? StrCast(this.props.Document._viewTransition); const durationMiliStr = viewTrans.match(/([0-9]*)ms/); const durationSecStr = viewTrans.match(/([0-9.]*)s/); const duration = durationMiliStr ? Number(durationMiliStr[1]) : durationSecStr ? Number(durationSecStr[1]) * 1000 : 0; @@ -209,7 +227,7 @@ export class PDFViewer extends React.Component<IViewerProps> { if (duration) { setTimeout( () => { - this._mainCont.current && smoothScroll(duration, this._mainCont.current, pos); + this._mainCont.current && (this._scrollStopper = smoothScroll(duration, this._mainCont.current, pos, this._initialScroll?.easeFunc ?? 'ease', this._scrollStopper)); setTimeout(() => (this._forcedScroll = false), duration); }, this._mainCont.current ? 0 : 250 @@ -224,7 +242,7 @@ export class PDFViewer extends React.Component<IViewerProps> { ); quickScroll = undefined; if (this._initialScroll !== undefined && this._mainCont.current) { - this._mainCont.current?.scrollTo({ top: Math.abs(this._initialScroll || 0) }); + this._mainCont.current?.scrollTo({ top: Math.abs(this._initialScroll?.loc || 0) }); this._initialScroll = undefined; } }; @@ -283,7 +301,7 @@ export class PDFViewer extends React.Component<IViewerProps> { @action scrollToAnnotation = (scrollToAnnotation: Doc) => { if (scrollToAnnotation) { - this.scrollFocus(scrollToAnnotation, true); + this.scrollFocus(scrollToAnnotation, NumCast(scrollToAnnotation.y), { zoomTime: 500 }); Doc.linkFollowHighlight(scrollToAnnotation); } }; @@ -358,7 +376,8 @@ export class PDFViewer extends React.Component<IViewerProps> { MarqueeAnnotator.clearAnnotations(this._savedAnnotations); this._marqueeing = [e.clientX, e.clientY]; this.isAnnotating = true; - if (e.target && ((e.target as any).className.includes('endOfContent') || (e.target as any).parentElement.className !== 'textLayer')) { + const target = e.target as any; + if (e.target && (target.className.includes('endOfContent') || (target.parentElement.className !== 'textLayer' && target.parentElement.parentElement?.className !== 'textLayer'))) { this._textSelecting = false; document.addEventListener('pointermove', this.onSelectMove); // need this to prevent document from being dragged if stopPropagation doesn't get called } else { @@ -377,7 +396,6 @@ export class PDFViewer extends React.Component<IViewerProps> { @action finishMarquee = (x?: number, y?: number) => { - this._getAnchor = AnchorMenu.Instance?.GetAnchor; this.isAnnotating = false; this._marqueeing = undefined; this._textSelecting = true; @@ -422,7 +440,8 @@ export class PDFViewer extends React.Component<IViewerProps> { } } } - this._selectionText = selRange.cloneContents().textContent || ''; + this._selectionContent = selRange.cloneContents(); + this._selectionText = this._selectionContent?.textContent || ''; // clear selection if (sel.empty) { @@ -439,6 +458,7 @@ export class PDFViewer extends React.Component<IViewerProps> { }; onClick = (e: React.MouseEvent) => { + this._scrollStopper?.(); if (this._setPreviewCursor && e.button === 0 && Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) { this._setPreviewCursor(e.clientX, e.clientY, false, false); } @@ -466,6 +486,7 @@ export class PDFViewer extends React.Component<IViewerProps> { <div className="pdfViewerDash-annotationLayer" style={{ height: Doc.NativeHeight(this.props.Document), transform: `scale(${NumCast(this.props.layoutDoc._viewScale, 1)})` }} ref={this._annotationLayer}> {this.inlineTextAnnotations .sort((a, b) => NumCast(a.y) - NumCast(b.y)) + .filter(anno => !anno.hidden) .map(anno => ( <Annotation {...this.props} fieldKey={this.props.fieldKey + '-annotations'} pointerEvents={this.pointerEvents} showInfo={this.showInfo} dataDoc={this.props.dataDoc} anno={anno} key={`${anno[Id]}-annotation`} /> ))} @@ -574,6 +595,7 @@ export class PDFViewer extends React.Component<IViewerProps> { docView={this.props.docViewPath().lastElement()} finishMarquee={this.finishMarquee} savedAnnotations={this.savedAnnotations} + selectionText={this.selectionText} annotationLayer={this._annotationLayer.current} mainCont={this._mainCont.current} anchorMenuCrop={this._textSelecting ? undefined : this.crop} diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index c5177de90..aac488559 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -4,10 +4,12 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import { DirectLinksSym, Doc, DocListCast, DocListCastAsync, Field } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; -import { StrCast } from '../../../fields/Types'; +import { DocCast, StrCast } from '../../../fields/Types'; +import { StopEvent } from '../../../Utils'; import { DocUtils } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; import { DocumentManager } from '../../util/DocumentManager'; +import { LinkManager } from '../../util/LinkManager'; import { CollectionDockingView } from '../collections/CollectionDockingView'; import { ViewBoxBaseComponent } from '../DocComponent'; import { FieldView, FieldViewProps } from '../nodes/FieldView'; @@ -20,6 +22,8 @@ const ERROR = 0.03; export interface SearchBoxProps extends FieldViewProps { linkSearch: boolean; linkFrom?: (() => Doc | undefined) | undefined; + linkCreateAnchor?: () => Doc | undefined; + linkCreated?: (link: Doc) => void; } /** @@ -111,10 +115,11 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { // TODO: nda -- Change this method to change what happens when you click on the item. makeLink = action((linkTo: Doc) => { - if (this.props.linkFrom) { - const linkFrom = this.props.linkFrom(); + if (this.props.linkCreateAnchor) { + const linkFrom = this.props.linkCreateAnchor(); if (linkFrom) { - DocUtils.MakeLink({ doc: linkFrom }, { doc: linkTo }); + const link = DocUtils.MakeLink({ doc: linkFrom }, { doc: linkTo }); + link && this.props.linkCreated?.(link); } } }); @@ -380,7 +385,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { const query = StrCast(this._searchString); Doc.SetSearchQuery(query); - Array.from(this._results.keys()).forEach(doc => DocumentManager.Instance.getFirstDocumentView(doc)?.ComponentView?.search?.(this._searchString, undefined, true)); + if (!this.props.linkSearch) Array.from(this._results.keys()).forEach(doc => DocumentManager.Instance.getFirstDocumentView(doc)?.ComponentView?.search?.(this._searchString, undefined, true)); this._results.clear(); if (query) { @@ -407,7 +412,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { * or opening it in a new tab. */ selectElement = async (doc: Doc, finishFunc: () => void) => { - await DocumentManager.Instance.jumpToDocument(doc, true, undefined, [], undefined, undefined, undefined, finishFunc); + await DocumentManager.Instance.jumpToDocument(doc, { willPanZoom: true }, undefined, [], finishFunc); }; /** @@ -437,6 +442,8 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { const resultsJSX = Array(); + const fromDoc = this.props.linkFrom?.(); + sortedResults.forEach(result => { var className = 'searchBox-results-scroll-view-result'; @@ -460,6 +467,13 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { e.stopPropagation(); } } + style={{ + fontWeight: DocListCast(fromDoc?.links).find( + link => Doc.AreProtosEqual(LinkManager.getOppositeAnchor(link, fromDoc!), result[0] as Doc) || Doc.AreProtosEqual(DocCast(LinkManager.getOppositeAnchor(link, fromDoc!)?.annotationOn), result[0] as Doc) + ) + ? 'bold' + : '', + }} className={className}> <div className="searchBox-result-title">{title as string}</div> <div className="searchBox-result-type">{formattedType}</div> @@ -482,7 +496,10 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { defaultValue={''} autoComplete="off" onChange={this.onInputChange} - onKeyPress={e => (e.key === 'Enter' ? this.submitSearch() : null)} + onKeyPress={e => { + e.key === 'Enter' ? this.submitSearch() : null; + e.stopPropagation(); + }} type="text" placeholder="Search..." id="search-input" diff --git a/src/debug/Repl.tsx b/src/debug/Repl.tsx index 1b12e208c..b8081648f 100644 --- a/src/debug/Repl.tsx +++ b/src/debug/Repl.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import * as ReactDOM from 'react-dom'; +import * as ReactDOM from 'react-dom/client'; import { observer } from 'mobx-react'; import { observable, computed } from 'mobx'; import { CompileScript } from '../client/util/Scripting'; @@ -11,39 +11,40 @@ import { resolvedPorts } from '../client/util/CurrentUserUtils'; @observer class Repl extends React.Component { - @observable text: string = ""; + @observable text: string = ''; - @observable executedCommands: { command: string, result: any }[] = []; + @observable executedCommands: { command: string; result: any }[] = []; onChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { this.text = e.target.value; - } + }; onKeyDown = (e: React.KeyboardEvent) => { - if (!e.ctrlKey && e.key === "Enter") { + if (!e.ctrlKey && e.key === 'Enter') { e.preventDefault(); const script = CompileScript(this.text, { - addReturn: true, typecheck: false, - params: { makeInterface: "any" } + addReturn: true, + typecheck: false, + params: { makeInterface: 'any' }, }); if (!script.compiled) { - this.executedCommands.push({ command: this.text, result: "Compile Error" }); + this.executedCommands.push({ command: this.text, result: 'Compile Error' }); } else { const result = script.run({ makeInterface }, e => this.executedCommands.push({ command: this.text, result: e.message || e })); result.success && this.executedCommands.push({ command: this.text, result: result.result }); } - this.text = ""; + this.text = ''; } - } + }; @computed get commands() { return this.executedCommands.map(command => { return ( - <div style={{ marginTop: "5px" }}> + <div style={{ marginTop: '5px' }}> <p>{command.command}</p> {/* <pre>{JSON.stringify(command.result, null, 2)}</pre> */} - <pre>{command.result instanceof RefField || command.result instanceof ObjectField ? "object" : String(command.result)}</pre> + <pre>{command.result instanceof RefField || command.result instanceof ObjectField ? 'object' : String(command.result)}</pre> </div> ); }); @@ -52,16 +53,14 @@ class Repl extends React.Component { render() { return ( <div> - <div style={{ verticalAlign: "bottom" }}> - {this.commands} - </div> - <textarea style={{ width: "100%", position: "absolute", bottom: "0px" }} value={this.text} onChange={this.onChange} onKeyDown={this.onKeyDown} /> + <div style={{ verticalAlign: 'bottom' }}>{this.commands}</div> + <textarea style={{ width: '100%', position: 'absolute', bottom: '0px' }} value={this.text} onChange={this.onChange} onKeyDown={this.onKeyDown} /> </div> ); } } (async function () { - DocServer.init(window.location.protocol, window.location.hostname, resolvedPorts.socket, "repl"); - ReactDOM.render(<Repl />, document.getElementById("root")); -})();
\ No newline at end of file + DocServer.init(window.location.protocol, window.location.hostname, resolvedPorts.socket, 'repl'); + ReactDOM.createRoot(document.getElementById('root')!).render(<Repl />); +})(); diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index fc43325fe..df49c32f0 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -1,6 +1,6 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { saveAs } from 'file-saver'; -import { action, computed, observable, ObservableMap, runInAction } from 'mobx'; +import { action, computed, observable, ObservableMap, ObservableSet, runInAction } from 'mobx'; import { computedFn } from 'mobx-utils'; import { alias, map, serializable } from 'serializr'; import { DocServer } from '../client/DocServer'; @@ -94,6 +94,8 @@ export function DocListCastOrNull(field: FieldResult) { export const WidthSym = Symbol('Width'); export const HeightSym = Symbol('Height'); +export const AnimationSym = Symbol('Animation'); +export const HighlightSym = Symbol('Highlight'); export const DataSym = Symbol('Data'); export const LayoutSym = Symbol('Layout'); export const FieldsSym = Symbol('Fields'); @@ -111,28 +113,37 @@ export const Initializing = Symbol('Initializing'); export const ForceServerWrite = Symbol('ForceServerWrite'); export const CachedUpdates = Symbol('Cached updates'); -const AclMap = new Map<string, symbol>([ - ['None', AclUnset], - [SharingPermissions.None, AclPrivate], - [SharingPermissions.View, AclReadonly], - [SharingPermissions.Augment, AclAugment], - [SharingPermissions.SelfEdit, AclSelfEdit], - [SharingPermissions.Edit, AclEdit], - [SharingPermissions.Admin, AclAdmin], +export enum aclLevel { + unset = -1, + unshared = 0, + viewable = 1, + augmentable = 2, + selfEditable = 2.5, + editable = 3, + admin = 4, +} +// prettier-ignore +export const HierarchyMapping: Map<symbol, { level:aclLevel; name: SharingPermissions }> = new Map([ + [AclPrivate, { level: aclLevel.unshared, name: SharingPermissions.None }], + [AclReadonly, { level: aclLevel.viewable, name: SharingPermissions.View }], + [AclAugment, { level: aclLevel.augmentable, name: SharingPermissions.Augment}], + [AclSelfEdit, { level: aclLevel.selfEditable, name: SharingPermissions.SelfEdit }], + [AclEdit, { level: aclLevel.editable, name: SharingPermissions.Edit }], + [AclAdmin, { level: aclLevel.admin, name: SharingPermissions.Admin }], + [AclUnset, { level: aclLevel.unset, name: SharingPermissions.Unset }], ]); +export const ReverseHierarchyMap: Map<string, { level: aclLevel; acl: symbol }> = new Map(Array.from(HierarchyMapping.entries()).map(value => [value[1].name, { level: value[1].level, acl: value[0] }])); // caches the document access permissions for the current user. // this recursively updates all protos as well. export function updateCachedAcls(doc: Doc) { if (!doc) return; - const permissions: { [key: string]: symbol } = {}; - - doc[UpdatingFromServer] = true; - Object.keys(doc).filter(key => key.startsWith('acl') && (permissions[key] = AclMap.get(StrCast(doc[key]))!)); - doc[UpdatingFromServer] = false; - if (Object.keys(permissions).length) { - doc[AclSym] = permissions; + const target = (doc as any)?.__fields ?? doc; + const permissions: { [key: string]: symbol } = !target.author || target.author === Doc.CurrentUserEmail ? { 'acl-Me': AclAdmin } : {}; + Object.keys(target).filter(key => key.startsWith('acl') && (permissions[key] = ReverseHierarchyMap.get(StrCast(target[key]))!.acl)); + if (Object.keys(permissions).length || doc[AclSym]?.length) { + runInAction(() => (doc[AclSym] = permissions)); } if (doc.proto instanceof Promise) { @@ -228,6 +239,19 @@ export class Doc extends RefField { public static get MyFileOrphans() { return DocCast(Doc.UserDoc().myFileOrphans); } + public static AddFileOrphan(doc: Doc) { + if ( + doc && + Doc.MyFileOrphans instanceof Doc && + Doc.IsPrototype(doc) && + !Doc.IsSystem(doc) && + ![DocumentType.MARKER, DocumentType.KVP, DocumentType.LINK, DocumentType.LINKANCHOR].includes(doc.type as any) && + !doc.isFolder && + !doc.annotationOn + ) { + Doc.AddDocToList(Doc.MyFileOrphans, undefined, doc); + } + } public static get MyTools() { return DocCast(Doc.UserDoc().myTools); } @@ -265,7 +289,7 @@ export class Doc extends RefField { } constructor(id?: FieldId, forceSave?: boolean) { super(id); - const doc = new Proxy<this>(this, { + const docProxy = new Proxy<this>(this, { set: setter, get: getter, // getPrototypeOf: (target) => Cast(target[SelfProxy].proto, Doc) || null, // TODO this might be able to replace the proto logic in getter @@ -294,11 +318,11 @@ export class Doc extends RefField { throw new Error("Currently properties can't be defined on documents using Object.defineProperty"); }, }); - this[SelfProxy] = doc; + this[SelfProxy] = docProxy; if (!id || forceSave) { - DocServer.CreateField(doc); + DocServer.CreateField(docProxy); } - return doc; + return docProxy; } proto: Opt<Doc>; @@ -327,8 +351,15 @@ export class Doc extends RefField { @observable private ___fields: any = {}; @observable private ___fieldKeys: any = {}; + /// all of the raw acl's that have been set on this document. Use GetEffectiveAcl to determine the actual ACL of the doc for editing @observable public [AclSym]: { [key: string]: symbol } = {}; @observable public [DirectLinksSym]: Set<Doc> = new Set(); + @observable public [AnimationSym]: Opt<Doc>; + @observable public [HighlightSym]: boolean = false; + static __Anim(Doc: Doc) { + // for debugging to print AnimationSym field easily. + return Doc[AnimationSym]; + } private [UpdatingFromServer]: boolean = false; private [ForceServerWrite]: boolean = false; @@ -756,12 +787,13 @@ export namespace Doc { } cloneMap.set(doc[Id], copy); } + Doc.AddFileOrphan(copy); return copy; } export async function MakeClone(doc: Doc, dontCreate: boolean = false, asBranch = false, cloneMap: Map<string, Doc> = new Map()) { const linkMap = new Map<Doc, Doc>(); const rtfMap: { copy: Doc; key: string; field: RichTextField }[] = []; - const copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ['cloneOf', 'branches', 'branchOf'], dontCreate, asBranch); + const copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ['cloneOf', 'branches', 'branchOf', 'context'], dontCreate, asBranch); Array.from(linkMap.entries()).map((links: Doc[]) => LinkManager.Instance.addLink(links[1], true)); rtfMap.map(({ copy, key, field }) => { const replacer = (match: any, attr: string, id: string, offset: any, string: any) => { @@ -934,11 +966,12 @@ export namespace Doc { export function MakeCopy(doc: Doc, copyProto: boolean = false, copyProtoId?: string, retitle = false): Doc { const copy = new Doc(copyProtoId, true); + updateCachedAcls(copy); const exclude = Cast(doc.cloneFieldFilter, listSpec('string'), []); Object.keys(doc).forEach(key => { if (exclude.includes(key)) return; const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); - const field = ProxyField.WithoutProxy(() => doc[key]); + const field = key === 'author' ? Doc.CurrentUserEmail : ProxyField.WithoutProxy(() => doc[key]); if (key === 'proto' && copyProto) { if (doc[key] instanceof Doc) { copy[key] = Doc.MakeCopy(doc[key]!, false); @@ -962,7 +995,6 @@ export namespace Doc { } } }); - copy.author = Doc.CurrentUserEmail; if (copyProto) { Doc.GetProto(copy).context = undefined; Doc.GetProto(copy).aliases = new List<Doc>([copy]); @@ -974,6 +1006,7 @@ export namespace Doc { if (retitle) { copy.title = incrementTitleCopy(StrCast(copy.title)); } + Doc.AddFileOrphan(copy); return copy; } @@ -983,6 +1016,7 @@ export namespace Doc { if (doc) { const delegate = new Doc(id, true); delegate[Initializing] = true; + updateCachedAcls(delegate); delegate.proto = doc; delegate.author = Doc.CurrentUserEmail; Object.keys(doc) @@ -991,6 +1025,7 @@ export namespace Doc { if (!Doc.IsSystem(doc)) Doc.AddDocToList(doc[DataSym], 'aliases', delegate); title && (delegate.title = title); delegate[Initializing] = false; + Doc.AddFileOrphan(delegate); return delegate; } return undefined; @@ -1113,7 +1148,7 @@ export namespace Doc { BrushedDoc: ObservableMap<Doc, boolean> = new ObservableMap(); SearchMatchDoc: ObservableMap<Doc, { searchMatch: number }> = new ObservableMap(); } - const brushManager = new DocBrush(); + export const brushManager = new DocBrush(); export class DocData { @observable _user_doc: Doc = undefined!; @@ -1259,48 +1294,55 @@ export namespace Doc { } export function linkFollowUnhighlight() { - Doc.UnhighlightAll(); + UnhighlightWatchers.forEach(watcher => watcher()); + UnhighlightWatchers.length = 0; + highlightedDocs.forEach(doc => Doc.UnHighlightDoc(doc)); document.removeEventListener('pointerdown', linkFollowUnhighlight); } - let _lastDate = 0; - export function linkFollowHighlight(destDoc: Doc | Doc[], dataAndDisplayDocs = true) { + let UnhighlightWatchers: (() => void)[] = []; + export let UnhighlightTimer: any; + export function AddUnHighlightWatcher(watcher: () => void) { + if (UnhighlightTimer) { + UnhighlightWatchers.push(watcher); + } else watcher(); + } + export function linkFollowHighlight(destDoc: Doc | Doc[], dataAndDisplayDocs = true, presEffect?: Doc) { linkFollowUnhighlight(); - (destDoc instanceof Doc ? [destDoc] : destDoc).forEach(doc => Doc.HighlightDoc(doc, dataAndDisplayDocs)); + (destDoc instanceof Doc ? [destDoc] : destDoc).forEach(doc => Doc.HighlightDoc(doc, dataAndDisplayDocs, presEffect)); document.removeEventListener('pointerdown', linkFollowUnhighlight); document.addEventListener('pointerdown', linkFollowUnhighlight); - const lastDate = (_lastDate = Date.now()); - window.setTimeout(() => _lastDate === lastDate && linkFollowUnhighlight(), 5000); + if (UnhighlightTimer) clearTimeout(UnhighlightTimer); + UnhighlightTimer = window.setTimeout(() => { + linkFollowUnhighlight(); + UnhighlightTimer = 0; + }, 5000); } - export class HighlightBrush { - @observable HighlightedDoc: Map<Doc, boolean> = new Map(); - } - const highlightManager = new HighlightBrush(); + export var highlightedDocs = new ObservableSet<Doc>(); export function IsHighlighted(doc: Doc) { if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate || doc.opacity === 0) return false; - return highlightManager.HighlightedDoc.get(doc) || highlightManager.HighlightedDoc.get(Doc.GetProto(doc)); + return doc[HighlightSym] || Doc.GetProto(doc)[HighlightSym]; } - export function HighlightDoc(doc: Doc, dataAndDisplayDocs = true) { + export function HighlightDoc(doc: Doc, dataAndDisplayDocs = true, presEffect?: Doc) { runInAction(() => { - highlightManager.HighlightedDoc.set(doc, true); - dataAndDisplayDocs && highlightManager.HighlightedDoc.set(Doc.GetProto(doc), true); + highlightedDocs.add(doc); + doc[HighlightSym] = true; + doc[AnimationSym] = presEffect; + if (dataAndDisplayDocs) { + highlightedDocs.add(Doc.GetProto(doc)); + Doc.GetProto(doc)[HighlightSym] = true; + } }); } export function UnHighlightDoc(doc: Doc) { runInAction(() => { - highlightManager.HighlightedDoc.set(doc, false); - highlightManager.HighlightedDoc.set(Doc.GetProto(doc), false); + highlightedDocs.delete(doc); + highlightedDocs.delete(Doc.GetProto(doc)); + doc[HighlightSym] = Doc.GetProto(doc)[HighlightSym] = false; + doc[AnimationSym] = undefined; }); } - export function UnhighlightAll() { - const mapEntries = highlightManager.HighlightedDoc.keys(); - let docEntry: IteratorResult<Doc>; - while (!(docEntry = mapEntries.next()).done) { - const targetDoc = docEntry.value; - targetDoc && Doc.UnHighlightDoc(targetDoc); - } - } export function UnBrushAllDocs() { brushManager.BrushedDoc.clear(); } diff --git a/src/fields/FieldLoader.scss b/src/fields/FieldLoader.scss new file mode 100644 index 000000000..9a23c3e4f --- /dev/null +++ b/src/fields/FieldLoader.scss @@ -0,0 +1,15 @@ +.fieldLoader { + z-index: 10000; + width: 200px; + height: 30; + background: lightblue; + position: absolute; + left: calc(50% - 99px); + top: calc(50% + 110px); + display: flex; + align-items: center; + padding: 20px; + margin: auto; + display: 'block'; + box-shadow: darkslategrey 0.2vw 0.2vw 0.8vw; +} diff --git a/src/fields/FieldLoader.tsx b/src/fields/FieldLoader.tsx new file mode 100644 index 000000000..2a7b936f7 --- /dev/null +++ b/src/fields/FieldLoader.tsx @@ -0,0 +1,15 @@ +import { observable } from 'mobx'; +import { observer } from 'mobx-react'; + +import * as React from 'react'; +import './FieldLoader.scss'; + +@observer +export class FieldLoader extends React.Component { + @observable public static ServerLoadStatus = { requested: 0, retrieved: 0 }; + public static active = false; + + render() { + return <div className="fieldLoader">{`Requested: ${FieldLoader.ServerLoadStatus.requested} ... ${FieldLoader.ServerLoadStatus.retrieved} `}</div>; + } +} diff --git a/src/fields/FieldSymbols.ts b/src/fields/FieldSymbols.ts index 8d040f493..e50c2856f 100644 --- a/src/fields/FieldSymbols.ts +++ b/src/fields/FieldSymbols.ts @@ -1,12 +1,12 @@ - -export const Update = Symbol("Update"); -export const Self = Symbol("Self"); -export const SelfProxy = Symbol("SelfProxy"); -export const HandleUpdate = Symbol("HandleUpdate"); -export const Id = Symbol("Id"); -export const OnUpdate = Symbol("OnUpdate"); -export const Parent = Symbol("Parent"); -export const Copy = Symbol("Copy"); -export const ToScriptString = Symbol("ToScriptString"); -export const ToPlainText = Symbol("ToPlainText"); -export const ToString = Symbol("ToString"); +export const Update = Symbol('Update'); +export const Self = Symbol('Self'); +export const SelfProxy = Symbol('SelfProxy'); +export const HandleUpdate = Symbol('HandleUpdate'); +export const Id = Symbol('Id'); +export const OnUpdate = Symbol('OnUpdate'); +export const Parent = Symbol('Parent'); +export const Copy = Symbol('Copy'); +export const ToValue = Symbol('ToValue'); +export const ToScriptString = Symbol('ToScriptString'); +export const ToPlainText = Symbol('ToPlainText'); +export const ToString = Symbol('ToString'); diff --git a/src/fields/List.ts b/src/fields/List.ts index 5cc4ca543..9c7794813 100644 --- a/src/fields/List.ts +++ b/src/fields/List.ts @@ -127,6 +127,9 @@ const listHandlers: any = { this[Self].__realFields(); return this[Self].__fields.map(toRealField).join(separator); }, + lastElement() { + return this[Self].__realFields().lastElement(); + }, lastIndexOf(valueToFind: any, fromIndex: number) { if (valueToFind instanceof RefField) { return this[Self].__realFields().lastIndexOf(valueToFind, fromIndex); @@ -210,10 +213,10 @@ function toObjectField(field: Field) { } function toRealField(field: Field) { - return field instanceof ProxyField ? field.value() : field; + return field instanceof ProxyField ? field.value : field; } -function listGetter(target: any, prop: string | number | symbol, receiver: any): any { +function listGetter(target: any, prop: string | symbol, receiver: any): any { if (listHandlers.hasOwnProperty(prop)) { return listHandlers[prop]; } @@ -271,19 +274,17 @@ class ListImpl<T extends Field> extends ObjectField { // this requests all ProxyFields at the same time to avoid the overhead // of separate network requests and separate updates to the React dom. private __realFields() { - const promised = this.__fields.filter(f => f instanceof ProxyField && f.promisedValue()).map(f => ({ field: f as any, promisedFieldId: f instanceof ProxyField ? f.promisedValue() : '' })); + const unrequested = this.__fields.filter(f => f instanceof ProxyField && f.needsRequesting).map(f => f as ProxyField<RefField>); // if we find any ProxyFields that don't have a current value, then // start the server request for all of them - if (promised.length) { - const batchPromise = DocServer.GetRefFields(promised.map(p => p.promisedFieldId)); + if (unrequested.length) { + const batchPromise = DocServer.GetRefFields(unrequested.map(p => p.fieldId)); // as soon as we get the fields from the server, set all the list values in one // action to generate one React dom update. - batchPromise.then(pfields => promised.forEach(p => p.field.setValue(pfields[p.promisedFieldId]))); + const allSetPromise = batchPromise.then(action(pfields => unrequested.map(toReq => toReq.setValue(pfields[toReq.fieldId])))); // we also have to mark all lists items with this promise so that any calls to them // will await the batch request and return the requested field value. - // This assumes the handler for 'promise' in the call above being invoked before the - // handler for 'promise' in the lines below. - promised.forEach(p => p.field.setPromise(batchPromise.then(pfields => pfields[p.promisedFieldId]))); + unrequested.forEach(p => p.setExternalValuePromise(allSetPromise)); } return this.__fields.map(toRealField); } diff --git a/src/fields/Proxy.ts b/src/fields/Proxy.ts index 2c5f38818..55d1d9ea4 100644 --- a/src/fields/Proxy.ts +++ b/src/fields/Proxy.ts @@ -1,95 +1,90 @@ -import { Deserializable } from "../client/util/SerializationHelper"; -import { FieldWaiting } from "./Doc"; -import { primitive, serializable } from "serializr"; -import { observable, action, runInAction } from "mobx"; -import { DocServer } from "../client/DocServer"; -import { RefField } from "./RefField"; -import { ObjectField } from "./ObjectField"; -import { Id, Copy, ToScriptString, ToString } from "./FieldSymbols"; -import { scriptingGlobal } from "../client/util/ScriptingGlobals"; -import { Plugins } from "./util"; +import { Deserializable } from '../client/util/SerializationHelper'; +import { FieldWaiting, Opt } from './Doc'; +import { primitive, serializable } from 'serializr'; +import { observable, action, runInAction, computed } from 'mobx'; +import { DocServer } from '../client/DocServer'; +import { RefField } from './RefField'; +import { ObjectField } from './ObjectField'; +import { Id, Copy, ToScriptString, ToString, ToValue } from './FieldSymbols'; +import { scriptingGlobal } from '../client/util/ScriptingGlobals'; function deserializeProxy(field: any) { - if (!field.cache) { - field.cache = DocServer.GetCachedRefField(field.fieldId) as any; + if (!field.cache.field) { + field.cache = { field: DocServer.GetCachedRefField(field.fieldId) as any, p: undefined }; } } -@Deserializable("proxy", deserializeProxy) +@Deserializable('proxy', deserializeProxy) export class ProxyField<T extends RefField> extends ObjectField { constructor(); constructor(value: T); constructor(fieldId: string); constructor(value?: T | string) { super(); - if (typeof value === "string") { - this.cache = DocServer.GetCachedRefField(value) as any; + if (typeof value === 'string') { + //this.cache = DocServer.GetCachedRefField(value) as any; this.fieldId = value; } else if (value) { - this.cache = value; + this.cache = { field: value, p: undefined }; this.fieldId = value[Id]; } } + [ToValue](doc: any) { + return ProxyField.toValue(this); + } + [Copy]() { - if (this.cache) return new ProxyField<T>(this.cache); + if (this.cache.field) return new ProxyField<T>(this.cache.field); return new ProxyField<T>(this.fieldId); } [ToScriptString]() { - return "invalid"; + return 'invalid'; } [ToString]() { - return "ProxyField"; + return 'ProxyField'; } @serializable(primitive()) - readonly fieldId: string = ""; + readonly fieldId: string = ''; - // This getter/setter and nested object thing is + // This getter/setter and nested object thing is // because mobx doesn't play well with observable proxies @observable.ref - private _cache: { readonly field: T | undefined } = { field: undefined }; - private get cache(): T | undefined { - return this._cache.field; + private _cache: { readonly field: T | undefined; p: FieldWaiting<T> | undefined } = { field: undefined, p: undefined }; + private get cache(): { field: T | undefined; p: FieldWaiting<T> | undefined } { + return this._cache; } - private set cache(field: T | undefined) { - this._cache = { field }; + private set cache(val: { field: T | undefined; p: FieldWaiting<T> | undefined }) { + runInAction(() => (this._cache = { ...val })); } private failed = false; - private promise?: Promise<any>; - value(): T | undefined | FieldWaiting<T> { - if (this.cache) { - return this.cache; - } - if (this.failed) { - return undefined; - } - if (!this.promise) { - const cached = DocServer.GetCachedRefField(this.fieldId); - if (cached !== undefined) { - runInAction(() => this.cache = cached as any); - return cached as any; - } - this.promise = DocServer.GetRefField(this.fieldId).then(action((field: any) => { - this.promise = undefined; - this.cache = field; - if (field === undefined) this.failed = true; - return field; - })); + @computed get value(): T | undefined | FieldWaiting<T> { + if (this.cache.field) return this.cache.field; + if (this.failed) return undefined; + + this.cache.field = DocServer.GetCachedRefField(this.fieldId) as T; + if (!this.cache.field && !this.cache.p) { + this.cache = { + field: undefined, + p: DocServer.GetRefField(this.fieldId).then(val => this.setValue(val as T)) as FieldWaiting<T>, + }; } - return DocServer.GetCachedRefField(this.fieldId) ?? (this.promise as any); + return this.cache.field ?? this.cache.p; } - promisedValue(): string { return !this.cache && !this.failed && !this.promise ? this.fieldId : ""; } - setPromise(promise: any) { - this.promise = promise; + @computed get needsRequesting(): boolean { + return !this.cache.field && !this.failed && !this._cache.p && !DocServer.GetCachedRefField(this.fieldId) ? true : false; + } + + setExternalValuePromise(externalValuePromise: Promise<any>) { + this.cache.p = externalValuePromise.then(() => this.value) as FieldWaiting<T>; } @action - setValue(field: any) { - this.promise = undefined; - this.cache = field; - if (field === undefined) this.failed = true; + setValue(field: Opt<T>) { + this.cache = { field, p: undefined }; + this.failed = field === undefined; return field; } } @@ -113,20 +108,17 @@ export namespace ProxyField { } } - export function initPlugin() { - Plugins.addGetterPlugin((doc, _, value) => { - if (useProxy && value instanceof ProxyField) { - return { value: value.value() }; - } - }); + export function toValue(value: any) { + if (useProxy) { + return { value: value.value }; + } } } function prefetchValue(proxy: PrefetchProxy<RefField>) { - return proxy.value() as any; + return proxy.value as any; } @scriptingGlobal -@Deserializable("prefetch_proxy", prefetchValue) -export class PrefetchProxy<T extends RefField> extends ProxyField<T> { -} +@Deserializable('prefetch_proxy', prefetchValue) +export class PrefetchProxy<T extends RefField> extends ProxyField<T> {} diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts index 4896c027d..b23732b45 100644 --- a/src/fields/ScriptField.ts +++ b/src/fields/ScriptField.ts @@ -6,11 +6,10 @@ import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGloba import { autoObject, Deserializable } from '../client/util/SerializationHelper'; import { numberRange } from '../Utils'; import { Doc, Field, Opt } from './Doc'; -import { Copy, Id, ToScriptString, ToString } from './FieldSymbols'; +import { Copy, Id, ToScriptString, ToString, ToValue } from './FieldSymbols'; import { List } from './List'; import { ObjectField } from './ObjectField'; import { Cast, StrCast } from './Types'; -import { Plugins } from './util'; function optional(propSchema: PropSchema) { return custom( @@ -175,6 +174,9 @@ export class ComputedField extends ScriptField { value = computedFn((doc: Doc) => this._valueOutsideReaction(doc)); _valueOutsideReaction = (doc: Doc) => (this._lastComputedResult = this.script.run({ this: doc, self: Cast(doc.rootDocument, Doc, null) || doc, _last_: this._lastComputedResult, _readOnly_: true }, console.log).result); + [ToValue](doc: Doc) { + return ComputedField.toValue(doc, this); + } [Copy](): ObjectField { return new ComputedField(this.script, this.setterscript, this.rawscript); } @@ -239,12 +241,10 @@ export namespace ComputedField { } } - export function initPlugin() { - Plugins.addGetterPlugin((doc, _, value) => { - if (useComputed && value instanceof ComputedField) { - return { value: value._valueOutsideReaction(doc), shouldReturn: true }; - } - }); + export function toValue(doc: any, value: any) { + if (useComputed) { + return { value: value._valueOutsideReaction(doc) }; + } } } diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts index 24b5a359d..10324449f 100644 --- a/src/fields/documentSchemas.ts +++ b/src/fields/documentSchemas.ts @@ -93,7 +93,7 @@ export const documentSchema = createSchema({ layers: listSpec('string'), // which layers the document is part of _lockedPosition: 'boolean', // whether the document can be moved (dragged) _lockedTransform: 'boolean', // whether a freeformview can pan/zoom - displayArrow: 'boolean', // toggles directed arrows + linkDisplayArrow: 'boolean', // toggles directed arrows // drag drop properties _stayInCollection: 'boolean', // whether document can be dropped into a different collection diff --git a/src/fields/util.ts b/src/fields/util.ts index 4a62a6a1f..dc0b41276 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -1,21 +1,18 @@ -import { action, observable, runInAction, trace } from 'mobx'; +import { $mobx, action, observable, runInAction, trace } from 'mobx'; import { computedFn } from 'mobx-utils'; import { DocServer } from '../client/DocServer'; import { CollectionViewType } from '../client/documents/DocumentTypes'; import { SerializationHelper } from '../client/util/SerializationHelper'; import { UndoManager } from '../client/util/UndoManager'; -import { CollectionDockingView } from '../client/views/collections/CollectionDockingView'; import { returnZero } from '../Utils'; import CursorField from './CursorField'; import { AclAdmin, - AclAugment, AclEdit, + aclLevel, AclPrivate, - AclReadonly, AclSelfEdit, AclSym, - AclUnset, DataSym, Doc, DocListCast, @@ -23,13 +20,15 @@ import { FieldResult, ForceServerWrite, HeightSym, + HierarchyMapping, Initializing, LayoutSym, + ReverseHierarchyMap, updateCachedAcls, UpdatingFromServer, WidthSym, } from './Doc'; -import { Id, OnUpdate, Parent, Self, SelfProxy, Update } from './FieldSymbols'; +import { Id, OnUpdate, Parent, SelfProxy, ToValue, Update } from './FieldSymbols'; import { List } from './List'; import { ObjectField } from './ObjectField'; import { PrefetchProxy, ProxyField } from './Proxy'; @@ -48,19 +47,6 @@ export function TraceMobx() { tracing && trace(); } -export interface GetterResult { - value: FieldResult; - shouldReturn?: boolean; -} -export type GetterPlugin = (receiver: any, prop: string | number, currentValue: any) => GetterResult | undefined; -const getterPlugins: GetterPlugin[] = []; - -export namespace Plugins { - export function addGetterPlugin(plugin: GetterPlugin) { - getterPlugins.push(plugin); - } -} - const _setterImpl = action(function (target: any, prop: string | symbol | number, value: any, receiver: any): boolean { if (SerializationHelper.IsSerializing()) { target[prop] = value; @@ -119,6 +105,7 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number if (writeToServer) { if (value === undefined) target[Update]({ $unset: { ['fields.' + prop]: '' } }); else target[Update]({ $set: { ['fields.' + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) : value === undefined ? null : value } }); + if (prop === 'author' || prop.toString().startsWith('acl')) updateCachedAcls(target); } else { DocServer.registerDocWithCachedUpdate(receiver, prop as string, curValue); } @@ -126,7 +113,12 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number (!receiver[UpdatingFromServer] || receiver[ForceServerWrite]) && UndoManager.AddEvent({ redo: () => (receiver[prop] = value), - undo: () => (receiver[prop] = curValue), + undo: () => { + const wasUpdate = receiver[UpdatingFromServer]; + receiver[UpdatingFromServer] = true; // needed if the event caused ACL's to change such that the doc is otherwise no longer editable. + receiver[prop] = curValue; + receiver[UpdatingFromServer] = wasUpdate; + }, prop: prop?.toString(), }); return true; @@ -183,8 +175,11 @@ export function inheritParentAcls(parent: Doc, child: Doc) { * View: a user with view access to a document can only view it - they cannot add/remove/edit anything. * * None: the document is not shared with that user. + * + * Unset: Remove a sharing permission (eg., used ) */ export enum SharingPermissions { + Unset = 'None', Admin = 'Admin', Edit = 'Edit', SelfEdit = 'Self Edit', @@ -204,22 +199,16 @@ const getEffectiveAclCache = computedFn(function (target: any, user?: string) { export function GetEffectiveAcl(target: any, user?: string): symbol { if (!target) return AclPrivate; if (target[UpdatingFromServer]) return AclAdmin; - // authored documents are private until an ACL is set. - if (!target[AclSym] && target.author && target.author !== Doc.CurrentUserEmail) return AclPrivate; return getEffectiveAclCache(target, user); // all changes received from the server must be processed as Admin. return this directly so that the acls aren't cached (UpdatingFromServer is not observable) } function getPropAcl(target: any, prop: string | symbol | number) { - if (prop === UpdatingFromServer || prop === Initializing || target[UpdatingFromServer] || prop === AclSym) return AclAdmin; // requesting the UpdatingFromServer prop or AclSym must always go through to keep the local DB consistent + if (typeof prop === 'symbol' || target[UpdatingFromServer]) return AclAdmin; // requesting the UpdatingFromServer prop or AclSym must always go through to keep the local DB consistent if (prop && DocServer.IsPlaygroundField(prop.toString())) return AclEdit; // playground props are always editable return GetEffectiveAcl(target); } -let HierarchyMapping: Map<symbol, number> | undefined; - let cachedGroups = observable([] as string[]); -/// bcz; argh!! TODO; These do not belong here, but there were include order problems with leaving them in util.ts -// need to investigate further what caused the mobx update problems and move to a better location. const getCachedGroupByNameCache = computedFn(function (name: string) { return cachedGroups.includes(name); }, true); @@ -231,42 +220,32 @@ export function SetCachedGroups(groups: string[]) { } function getEffectiveAcl(target: any, user?: string): symbol { const targetAcls = target[AclSym]; - 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 - const targetAuthor = target.__fields?.author || target.author; // target may be a Doc of Proxy, so check __fields.author and .author - if (userChecked === targetAuthor || !targetAuthor) return AclAdmin; - if (GetCachedGroupByName('Admin')) return AclAdmin; + if (targetAcls?.['acl-Me'] === AclAdmin || GetCachedGroupByName('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 (targetAcls && Object.keys(targetAcls).length) { - HierarchyMapping = - HierarchyMapping || - new Map<symbol, number>([ - [AclPrivate, 0], - [AclReadonly, 1], - [AclAugment, 2], - [AclSelfEdit, 2.5], - [AclEdit, 3], - [AclAdmin, 4], - ]); - let effectiveAcl = AclPrivate; for (const [key, value] of Object.entries(targetAcls)) { // 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. const entity = denormalizeEmail(key.substring(4)); // an individual or a group - if (HierarchyMapping.get(value as symbol)! > HierarchyMapping.get(effectiveAcl)!) { - if (GetCachedGroupByName(entity) || userChecked === entity) { + if (HierarchyMapping.get(value as symbol)!.level > HierarchyMapping.get(effectiveAcl)!.level) { + if (GetCachedGroupByName(entity) || userChecked === entity || entity === 'Me') { effectiveAcl = value as symbol; } } } // 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 = targetAcls['acl-Override']; - if (override !== AclUnset && override !== undefined) effectiveAcl = override; + //const override = targetAcls['acl-Override']; + // if (override !== AclUnset && override !== undefined) effectiveAcl = 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; + return DocServer?.Control?.isReadOnly?.() && HierarchyMapping.get(effectiveAcl)!.level < aclLevel.editable ? AclEdit : effectiveAcl; } + // authored documents are private until an ACL is set. + const targetAuthor = target.__fields?.author || target.author; // target may be a Doc of Proxy, so check __fields.author and .author + if (targetAuthor && targetAuthor !== userChecked) return AclPrivate; return AclAdmin; } /** @@ -291,21 +270,9 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc } visited.push(target); - const HierarchyMapping = new Map<string, number>([ - ['Not Shared', 0], - ['Can View', 1], - ['Can Augment', 2], - ['Self Edit', 2.5], - ['Can Edit', 3], - ['Admin', 4], - ]); - let layoutDocChanged = false; // determines whether fetchProto should be called or not (i.e. is there a change that should be reflected in target[AclSym]) - let dataDocChanged = false; - 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 (GetEffectiveAcl(target) === AclAdmin && (!inheritingFromCollection || !target[key] || HierarchyMapping.get(StrCast(target[key]))! > HierarchyMapping.get(acl)!)) { + if (GetEffectiveAcl(target) === AclAdmin && (!inheritingFromCollection || !target[key] || ReverseHierarchyMap.get(StrCast(target[key]))!.level > ReverseHierarchyMap.get(acl)!.level)) { target[key] = acl; layoutDocChanged = true; @@ -316,15 +283,16 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc } } - if (dataDoc && (!inheritingFromCollection || !dataDoc[key] || HierarchyMapping.get(StrCast(dataDoc[key]))! > HierarchyMapping.get(acl)!)) { + let dataDocChanged = false; + const dataDoc = target[DataSym]; + if (dataDoc && (!inheritingFromCollection || !dataDoc[key] || ReverseHierarchyMap.get(StrCast(dataDoc[key]))! > ReverseHierarchyMap.get(acl)!)) { if (GetEffectiveAcl(dataDoc) === AclAdmin) { dataDoc[key] = acl; dataDocChanged = true; } // maps over the links of the document - const links = DocListCast(dataDoc.links); - links.forEach(link => distributeAcls(key, acl, link, inheritingFromCollection, visited)); + DocListCast(dataDoc.links).forEach(link => distributeAcls(key, acl, link, inheritingFromCollection, visited)); // maps over the children of the document DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + (isDashboard ? '-all' : '')]).map(d => { @@ -353,10 +321,10 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc export function setter(target: any, in_prop: string | symbol | number, value: any, receiver: any): boolean { let prop = in_prop; - const effectiveAcl = getPropAcl(target, prop); + const effectiveAcl = in_prop === 'constructor' || typeof in_prop === 'symbol' ? AclAdmin : getPropAcl(target, prop); if (effectiveAcl !== AclEdit && effectiveAcl !== AclAdmin && !(effectiveAcl === AclSelfEdit && value instanceof RichTextField)) 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, 'None'].includes(value))) return true; + 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 Augment", "Can View", "Not Shared", undefined].includes(value)) return true; if (typeof prop === 'string' && prop !== '__id' && prop !== '__fields' && prop.startsWith('_')) { @@ -372,53 +340,44 @@ export function setter(target: any, in_prop: string | symbol | number, value: an return _setter(target, prop, value, receiver); } -export function getter(target: any, in_prop: string | symbol | number, receiver: any): any { - let prop = in_prop; - - if (in_prop === AclSym) return target[AclSym]; - if (in_prop === 'toString' || (in_prop !== HeightSym && in_prop !== WidthSym && in_prop !== LayoutSym && typeof prop === 'symbol')) return target.__fields[prop] || target[prop]; - if (GetEffectiveAcl(target) === AclPrivate) return prop === HeightSym || prop === WidthSym ? returnZero : undefined; - if (prop === LayoutSym) return target.__LAYOUT__; - if (typeof prop === 'string' && prop !== '__id' && prop !== '__fields' && prop.startsWith('_')) { - if (!prop.startsWith('__')) prop = prop.substring(1); - if (target.__LAYOUT__) return target.__LAYOUT__[prop]; - } - if (prop === 'then') { - //If we're being awaited - return undefined; +export function getter(target: any, prop: string | symbol, proxy: any): any { + // prettier-ignore + switch (prop) { + case 'then' : return undefined; + case '__fields' : case '__id': + case 'constructor': case 'toString': case 'valueOf': + case 'factory': case 'serializeInfo': + return target[prop]; + case AclSym : return target[AclSym]; + case $mobx: return target.__fields[prop]; + case LayoutSym: return target.__Layout__; + case HeightSym: case WidthSym: if (GetEffectiveAcl(target) === AclPrivate) return returnZero; + default : + if (typeof prop === 'symbol') return target[prop]; + if (prop.startsWith('isMobX')) return target[prop]; + if (prop.startsWith('__')) return target[prop]; + if (GetEffectiveAcl(target) === AclPrivate && prop !== 'author') return undefined; } - if (typeof prop === 'symbol') { - return target.__fields[prop] || target[prop]; - } - if (SerializationHelper.IsSerializing()) { - return target[prop]; - } - return getFieldImpl(target, prop, receiver); + + const layout_prop = prop.startsWith('_') ? prop.substring(1) : undefined; + if (layout_prop && target.__LAYOUT__) return target.__LAYOUT__[layout_prop]; + return getFieldImpl(target, layout_prop ?? prop, proxy); } -function getFieldImpl(target: any, prop: string | number, receiver: any, ignoreProto: boolean = false): any { - receiver = receiver || target[SelfProxy]; - let field = target.__fields[prop]; - for (const plugin of getterPlugins) { - const res = plugin(receiver, prop, field); - if (res === undefined) continue; - if (res.shouldReturn) { - return res.value; - } else { - field = res.value; - } - } +function getFieldImpl(target: any, prop: string | number, proxy: any, ignoreProto: boolean = false): any { + const field = target.__fields[prop]; + const value = field?.[ToValue]?.(proxy); // converts ComputedFields to values, or unpacks ProxyFields into Proxys + if (value) return value.value; if (field === undefined && !ignoreProto && prop !== 'proto') { - const proto = getFieldImpl(target, 'proto', receiver, true); //TODO tfs: instead of receiver we could use target[SelfProxy]... I don't which semantics we want or if it really matters + const proto = getFieldImpl(target, 'proto', proxy, true); //TODO tfs: instead of proxy we could use target[SelfProxy]... I don't which semantics we want or if it really matters if (proto instanceof Doc && GetEffectiveAcl(proto) !== AclPrivate) { - return getFieldImpl(proto[Self], prop, receiver, ignoreProto); + return getFieldImpl(proto, prop, proxy, ignoreProto); } - return undefined; } return field; } export function getField(target: any, prop: string | number, ignoreProto: boolean = false): any { - return getFieldImpl(target, prop, undefined, ignoreProto); + return getFieldImpl(target, prop, target[SelfProxy], ignoreProto); } export function deleteProperty(target: any, prop: string | number | symbol) { @@ -450,7 +409,7 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any diff?.op === '$addToSet' ? { redo: () => { - receiver[prop].push(...diff.items.map((item: any) => (item.value ? item.value() : item))); + receiver[prop].push(...diff.items.map((item: any) => item.value ?? item)); lastValue = ObjectField.MakeCopy(receiver[prop]); }, undo: action(() => { @@ -460,7 +419,7 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any const ind = receiver[prop].findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading); ind !== -1 && receiver[prop].splice(ind, 1); } else { - const ind = receiver[prop].indexOf(item.value ? item.value() : item); + const ind = receiver[prop].indexOf(item.value ?? item); ind !== -1 && receiver[prop].splice(ind, 1); } }); @@ -472,7 +431,7 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any ? { redo: action(() => { diff.items.forEach((item: any) => { - const ind = item instanceof SchemaHeaderField ? receiver[prop].findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading) : receiver[prop].indexOf(item.value ? item.value() : item); + const ind = item instanceof SchemaHeaderField ? receiver[prop].findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading) : receiver[prop].indexOf(item.value ?? item); ind !== -1 && receiver[prop].splice(ind, 1); }); lastValue = ObjectField.MakeCopy(receiver[prop]); @@ -484,8 +443,8 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any const ind = (prevValue as List<any>).findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading); ind !== -1 && receiver[prop].findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading) === -1 && receiver[prop].splice(ind, 0, item); } else { - const ind = (prevValue as List<any>).indexOf(item.value ? item.value() : item); - ind !== -1 && receiver[prop].indexOf(item.value ? item.value() : item) === -1 && receiver[prop].splice(ind, 0, item); + const ind = (prevValue as List<any>).indexOf(item.value ?? item); + ind !== -1 && receiver[prop].indexOf(item.value ?? item) === -1 && receiver[prop].splice(ind, 0, item); } }); lastValue = ObjectField.MakeCopy(receiver[prop]); diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx index 8265de445..2ae597b0b 100644 --- a/src/mobile/MobileInterface.tsx +++ b/src/mobile/MobileInterface.tsx @@ -700,7 +700,7 @@ export class MobileInterface extends React.Component { className="docButton" title={Doc.isDocPinned(this._activeDoc) ? 'Unpin from presentation' : 'Pin to presentation'} style={{ backgroundColor: isPinned ? 'black' : 'white', color: isPinned ? 'white' : 'black' }} - onClick={e => TabDocView.PinDoc(this._activeDoc)}> + onClick={e => TabDocView.PinDoc(this._activeDoc, {})}> <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="map-pin" /> </div> ); diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts index 4870d218b..33809824f 100644 --- a/src/server/DashUploadUtils.ts +++ b/src/server/DashUploadUtils.ts @@ -398,7 +398,9 @@ export namespace DashUploadUtils { // Use the request library to parse out file level image information in the headers const { headers } = await new Promise<any>((resolve, reject) => { return request.head(resolvedUrl, (error, res) => (error ? reject(error) : resolve(res))); - }).catch(e => console.log(e)); + }).catch(e => { + console.log('Error processing headers: ', e); + }); try { // Compute the native width and height ofthe image with an npm module |