/* eslint-disable no-use-before-define */ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Toggle, ToggleType, Type } from 'browndash-components'; import { action, computed, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { ClientUtils, returnEmptyFilter, returnTrue } from '../../ClientUtils'; import { emptyFunction } from '../../Utils'; import { CreateLinkToActiveAudio, Doc, DocListCast, FieldResult, Opt, returnEmptyDoclist } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; import { InkTool } from '../../fields/InkField'; import { BoolCast, Cast, NumCast, toList } from '../../fields/Types'; import { ScriptingGlobals } from '../util/ScriptingGlobals'; import { SnappingManager } from '../util/SnappingManager'; import { Transform } from '../util/Transform'; import { GestureOverlay } from './GestureOverlay'; import './LightboxView.scss'; import { ObservableReactComponent } from './ObservableReactComponent'; import { OverlayView } from './OverlayView'; import { DefaultStyleProvider, returnEmptyDocViewList, wavyBorderPath } from './StyleProvider'; import { DocumentView } from './nodes/DocumentView'; import { OpenWhere, OpenWhereMod } from './nodes/OpenWhere'; interface LightboxViewProps { PanelWidth: number; PanelHeight: number; maxBorder: number[]; addSplit: (document: Doc, pullSide: OpenWhereMod, stack?: unknown, panelName?: string | undefined, keyValue?: boolean | undefined) => boolean; } const savedKeys = ['freeform_panX', 'freeform_panY', 'freeform_scale', 'layout_scrollTop', 'layout_fieldKey']; type LightboxSavedState = { [key: string]: FieldResult; }; // prettier-ignore @observer export class LightboxView extends ObservableReactComponent { /** * Determines whether a DocumentView is descendant of the lightbox view * @param view * @returns true if a DocumentView is descendant of the lightbox view */ public static Contains(view?:DocumentView) { return view && LightboxView.Instance?._docView && (view.containerViewPath?.() ?? []).concat(view).includes(LightboxView.Instance?._docView); } // prettier-ignore public static LightboxDoc = () => LightboxView.Instance?._doc; // eslint-disable-next-line no-use-before-define static Instance: LightboxView; private _path: { doc: Opt; // target: Opt; history: { doc: Doc; target?: Doc }[]; future: Doc[]; saved: LightboxSavedState; }[] = []; private _savedState: LightboxSavedState = {}; private _history: { doc: Doc; target?: Doc }[] = []; @observable private _future: Doc[] = []; @observable private _layoutTemplate: Opt = undefined; @observable private _layoutTemplateString: Opt = undefined; @observable private _doc: Opt = undefined; @observable private _docTarget: Opt = undefined; @observable private _docView: Opt = undefined; @computed get leftBorder() { return Math.min(this._props.PanelWidth / 4, this._props.maxBorder[0]); } // prettier-ignore @computed get topBorder() { return Math.min(this._props.PanelHeight / 4, this._props.maxBorder[1]); } // prettier-ignore constructor(props: LightboxViewProps) { super(props); makeObservable(this); LightboxView.Instance = this; DocumentView._setLightboxDoc = this.SetLightboxDoc; DocumentView._lightboxContains = LightboxView.Contains; DocumentView._lightboxDoc = LightboxView.LightboxDoc; } /** * Sets the root Doc to render in the lightbox view. * @param doc * @param target a Doc within 'doc' to focus on (useful for freeform collections) * @param future a list of Docs to step through with the arrow buttons of the lightbox * @param layoutTemplate a template to apply to 'doc' to render it. * @returns success flag which is currently always true */ @action public SetLightboxDoc = (doc: Opt, target?: Doc, future?: Doc[], layoutTemplate?: Doc | string) => { const lightDoc = this._doc; lightDoc && lightDoc !== doc && savedKeys.forEach(key => { lightDoc[key] = this._savedState[key]; }); this._savedState = {}; if (doc) { lightDoc !== doc && savedKeys.forEach(key => { this._savedState[key] = Doc.Get(doc, key, true); }); const l = CreateLinkToActiveAudio(() => doc).lastElement(); l && (Cast(l.link_anchor_2, Doc, null).backgroundColor = 'lightgreen'); DocumentView.CurrentlyPlaying?.forEach(dv => dv.ComponentView?.Pause?.()); this._history.push({ doc, target }); } else { this._future = []; this._history = []; Doc.ActiveTool = InkTool.None; SnappingManager.SetExploreMode(false); } DocumentView.DeselectAll(); if (future) { this._future.push( ...(this._doc ? [this._doc] : []), ...future .slice() .sort((a, b) => NumCast(b._timecodeToShow) - NumCast(a._timecodeToShow)) .sort((a, b) => Doc.Links(a).length - Doc.Links(b).length) ); } this._doc = doc; this._layoutTemplate = layoutTemplate instanceof Doc ? layoutTemplate : undefined; if (doc && (typeof layoutTemplate === 'string' ? layoutTemplate : undefined)) { doc.layout_fieldKey = layoutTemplate; } this._docTarget = target ?? doc; return true; }; public AddDocTab = (docs: Doc | Doc[], location: OpenWhere, layoutTemplate?: Doc | string) => { const doc = toList(docs).lastElement(); return this.SetLightboxDoc( doc, undefined, [...DocListCast(doc[Doc.LayoutFieldKey(doc)]), ...DocListCast(doc[Doc.LayoutFieldKey(doc) + '_annotations']).filter(anno => anno.annotationOn !== doc), ...this._future].sort( (a, b) => NumCast(b._timecodeToShow) - NumCast(a._timecodeToShow) ), layoutTemplate ); }; @action next = () => { const lightDoc = this._doc; if (!lightDoc) return; const target = (this._docTarget = this._future.pop()); const targetDocView = target && DocumentView.getLightboxDocumentView(target); if (targetDocView && target) { const l = CreateLinkToActiveAudio(() => targetDocView.ComponentView?.getAnchor?.(true) || target).lastElement(); l && (Cast(l.link_anchor_2, Doc, null).backgroundColor = 'lightgreen'); DocumentView.showDocument(target, { willZoomCentered: true, zoomScale: 0.9 }); if (this._history.lastElement().target !== target) this._history.push({ doc: lightDoc, target }); } else if (!target && this._path.length) { savedKeys.forEach(key => { lightDoc[key] = this._savedState[key]; }); this._path.pop(); } else { this.SetLightboxDoc(target); } }; @action previous = () => { const previous = this._history.pop(); if (!previous || !this._history.length) { this.SetLightboxDoc(undefined); return; } const { doc, target } = this._history.lastElement(); const docView = DocumentView.getLightboxDocumentView(target || doc); if (docView) { this._docTarget = target; target && DocumentView.showDocument(target, { willZoomCentered: true, zoomScale: 0.9 }); } else { this.SetLightboxDoc(doc, target); } if (this._future.lastElement() !== previous.target || previous.doc) this._future.push(previous.target || previous.doc); }; @action stepInto = () => { this._path.push({ doc: this._doc, target: this._docTarget, future: this._future, history: this._history, saved: this._savedState, }); if (this._docTarget) { const fieldKey = Doc.LayoutFieldKey(this._docTarget); const contents = [...DocListCast(this._docTarget[fieldKey]), ...DocListCast(this._docTarget[fieldKey + '_annotations'])]; const links = Doc.Links(this._docTarget) .map(link => Doc.getOppositeAnchor(link, this._docTarget!)!) .filter(doc => doc); this.SetLightboxDoc(this._docTarget, undefined, contents.length ? contents : links); } }; downloadDoc = () => { const lightDoc = this._docTarget ?? this._doc; if (lightDoc) { Doc.RemoveDocFromList(Doc.MyRecentlyClosed, 'data', lightDoc); this._props.addSplit(lightDoc, OpenWhereMod.none); this.SetLightboxDoc(undefined); } }; toggleFitWidth = () => { this._doc && (this._doc._layout_fitWidth = !this._doc._layout_fitWidth); }; togglePen = () => { Doc.ActiveTool = Doc.ActiveTool === InkTool.Pen ? InkTool.None : InkTool.Pen; }; toggleExplore = () => SnappingManager.SetExploreMode(!SnappingManager.ExploreMode); lightboxDoc = () => this._doc; lightboxWidth = () => this._props.PanelWidth - this.leftBorder * 2; lightboxHeight = () => this._props.PanelHeight - this.topBorder * 2; lightboxScreenToLocal = () => new Transform(-this.leftBorder, -this.topBorder, 1); lightboxDocTemplate = () => this._layoutTemplate; future = () => this._future; renderNavBtn = (left: Opt, bottom: Opt, top: number, icon: IconProp, display: boolean, click: () => void, color?: string) => (
{ e.stopPropagation(); click(); }}>
{color}
); render() { let downx = 0; let downy = 0; const toggleBtn = (classname: string, tooltip: string, toggleBackground: boolean, icon: IconProp, icon2: IconProp | string, onClick: () => void) => (
} onClick={e => { e.stopPropagation(); runInAction(onClick); }} />
); return !this._doc ? ( ) : (
{ downx = e.clientX; downy = e.clientY; }} onClick={e => ClientUtils.isClick(e.clientX, e.clientY, downx, downy, Date.now()) && this.SetLightboxDoc(undefined)}>
{ this._docView = r !== null ? r : undefined; })} Document={this._doc} PanelWidth={this.lightboxWidth} PanelHeight={this.lightboxHeight} LayoutTemplate={this.lightboxDocTemplate} isDocumentActive={returnTrue} // without this being true, sidebar annotations need to be activated before text can be selected. isContentActive={returnTrue} styleProvider={DefaultStyleProvider} ScreenToLocalTransform={this.lightboxScreenToLocal} renderDepth={0} suppressSetHeight={!!this._doc._layout_fitWidth} containerViewPath={returnEmptyDocViewList} childFilters={returnEmptyFilter} childFiltersByRanges={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} addDocument={undefined} removeDocument={undefined} whenChildContentsActiveChanged={emptyFunction} addDocTab={this.AddDocTab} pinToPres={DocumentView.PinDoc} focus={emptyFunction} />
{this.renderNavBtn(0, undefined, this._props.PanelHeight / 2 - 12.5, 'chevron-left', this._doc && this._history.length ? true : false, this.previous)} {this.renderNavBtn( this._props.PanelWidth - Math.min(this._props.PanelWidth / 4, this._props.maxBorder[0]), undefined, this._props.PanelHeight / 2 - 12.5, 'chevron-right', this._doc && this._future.length ? true : false, this.next, this.future().length.toString() )} {toggleBtn('lightboxView-navBtn', 'toggle reading view', BoolCast(this._doc?._layout_fitWidth), 'book-open', 'book', this.toggleFitWidth)} {toggleBtn('lightboxView-tabBtn', 'open document in a tab', false, 'file-download', '', this.downloadDoc)} {toggleBtn('lightboxView-penBtn', 'toggle pen annotation', Doc.ActiveTool === InkTool.Pen, 'pen', '', this.togglePen)} {toggleBtn('lightboxView-exploreBtn', 'toggle navigate only mode', SnappingManager.ExploreMode, 'globe-americas', '', this.toggleExplore)}
); } } interface LightboxTourBtnProps { navBtn: (left: Opt, bottom: Opt, top: number, icon: IconProp, display: boolean, click: () => void, color?: string) => JSX.Element; // eslint-disable-next-line react/no-unused-prop-types future: () => Opt; stepInto: () => void; lightboxDoc: () => Opt; } @observer export class LightboxTourBtn extends React.Component { render() { return this.props.navBtn('50%', 0, 0, 'chevron-down', this.props.lightboxDoc() ? true : false, this.props.stepInto, ''); } } // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function deiconifyViewToLightbox(documentView: DocumentView) { LightboxView.Instance.AddDocTab(documentView.Document, OpenWhere.lightboxAlways, 'layout'); // , 0); });