import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { action, reaction, runInAction } from 'mobx'; import { basename } from 'path'; import { DateField } from '../../fields/DateField'; import { Doc, DocListCast, Field, LinkedTo, Opt, StrListCast, updateCachedAcls } from '../../fields/Doc'; import { DocData, Initializing } from '../../fields/DocSymbols'; import { Id } from '../../fields/FieldSymbols'; import { HtmlField } from '../../fields/HtmlField'; import { InkField, PointData } from '../../fields/InkField'; import { List } from '../../fields/List'; import { RichTextField } from '../../fields/RichTextField'; import { SchemaHeaderField } from '../../fields/SchemaHeaderField'; import { ComputedField, ScriptField } from '../../fields/ScriptField'; import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } from '../../fields/Types'; import { AudioField, CsvField, ImageField, PdfField, VideoField, WebField, YoutubeField } from '../../fields/URLField'; import { inheritParentAcls, SharingPermissions } from '../../fields/util'; import { Upload } from '../../server/SharedMediaTypes'; import { OmitKeys, Utils } from '../../Utils'; import { YoutubeBox } from '../apis/youtube/YoutubeBox'; import { DocServer } from '../DocServer'; import { Networking } from '../Network'; import { DragManager, dropActionType } from '../util/DragManager'; import { FollowLinkScript } from '../util/LinkFollower'; import { LinkManager } from '../util/LinkManager'; import { ScriptingGlobals } from '../util/ScriptingGlobals'; import { undoable, UndoManager } from '../util/UndoManager'; import { CollectionDockingView } from '../views/collections/CollectionDockingView'; import { DimUnit } from '../views/collections/collectionMulticolumn/CollectionMulticolumnView'; import { CollectionView } from '../views/collections/CollectionView'; import { ContextMenu } from '../views/ContextMenu'; import { ContextMenuProps } from '../views/ContextMenuItem'; import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, InkingStroke } from '../views/InkingStroke'; import { AudioBox, media_state } from '../views/nodes/AudioBox'; import { ComparisonBox } from '../views/nodes/ComparisonBox'; import { DataVizBox } from '../views/nodes/DataVizBox/DataVizBox'; import { EquationBox } from '../views/nodes/EquationBox'; import { FieldViewProps } from '../views/nodes/FieldView'; import { FontIconBox } from '../views/nodes/FontIconBox/FontIconBox'; import { FormattedTextBox } from '../views/nodes/formattedText/FormattedTextBox'; import { FunctionPlotBox } from '../views/nodes/FunctionPlotBox'; import { ImageBox } from '../views/nodes/ImageBox'; import { KeyValueBox } from '../views/nodes/KeyValueBox'; import { LabelBox } from '../views/nodes/LabelBox'; import { LinkBox } from '../views/nodes/LinkBox'; import { LinkDescriptionPopup } from '../views/nodes/LinkDescriptionPopup'; import { LoadingBox } from '../views/nodes/LoadingBox'; import { MapBox } from '../views/nodes/MapBox/MapBox'; import { MapPushpinBox } from '../views/nodes/MapBox/MapPushpinBox'; import { PDFBox } from '../views/nodes/PDFBox'; import { PhysicsSimulationBox } from '../views/nodes/PhysicsBox/PhysicsSimulationBox'; import { RecordingBox } from '../views/nodes/RecordingBox/RecordingBox'; import { ScreenshotBox } from '../views/nodes/ScreenshotBox'; import { ScriptingBox } from '../views/nodes/ScriptingBox'; import { TaskCompletionBox } from '../views/nodes/TaskCompletedBox'; import { PresBox } from '../views/nodes/trails/PresBox'; import { PresElementBox } from '../views/nodes/trails/PresElementBox'; import { VideoBox } from '../views/nodes/VideoBox'; import { DiagramBox } from '../views/nodes/DiagramBox'; import { WebBox } from '../views/nodes/WebBox'; import { SearchBox } from '../views/search/SearchBox'; import { CollectionViewType, DocumentType } from './DocumentTypes'; import { CalendarBox } from '../views/nodes/calendarBox/CalendarBox'; import { OpenWhere } from '../views/nodes/DocumentView'; const { default: { DFLT_IMAGE_NATIVE_DIM } } = require('../views/global/globalCssVariables.module.scss'); // prettier-ignore const defaultNativeImageDim = Number(DFLT_IMAGE_NATIVE_DIM.replace('px', '')); class EmptyBox { public static LayoutString() { return ''; } } export enum FInfoFieldType { string = 'string', boolean = 'boolean', number = 'number', Doc = 'Doc', enumeration = 'enum', date = 'date', list = 'list', rtf = 'rich text', } export class FInfo { description: string = ''; readOnly: boolean = false; fieldType?: FInfoFieldType; values?: Field[]; filterable?: boolean = true; // can be used as a Filter in FilterPanel // format?: string; // format to display values (e.g, decimal places, $, etc) // parse?: ScriptField; // parse a value from a string constructor(d: string, readOnly?: boolean) { this.description = d; this.readOnly = readOnly ?? false; } searchable = () => true; } class BoolInfo extends FInfo { fieldType? = FInfoFieldType.boolean; values?: boolean[] = [true, false]; constructor(d: string, filterable?: boolean) { super(d); this.filterable = filterable; } override searchable = () => false; } class NumInfo extends FInfo { fieldType? = FInfoFieldType.number; values?: number[] = []; constructor(d: string, filterable?: boolean, readOnly?: boolean, values?: number[]) { super(d, readOnly); this.values = values; this.filterable = filterable; } override searchable = () => false; } class StrInfo extends FInfo { fieldType? = FInfoFieldType.string; values?: string[] = []; constructor(d: string, filterable?: boolean, readOnly?: boolean, values?: string[]) { super(d, readOnly); this.values = values; this.filterable = filterable; } } class DocInfo extends FInfo { fieldType? = FInfoFieldType.Doc; values?: Doc[] = []; constructor(d: string, filterable?: boolean, values?: Doc[]) { super(d, true); this.values = values; this.filterable = filterable; } override searchable = () => false; } class DimInfo extends FInfo { fieldType? = FInfoFieldType.enumeration; values? = [DimUnit.Pixel, DimUnit.Ratio]; readOnly = false; filterable = false; override searchable = () => false; } class PEInfo extends FInfo { fieldType? = FInfoFieldType.enumeration; values? = ['all', 'none']; readOnly = false; filterable = false; override searchable = () => false; } class DAInfo extends FInfo { fieldType? = FInfoFieldType.enumeration; values? = ['embed', 'copy', 'move', 'same', 'add', 'inSame', 'proto']; readOnly = false; filterable = false; override searchable = () => false; } class CTypeInfo extends FInfo { fieldType? = FInfoFieldType.enumeration; values? = Array.from(Object.keys(CollectionViewType)); readOnly = false; filterable = false; override searchable = () => false; } class DTypeInfo extends FInfo { fieldType? = FInfoFieldType.enumeration; values? = Array.from(Object.keys(DocumentType)); override searchable = () => false; } class DateInfo extends FInfo { constructor(d: string, filterable?: boolean) { super(d, true); this.filterable = filterable; } fieldType? = FInfoFieldType.date; values?: DateField[] = []; } class RtfInfo extends FInfo { constructor(d: string, filterable?: boolean) { super(d, true); this.filterable = filterable; } fieldType? = FInfoFieldType.rtf; } class ListInfo extends FInfo { fieldType? = FInfoFieldType.list; values?: List[] = []; } type BOOLt = BoolInfo | boolean; type NUMt = NumInfo | number; type STRt = StrInfo | string; type LISTt = ListInfo | List; type DOCt = DocInfo | Doc; type RTFt = RtfInfo | RichTextField; type DIMt = DimInfo | typeof DimUnit.Pixel | typeof DimUnit.Ratio; type PEVt = PEInfo | 'none' | 'all'; type COLLt = CTypeInfo | CollectionViewType; type DROPt = DAInfo | dropActionType; type DATEt = DateInfo | number; type DTYPEt = DTypeInfo | string; export class DocumentOptions { // coordinate and dimensions depending on view x?: NUMt = new NumInfo('horizontal coordinate in freeform view', false); y?: NUMt = new NumInfo('vertical coordinate in freeform view', false); z?: NUMt = new NumInfo('whether document is in overlay (1) or not (0)', false, false, [1, 0]); overlayX?: NUMt = new NumInfo('horizontal coordinate in overlay view', false); overlayY?: NUMt = new NumInfo('vertical coordinate in overlay view', false); text?: RTFt = new RtfInfo('plain or rich text', true); _dimMagnitude?: NUMt = new NumInfo("magnitude of collectionMulti{row,col} element's width or height", false); _dimUnit?: DIMt = new DimInfo("units of collectionMulti{row,col} element's width or height - 'px' or '*' for pixels or relative units"); latitude?: NUMt = new NumInfo('latitude coordinate', false); longitude?: NUMt = new NumInfo('longitude coordinate', false); routeCoordinates?: STRt = new StrInfo("stores a route's/direction's coordinates (stringified version)"); // for a route document, this stores the route's coordinates markerType?: STRt = new StrInfo('marker type for a pushpin document'); markerColor?: STRt = new StrInfo('marker color for a pushpin document'); map?: STRt = new StrInfo('map location name'); map_type?: STRt = new StrInfo('type of map view', false); map_zoom?: NUMt = new NumInfo('zoom of a map view', false); map_pitch?: NUMt = new NumInfo('pitch of a map view', false); map_bearing?: NUMt = new NumInfo('bearing of a map view', false); map_style?: STRt = new StrInfo('mapbox style for a map view', false); date_range?: STRt = new StrInfo('date range for calendar', false); wikiData?: STRt = new StrInfo('WikiData ID related to map location'); description?: STRt = new StrInfo('description of document'); _timecodeToShow?: NUMt = new NumInfo('media timecode when document should appear (e.g., when an annotation shows up as a video plays)', false); _timecodeToHide?: NUMt = new NumInfo('media timecode when document should disappear', false); _width?: NUMt = new NumInfo("width of document in container's coordinates"); _height?: NUMt = new NumInfo("height of document in container's coordiantes"); data_nativeWidth?: NUMt = new NumInfo('native width of data field contents (e.g., the pixel width of an image)', false); data_nativeHeight?: NUMt = new NumInfo('native height of data field contents (e.g., the pixel height of an image)', false); linearBtnWidth?: NUMt = new NumInfo('unexpanded width of a linear menu button (button "width" changes when it expands)', false); _nativeWidth?: NUMt = new NumInfo('native width of document contents (e.g., the pixel width of an image)', false); _nativeHeight?: NUMt = new NumInfo('native height of document contents (e.g., the pixel height of an image)', false); 'acl-Guest'?: STRt = new StrInfo("permissions granted to users logged in as 'guest' (either view, or private)"); // public permissions '_acl-Guest'?: string; // public permissions type?: DTYPEt = new DTypeInfo('type of document', true); type_collection?: COLLt = new CTypeInfo('how collection is rendered'); // sub type of a collection _type_collection?: COLLt = new CTypeInfo('how collection is rendered'); // sub type of a collection title?: STRt = new StrInfo('title of document', true); title_custom?: BOOLt = new BoolInfo('whether title is a default or has been intentionally set'); caption?: RichTextField; author?: string; // STRt = new StrInfo('creator of document'); // bcz: don't change this. Otherwise, the userDoc's field Infos will have a FieldInfo assigned to its author field which will render it unreadable author_date?: DATEt = new DateInfo('date the document was created', true); annotationOn?: DOCt = new DocInfo('document annotated by this document', false); _embedContainer?: DOCt = new DocInfo('document that displays (contains) this document', false); rootDocument?: DOCt = new DocInfo('document that supplies the information needed for a rendering template (eg, pres slide for PresElement)'); color?: STRt = new StrInfo('foreground color data doc', false); hidden?: BOOLt = new BoolInfo('whether the document is not rendered by its collection', false); backgroundColor?: STRt = new StrInfo('background color for data doc', false); opacity?: NUMt = new NumInfo('document opacity', false); viewTransitionTime?: NUMt = new NumInfo('transition duration for view parameters', false); dontRegisterView?: BOOLt = new BoolInfo('are views of this document registered so that they can be found when following links, etc', false); _undoIgnoreFields?: List; //'fields that should not be added to the undo stack (opacity for Undo/Redo/and sidebar) AND whether modifications to document are undoable (true for linearview menu buttons to prevent open/close from entering undo stack)' undoIgnoreFields?: List; //'fields that should not be added to the undo stack (opacity for Undo/Redo/and sidebar) AND whether modifications to document are undoable (true for linearview menu buttons to prevent open/close from entering undo stack)' _headerHeight?: NUMt = new NumInfo('height of document header used for displaying title', false); _headerFontSize?: NUMt = new NumInfo('font size of header of custom notes', false); _headerPointerEvents?: PEVt = new PEInfo('types of events the header of a custom text document can consume'); _lockedPosition?: BOOLt = new BoolInfo("lock the x,y coordinates of the document so that it can't be dragged"); _lockedTransform?: BOOLt = new BoolInfo('lock the freeform_panx,freeform_pany and scale parameters of the document so that it be panned/zoomed'); layout?: string | Doc; // default layout string or template document layout_isSvg?: BOOLt = new BoolInfo('whether document decorations and other selections should handle pointerEvents for svg content or use doc bounding box'); layout_keyValue?: STRt = new StrInfo('layout definition for showing keyValue view of document', false); layout_explainer?: STRt = new StrInfo('explanation displayed at top of a collection to describe its purpose', false); layout_headerButton?: DOCt = new DocInfo('the (button) Doc to display at the top of a collection.', false); layout_disableBrushing?: BOOLt = new BoolInfo('whether to suppress border highlighting'); layout_unrendered?: BOOLt = new BoolInfo('denotes an annotation that is not rendered with a DocumentView (e.g, rtf/pdf text selections and links to scroll locations in web/pdf)'); layout_hideOpenButton?: BOOLt = new BoolInfo('whether to hide the open full screen button when selected'); layout_hideDocumentButtonBar?: BOOLt = new BoolInfo('whether to hide the document decorations lower button bar when selected'); layout_hideLinkAnchors?: BOOLt = new BoolInfo('suppresses link anchor dots from being displayed'); layout_hideAllLinks?: BOOLt = new BoolInfo('whether all individual blue anchor dots should be hidden'); layout_hideResizeHandles?: BOOLt = new BoolInfo('whether to hide the resize handles when selected'); layout_hideLinkButton?: BOOLt = new BoolInfo('whether the blue link counter button should be hidden'); layout_hideDecorationTitle?: BOOLt = new BoolInfo('whether to suppress the document decortations title when selected'); _layout_hideContextMenu?: BOOLt = new BoolInfo('whether the context menu can be shown'); layout_diagramEditor?: STRt = new StrInfo("specify the JSX string for a diagram editor view") layout_hideContextMenu?: BOOLt = new BoolInfo('whether the context menu can be shown'); layout_borderRounding?: string; _layout_modificationDate?: DATEt = new DateInfo('last modification date of doc layout', false); _layout_nativeDimEditable?: BOOLt = new BoolInfo('native dimensions can be modified using document decoration reizers', false); _layout_reflowVertical?: BOOLt = new BoolInfo('permit vertical resizing with content "reflow"'); _layout_reflowHorizontal?: BOOLt = new BoolInfo('permit horizontal resizing with content reflow'); layout_boxShadow?: string; // box-shadow css string OR "standard" to use dash standard box shadow layout_maxShown?: NUMt = new NumInfo('maximum number of children to display at one time (see multicolumnview)'); _layout_autoHeight?: BOOLt = new BoolInfo('whether document automatically resizes vertically to display contents'); _layout_curPage?: NUMt = new NumInfo('current page of a PDF or other? paginated document', false); _layout_currentTimecode?: NUMt = new NumInfo('the current timecode of a time-based document (e.g., current time of a video) value is in seconds', false); _layout_centered?: BOOLt = new BoolInfo('whether text should be vertically centered in Doc'); _layout_fitWidth?: BOOLt = new BoolInfo('whether document should scale its contents to fit its rendered width or not (e.g., for PDFviews)'); _layout_fieldKey?: STRt = new StrInfo('the field key containing the current layout definition', false); _layout_enableAltContentUI?: BOOLt = new BoolInfo('whether to show alternate content button'); _layout_showTitle?: string; // field name to display in header (:hover is an optional suffix) _layout_showSidebar?: BOOLt = new BoolInfo('whether an annotationsidebar should be displayed for text docuemnts'); _layout_showCaption?: string; // which field to display in the caption area. leave empty to have no caption _chromeHidden?: BOOLt = new BoolInfo('whether the editing chrome for a document is hidden'); hideClickBehaviors?: BOOLt = new BoolInfo('whether to hide click behaviors in context menu'); _gridGap?: NUMt = new NumInfo('gap between items in masonry view', false); _xMargin?: NUMt = new NumInfo('gap between left edge of document and start of masonry/stacking layouts', false); _yMargin?: NUMt = new NumInfo('gap between top edge of dcoument and start of masonry/stacking layouts', false); _xPadding?: NUMt = new NumInfo('x padding', false); _yPadding?: NUMt = new NumInfo('y padding', false); _singleLine?: boolean; // whether label box is restricted to one line of text _createDocOnCR?: boolean; // whether carriage returns and tabs create new text documents _columnWidth?: NUMt = new NumInfo('width of table column', false); _columnsHideIfEmpty?: BOOLt = new BoolInfo('whether stacking view column headings should be hidden'); _caption_xMargin?: NUMt = new NumInfo('x margin of caption inside of a carousel collection', false, true); _caption_yMargin?: NUMt = new NumInfo('y margin of caption inside of a carousel collection', false, true); icon_nativeWidth?: NUMt = new NumInfo('native width of icon view', false, true); icon_nativeHeight?: NUMt = new NumInfo('native height of icon view', false, true); _text_fontSize?: string; _text_fontFamily?: string; _text_fontWeight?: string; _pivotField?: string; // field key used to determine headings for sections in stacking, masonry, pivot views infoWindowOpen?: BOOLt = new BoolInfo('whether info window corresponding to pin is open (on MapDocuments)'); _carousel_index?: NUMt = new NumInfo('which item index the carousel viewer is showing'); _label_minFontSize?: NUMt = new NumInfo('minimum font size for labelBoxes', false); _label_maxFontSize?: NUMt = new NumInfo('maximum font size for labelBoxes', false); stroke_width?: NUMt = new NumInfo('width of an ink stroke', false); mediaState?: STRt = new StrInfo(`status of audio/video media document: ${media_state.PendingRecording}, ${media_state.Recording}, ${media_state.Paused}, ${media_state.Playing}`, false); recording?: BOOLt = new BoolInfo('whether WebCam is recording or not'); slides?: DOCt = new DocInfo('presentation slide associated with video recording (bcz: should be renamed!!)'); autoPlayAnchors?: BOOLt = new BoolInfo('whether to play audio/video when an anchor is clicked in a stackedTimeline.'); dontPlayLinkOnSelect?: BOOLt = new BoolInfo('whether an audio/video should start playing when a link is followed to it.'); openFactoryLocation?: string; // an OpenWhere value to place the factory created document openFactoryAsDelegate?: boolean; // onViewMounted?: ScriptField; // reactive script invoked Doc is viewed (used by showBackLinks view to update collection of links to Doc) toolTip?: string; // tooltip to display on hover toolType?: string; // type of pen tool expertMode?: BOOLt = new BoolInfo('something available only in expert (not novice) mode'); contextMenuFilters?: List; contextMenuScripts?: List; contextMenuLabels?: List; contextMenuIcons?: List; childContentPointerEvents?: string; // pointer events allowed for content of a document view. eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents childFilters_boolean?: string; childFilters?: List; childLimitHeight?: NUMt = new NumInfo('whether to limit the height of collection children. 0 - means height can be no bigger than width', false); childLayoutTemplate?: Doc; // template for collection to use to render its children (see PresBox layout in tree view) childLayoutString?: string; // template string for collection to use to render its children childDocumentsActive?: BOOLt = new BoolInfo('whether child documents are active when parent is document active'); childLayoutFitWidth?: BOOLt = new BoolInfo("whether a child doc's fitWith should be overriden by collection"); childDontRegisterViews?: BOOLt = new BoolInfo('whether child document views should be registered so that they can be found when following links, etc'); childHideLinkButton?: BOOLt = new BoolInfo('hide link buttons on all children'); childContextMenuFilters?: List; childContextMenuScripts?: List; childContextMenuLabels?: List; childContextMenuIcons?: List; targetScriptKey?: string; // where to write a template script (used by collections with click templates which need to target onClick, onDoubleClick, etc) lastFrame?: NUMt = new NumInfo('the last frame of a frame-based collection (e.g., progressive slide)', false); activeFrame?: NUMt = new NumInfo('the active frame of a document in a frame base collection', false); appearFrame?: NUMt = new NumInfo('the frame in which the document appears', false); _currentFrame?: NUMt = new NumInfo('the current frame of a frame-based collection (e.g., progressive slide)', false); isSystem?: BOOLt = new BoolInfo('is this a system created/owned doc', false); isBaseProto?: BOOLt = new BoolInfo('is doc a base level prototype for data documents as opposed to data documents which are prototypes for layout documents. base protos are not cloned during a deep'); isTemplateForField?: string; // the field key for which the containing document is a rendering template isTemplateDoc?: BOOLt = new BoolInfo('is the document a template for creating other documents'); isGroup?: BOOLt = new BoolInfo('should collection use a grouping UI behavior'); isFolder?: BOOLt = new BoolInfo('is document a tree view folder'); _isTimelineLabel?: BOOLt = new BoolInfo('is document a timeline label'); _isLightbox?: BOOLt = new BoolInfo('whether a collection acts as a lightbox by opening lightbox links by hiding all other documents in collection besides link target'); mapPin?: DOCt = new DocInfo('pin associated with a config anchor', false); config_latitude?: NUMt = new NumInfo('latitude of a map', false); config_longitude?: NUMt = new NumInfo('longitude of map', false); config_map_zoom?: NUMt = new NumInfo('zoom of map', false); config_map_type?: STRt = new StrInfo('map view type (e.g, aerial)', false); config_map?: STRt = new StrInfo('text location of map', false); config_panX?: NUMt = new NumInfo('panX saved as a view spec', false); config_panY?: NUMt = new NumInfo('panY saved as a view spec', false); config_viewScale?: NUMt = new NumInfo('viewScale saved as a view Spec', false); presentation_transition?: NUMt = new NumInfo('the time taken for the transition TO a document', false); presentation_duration?: NUMt = new NumInfo('the duration of the slide in presentation view', false); presentation_zoomText?: BOOLt = new BoolInfo('whether text anchors should shown in a larger box when following links to make them stand out', false); data?: any; data_useCors?: BOOLt = new BoolInfo('whether CORS protocol should be used for web page'); columnHeaders?: List; // headers for stacking views schemaHeaders?: List; // headers for schema view dockingConfig?: STRt = new StrInfo('configuration of golden layout windows (applies only if doc is rendered as a CollectionDockingView)', false); icon?: string; // icon used by fonticonbox to render button noteType?: string; // STOPPING HERE // freeform properties _freeform_backgroundGrid?: BOOLt = new BoolInfo('whether background grid is shown on freeform collections'); _freeform_scale_min?: NUMt = new NumInfo('how far out a view can zoom (used by image/videoBoxes that are clipped'); _freeform_scale_max?: NUMt = new NumInfo('how far in a view can zoom (used by sidebar freeform views'); _freeform_scale?: NUMt = new NumInfo('how much a freeform view has been scaled (zoomed)'); _freeform_panX?: NUMt = new NumInfo('horizontal pan location of a freeform view'); _freeform_panY?: NUMt = new NumInfo('vertical pan location of a freeform view'); _freeform_noAutoPan?: BOOLt = new BoolInfo('disables autopanning when this item is dragged'); _freeform_noZoom?: BOOLt = new BoolInfo('disables zooming (used by Pile docs)'); _freeform_fitContentsToBox?: BOOLt = new BoolInfo('whether a freeformview should zoom/scale to create a shrinkwrapped view of its content'); //BUTTONS buttonText?: string; btnType?: string; btnList?: List; docColorBtn?: string; userColorBtn?: string; script?: ScriptField; numBtnMax?: NUMt = new NumInfo('maximum value of a number button'); numBtnMin?: NUMt = new NumInfo('minimum value of a number button'); switchToggle?: boolean; badgeValue?: ScriptField; //LINEAR VIEW linearView_IsOpen?: BOOLt = new BoolInfo('is linear view open'); linearView_Expandable?: BOOLt = new BoolInfo('can linear view be expanded'); linearView_Dropdown?: BOOLt = new BoolInfo('can linear view be opened as a dropdown'); linearView_SubMenu?: BOOLt = new BoolInfo('is doc a sub menu of more linear views'); flexGap?: NUMt = new NumInfo('Linear view flex gap'); flexDirection?: 'unset' | 'row' | 'column' | 'row-reverse' | 'column-reverse'; link_description?: string; // added for links link_relationship?: string; // type of relatinoship a link represents link_displayLine?: BOOLt = new BoolInfo('whether a link line should be dipslayed between the two link anchors'); link_displayArrow?: BOOLt = new BoolInfo("whether to display link's directional arrowhead"); link_anchor_1?: DOCt = new DocInfo('start anchor of a link'); link_anchor_2?: DOCt = new DocInfo('end anchor of a link'); link_autoMoveAnchors?: BOOLt = new BoolInfo('whether link endpoint should move around the edges of a document to make shortest path to other link endpoint'); link_anchor_1_useSmallAnchor?: BOOLt = new BoolInfo('whether link_anchor_1 of a link should use a miniature anchor dot (as when the anchor is a text selection)'); link_anchor_2_useSmallAnchor?: BOOLt = new BoolInfo('whether link_anchor_1 of a link should use a miniature anchor dot (as when the anchor is a text selection)'); link_relationshipList?: List; // for storing different link relationships (when set by user in the link editor) link_relationshipSizes?: List; //stores number of links contained in each relationship link_colorList?: List; // colors of links corresponding to specific link relationships followLinkZoom?: BOOLt = new BoolInfo('whether to zoom to the target of a link'); followLinkToggle?: BOOLt = new BoolInfo('whether target of link should be toggled on and off when following a link to it'); followLinkLocation?: STRt = new StrInfo('where to open link target when following link'); followLinkAnimEffect?: STRt = new StrInfo('animation effect triggered on target of link'); followLinkAnimDirection?: STRt = new StrInfo('direction modifier for animation effect'); ignoreClick?: BOOLt = new BoolInfo('whether clicks on document should be ignored'); onClick?: ScriptField; onDoubleClick?: ScriptField; onChildClick?: ScriptField; // script given to children of a collection to execute when they are clicked onChildDoubleClick?: ScriptField; // script given to children of a collection to execute when they are double clicked onClickScriptDisable?: STRt = new StrInfo('"always" disable click script, "never" disable click script, or default'); defaultDoubleClick?: 'ignore' | 'default'; // ignore double clicks, or default (undefined) means open document full screen waitForDoubleClickToClick?: 'always' | 'never' | 'default'; // whether a click function wait for double click to expire. 'default' undefined = wait only if there's a click handler, "never" = never wait, "always" = alway wait onPointerDown?: ScriptField; onPointerUp?: ScriptField; _forceActive?: BOOLt = new BoolInfo('flag to handle pointer events when not selected (or otherwise active)'); _dragOnlyWithinContainer?: BOOLt = new BoolInfo('whether the document should remain in its collection when someone tries to drag and drop it elsewhere'); _keepZWhenDragged?: BOOLt = new BoolInfo('whether a document should keep its z-order when dragged.'); childDragAction?: DROPt = new DAInfo('what should happen to the child documents when they are dragged from the collection'); dropConverter?: ScriptField; // script to run when documents are dropped on this Document. dropAction?: DROPt = new DAInfo("what should happen to this document when it's dropped somewhere else"); _dropAction?: DROPt = new DAInfo("what should happen to this document when it's dropped somewhere else"); _dropPropertiesToRemove?: List; // 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 cloneFieldFilter?: List; // fields not to copy when the document is clonedclipboard?: Doc; dragWhenActive?: BOOLt = new BoolInfo('should document drag when it is active instead of interacting with its contents - e.g., pileView, group'); dragAction?: DROPt = new DAInfo('how to drag document when it is active (e.g., tree, groups)'); dragFactory_count?: NUMt = new NumInfo('number of items created from a drag button (used for setting title with incrementing index)', false, true); dragFactory?: DOCt = new DocInfo('document to create when dragging with a suitable onDragStart script', false); clickFactory?: DOCt = new DocInfo('document to create when clicking on a button with a suitable onClick script', false); onDragStart?: ScriptField; //script to execute at start of drag operation -- e.g., when a "creator" button is dragged this script generates a different document to drop target?: Doc; // available for use in scripts. used to provide a document parameter to the script (Note, this is a convenience entry since any field could be used for parameterizing a script) tags?: LISTt = new ListInfo('hashtags added to document, typically using a text view', true); treeView_HideTitle?: BOOLt = new BoolInfo('whether to hide the top document title of a tree view'); treeView_HideUnrendered?: BOOLt = new BoolInfo("tells tree view not to display documents that have an 'layout_unrendered' tag unless they also have a treeView_FieldKey tag (presBox)"); treeView_HideHeaderIfTemplate?: BOOLt = new BoolInfo('whether to hide the header for a document in a tree view only if a childLayoutTemplate is provided (presBox)'); treeView_HideHeader?: BOOLt = new BoolInfo('whether to hide the header for a document in a tree view'); treeView_HideHeaderFields?: BOOLt = new BoolInfo('whether to hide the drop down options for tree view items.'); treeView_ChildDoubleClick?: ScriptField; // treeView_OpenIsTransient?: BOOLt = new BoolInfo("ignores the treeView_Open Doc flag, allowing a treeView_Item's expand/collapse state to be independent of other views of the same document in the same or any other tree view"); treeView_Open?: BOOLt = new BoolInfo('whether this document is expanded in a tree view'); treeView_ExpandedView?: string; // which field/thing is displayed when this item is opened in tree view treeView_ExpandedViewLock?: BOOLt = new BoolInfo('whether the expanded view can be changed'); treeView_Checked?: ScriptField; // script to call when a tree view checkbox is checked treeView_TruncateTitleWidth?: NUMt = new NumInfo('maximum width of a treew view title before truncation'); treeView_HasOverlay?: BOOLt = new BoolInfo('whether the treeview has an overlay for freeform annotations'); treeView_Type?: string; // whether treeview is a Slide, file system, or (default) collection hierarchy treeView_FreezeChildren?: STRt = new StrInfo('set (add, remove, add|remove) to disable adding, removing or both from collection'); sidebar_color?: string; // background color of text sidebar sidebar_type_collection?: string; // collection type of text sidebar data_dashboards?: List; // list of dashboards used in shareddocs; textTransform?: string; letterSpacing?: string; iconTemplate?: string; // name of icon template style selectedIndex?: NUMt = new NumInfo("which item in a linear view has been selected using the 'thumb doc' ui"); fieldValues?: List; // possible values a field can have (used by FieldInfo's only) fieldType?: string; // display type of a field, e.g. string, number, enumeration (used by FieldInfo's only) clipboard?: Doc; hoverBackgroundColor?: string; // background color of a label when hovered userBackgroundColor?: STRt = new StrInfo('background color associated with a Dash user (seen in header fields of shared documents)'); userColor?: STRt = new StrInfo('color associated with a Dash user (seen in header fields of shared documents)'); } export const DocOptions = new DocumentOptions(); export namespace Docs { export let newAccount: boolean = false; export namespace Prototypes { type LayoutSource = { LayoutString: (key: string) => string }; type PrototypeTemplate = { layout: { view: LayoutSource; dataField: string; }; data?: any; options?: Partial; }; type TemplateMap = Map; type PrototypeMap = Map; const defaultDataKey = 'data'; const TemplateMap: TemplateMap = new Map([ [ DocumentType.RTF, { layout: { view: FormattedTextBox, dataField: 'text' }, options: { _height: 35, _xMargin: 10, _yMargin: 10, layout_nativeDimEditable: true, layout_reflowVertical: true, layout_reflowHorizontal: true, defaultDoubleClick: 'ignore', systemIcon: 'BsFileEarmarkTextFill', }, }, ], [ DocumentType.SEARCH, { layout: { view: SearchBox, dataField: defaultDataKey }, options: { _width: 400 }, }, ], [ DocumentType.IMG, { layout: { view: ImageBox, dataField: defaultDataKey }, options: { freeform: '', systemIcon: 'BsFileEarmarkImageFill' }, }, ], [ DocumentType.WEB, { layout: { view: WebBox, dataField: defaultDataKey }, options: { _height: 300, _layout_fitWidth: true, layout_nativeDimEditable: true, layout_reflowVertical: true, waitForDoubleClickToClick: 'always', systemIcon: 'BsGlobe' }, }, ], [ DocumentType.COL, { layout: { view: CollectionView, dataField: defaultDataKey }, options: { _layout_fitWidth: true, freeform: '', _freeform_panX: 0, _freeform_panY: 0, _freeform_scale: 1, layout_nativeDimEditable: true, layout_reflowHorizontal: true, layout_reflowVertical: true, systemIcon: 'BsFillCollectionFill', }, }, ], [ DocumentType.KVP, { layout: { view: KeyValueBox, dataField: defaultDataKey }, options: { _layout_fitWidth: true, _height: 150 }, }, ], [ DocumentType.VID, { layout: { view: VideoBox, dataField: defaultDataKey }, options: { _layout_currentTimecode: 0, systemIcon: 'BsFileEarmarkPlayFill' }, }, ], [ DocumentType.AUDIO, { layout: { view: AudioBox, dataField: defaultDataKey }, options: { _height: 100, layout_fitWidth: true, layout_reflowHorizontal: true, layout_reflowVertical: true, layout_nativeDimEditable: true, systemIcon: 'BsFillVolumeUpFill' }, }, ], [ DocumentType.REC, { layout: { view: VideoBox, dataField: defaultDataKey }, options: { _height: 100, backgroundColor: 'pink', systemIcon: 'BsFillMicFill' }, }, ], [ DocumentType.PDF, { layout: { view: PDFBox, dataField: defaultDataKey }, options: { _layout_curPage: 1, _layout_fitWidth: true, layout_nativeDimEditable: true, layout_reflowVertical: true, systemIcon: 'BsFileEarmarkPdfFill' }, }, ], [ DocumentType.MAP, { layout: { view: MapBox, dataField: defaultDataKey }, options: { map: '', _height: 600, _width: 800, layout_reflowHorizontal: true, layout_reflowVertical: true, layout_nativeDimEditable: true, systemIcon: 'BsFillPinMapFill' }, }, ], [ DocumentType.LINK, { layout: { view: LinkBox, dataField: 'link' }, options: { childDontRegisterViews: true, layout_hideLinkAnchors: true, _height: 1, _width: 1, link: '', link_description: '', color: 'lightBlue', // lightblue is default color for linking dot and link documents text comment area _dropPropertiesToRemove: new List(['onClick']), }, }, ], [ DocumentType.SCRIPTDB, { data: new List(), layout: { view: EmptyBox, dataField: defaultDataKey }, options: { title: 'Global Script Database' }, }, ], [ DocumentType.SCRIPTING, { layout: { view: ScriptingBox, dataField: defaultDataKey }, options: { systemIcon: 'BsFileEarmarkCodeFill' }, }, ], [ DocumentType.YOUTUBE, { layout: { view: YoutubeBox, dataField: defaultDataKey }, }, ], [ DocumentType.LABEL, { layout: { view: LabelBox, dataField: 'title' }, options: { _singleLine: true, layout_nativeDimEditable: true, layout_reflowHorizontal: true, layout_reflowVertical: true }, }, ], [ DocumentType.EQUATION, { layout: { view: EquationBox, dataField: 'text' }, options: { fontSize: '14px', layout_reflowHorizontal: true, layout_reflowVertical: true, layout_nativeDimEditable: true, layout_hideDecorationTitle: true, systemIcon: 'BsCalculatorFill', }, ///systemIcon: 'BsSuperscript' + BsSubscript }, ], [ DocumentType.FUNCPLOT, { layout: { view: FunctionPlotBox, dataField: defaultDataKey }, options: { layout_reflowHorizontal: true, layout_reflowVertical: true, layout_nativeDimEditable: true, }, }, ], [ DocumentType.DIAGRAM, { layout: { view: DiagramBox, dataField: defaultDataKey }, options: { _height: 300, _layout_fitWidth: true, layout_nativeDimEditable: true, layout_reflowVertical: true, waitForDoubleClickToClick: 'always', systemIcon: 'BsGlobe' }, }, ], [ DocumentType.BUTTON, { layout: { view: LabelBox, dataField: 'title' }, options: { layout_nativeDimEditable: true, layout_reflowHorizontal: true, layout_reflowVertical: true }, }, ], [ DocumentType.PRES, { layout: { view: PresBox, dataField: defaultDataKey }, options: { defaultDoubleClick: 'ignore', hideClickBehaviors: true, layout_hideLinkAnchors: true }, }, ], [ DocumentType.FONTICON, { layout: { view: FontIconBox, dataField: 'icon' }, options: { defaultDoubleClick: 'ignore', waitForDoubleClickToClick: 'never', layout_hideContextMenu: true, layout_hideLinkButton: true, _width: 40, _height: 40 }, }, ], [ DocumentType.WEBCAM, { layout: { view: RecordingBox, dataField: defaultDataKey }, options: { systemIcon: 'BsFillCameraVideoFill' }, }, ], [ DocumentType.PRESELEMENT, { layout: { view: PresElementBox, dataField: defaultDataKey }, options: { title: 'pres element template', _layout_fitWidth: true, _xMargin: 0, isTemplateDoc: true, isTemplateForField: 'data' }, }, ], [ DocumentType.CONFIG, { layout: { view: CollectionView, dataField: defaultDataKey }, options: { config: '', layout_hideLinkButton: true, layout_unrendered: true }, }, ], [ DocumentType.INK, { // NOTE: this is unused!! ink fields are filled in directly within the InkDocument() method layout: { view: InkingStroke, dataField: 'stroke' }, options: { systemIcon: 'BsFillPencilFill', // layout_nativeDimEditable: true, layout_reflowVertical: true, layout_reflowHorizontal: true, layout_hideDecorationTitle: true, // don't show title when selected fitWidth: false, layout_isSvg: true, }, }, ], [ DocumentType.SCREENSHOT, { layout: { view: ScreenshotBox, dataField: defaultDataKey }, options: { layout_nativeDimEditable: true, systemIcon: 'BsCameraFill' }, }, ], [ DocumentType.COMPARISON, { data: '', layout: { view: ComparisonBox, dataField: defaultDataKey }, options: { backgroundColor: 'gray', dropAction: dropActionType.move, waitForDoubleClickToClick: 'always', layout_reflowHorizontal: true, layout_reflowVertical: true, layout_nativeDimEditable: true, systemIcon: 'BsLayoutSplit' }, }, ], [ DocumentType.GROUPDB, { layout: { view: EmptyBox, dataField: defaultDataKey }, options: { title: 'Global Group Database' }, }, ], [ DocumentType.GROUP, { layout: { view: EmptyBox, dataField: defaultDataKey }, options: {}, }, ], [ DocumentType.DATAVIZ, { layout: { view: DataVizBox, dataField: defaultDataKey }, options: { dataViz_title: '', dataViz_line: '', dataViz_pie: '', dataViz_histogram: '', dataViz: 'table', _layout_fitWidth: true, layout_reflowHorizontal: true, layout_reflowVertical: true, layout_nativeDimEditable: true }, }, ], [ DocumentType.LOADING, { layout: { view: LoadingBox, dataField: '' }, options: { _layout_fitWidth: true, _fitHeight: true, layout_nativeDimEditable: true }, }, ], [ DocumentType.SIMULATION, { data: '', layout: { view: PhysicsSimulationBox, dataField: defaultDataKey, _width: 1000, _height: 800 }, options: { _height: 100, mass1: '', mass2: '', layout_nativeDimEditable: true, position: '', acceleration: '', pendulum: '', spring: '', wedge: '', simulation: '', review: '', systemIcon: 'BsShareFill', }, }, ], [ DocumentType.PUSHPIN, { layout: { view: MapPushpinBox, dataField: defaultDataKey }, options: {}, }, ], [ DocumentType.MAPROUTE, { layout: { view: CollectionView, dataField: defaultDataKey }, options: {}, }, ], [ DocumentType.CALENDAR, { layout: { view: CalendarBox, dataField: defaultDataKey }, options: {}, }, ], ]); const suffix = 'Proto'; /** * This function loads or initializes the prototype for each document type. * * This is an asynchronous function because it has to attempt * to fetch the prototype documents from the server. * * Once we have this object that maps the prototype ids to a potentially * undefined document, we either initialize our private prototype * variables with the document returned from the server or, if prototypes * haven't been initialized, the newly initialized prototype document. */ export async function initialize(): Promise { // non-guid string ids for each document prototype const prototypeIds = Object.values(DocumentType) .filter(type => type !== DocumentType.NONE) .map(type => type + suffix); // fetch the actual prototype documents from the server const actualProtos = await DocServer.GetRefFields(prototypeIds); // update this object to include any default values: DocumentOptions for all prototypes prototypeIds.map(id => { const existing = actualProtos[id] as Doc; const type = id.replace(suffix, '') as DocumentType; // get or create prototype of the specified type... const target = buildPrototype(type, id, existing); // ...and set it if not undefined (can be undefined only if TemplateMap does not contain // an entry dedicated to the given DocumentType) target && PrototypeMap.set(type, target); }); reaction( () => (proto => StrCast(proto?.BROADCAST_MESSAGE))(DocServer.GetCachedRefField('rtfProto') as Doc), msg => msg && alert(msg) ); } /** * Retrieves the prototype for the given document type, or * undefined if that type's proto doesn't have a configuration * in the template map. * @param type */ const PrototypeMap: PrototypeMap = new Map(); export function get(type: DocumentType): Doc { return PrototypeMap.get(type)!; } /** * A collection of all scripts in the database */ export function MainScriptDocument() { return Prototypes.get(DocumentType.SCRIPTDB); } /** * A collection of all user acl groups in the database */ export function MainGroupDocument() { return Prototypes.get(DocumentType.GROUPDB); } /** * This is a convenience method that is used to initialize * prototype documents for the first time. * * @param protoId the id of the prototype, indicating the specific prototype * to initialize (see the *protoId list at the top of the namespace) * @param title the prototype document's title, follows *-PROTO * @param layout the layout key for this prototype and thus the * layout key that all delegates will inherit * @param options any value specified in the DocumentOptions object likewise * becomes the default value for that key for all delegates */ function buildPrototype(type: DocumentType, prototypeId: string, existing?: Doc): Opt { // load template from type const template = TemplateMap.get(type); if (!template) { return undefined; } const layout = template.layout; // create title const upper = suffix.toUpperCase(); const title = prototypeId.toUpperCase().replace(upper, `_${upper}`); // synthesize the default options, the type and title from computed values and // whatever options pertain to this specific prototype const options: DocumentOptions = { isSystem: true, _layout_fieldKey: 'layout', title, type, isBaseProto: true, x: 0, y: 0, _width: 300, 'acl-Guest': SharingPermissions.View, ...(template.options || {}), layout: layout.view?.LayoutString(layout.dataField), data: template.data, }; Object.entries(options) .filter(pair => typeof pair[1] === 'string' && pair[1].startsWith('@')) .map(pair => { if (!existing || ScriptCast(existing[pair[0]])?.script.originalScript !== pair[1].substring(1)) { (options as any)[pair[0]] = ComputedField.MakeFunction(pair[1].substring(1)); } }); return Doc.assign(existing ?? new Doc(prototypeId, true), OmitKeys(options, Object.keys(existing ?? {})).omit, undefined, true); } } /** * Encapsulates the factory used to create new document instances * delegated from top-level prototypes */ export namespace Create { /** * This function receives the relevant document prototype and uses * it to create a new of that base-level prototype, or the * underlying data document, which it then delegates again * to create the view document. * * It also takes the opportunity to register the user * that created the document and the time of creation. * * @param proto the specific document prototype off of which to model * this new instance (textProto, imageProto, etc.) * @param data the Field to store at this new instance's data key * @param options any initial values to provide for this new instance * @param delegId if applicable, an existing document id. If undefined, Doc's * constructor just generates a new GUID. This is currently used * only when creating a DockDocument from the current user's already existing * main document. */ function InstanceFromProto(proto: Doc, data: Field | undefined, options: DocumentOptions, delegId?: string, fieldKey: string = 'data', protoId?: string, placeholderDoc?: Doc, noView?: boolean) { const viewKeys = ['x', 'y', 'isSystem']; // 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'] = SharingPermissions.Unset; dataProps['acl-Guest'] = options['acl-Guest'] ?? (Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.View); dataProps.isSystem = viewProps.isSystem; dataProps.isDataDoc = true; dataProps.author = Doc.CurrentUserEmail; dataProps.author_date = new DateField(); if (fieldKey) { dataProps[`${fieldKey}_modificationDate`] = new DateField(); dataProps[fieldKey] = options.data ?? data; // so that the list of annotations is already initialised, prevents issues in addonly. // without this, if a doc has no annotations but the user has AddOnly privileges, they won't be able to add an annotation because they would have needed to create the field's list which they don't have permissions to do. dataProps[fieldKey + '_annotations'] = new List(); dataProps[fieldKey + '_sidebar'] = new List(); } // users placeholderDoc as proto if it exists const dataDoc = Doc.assign(placeholderDoc ? Doc.GetProto(placeholderDoc) : Doc.MakeDelegate(proto, protoId), dataProps, undefined, true); if (placeholderDoc) { dataDoc.proto = proto; } if (!noView) { const viewFirstProps: { [id: string]: any } = { author: Doc.CurrentUserEmail }; viewFirstProps['acl-Guest'] = options['_acl-Guest'] ?? (Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.View); let viewDoc: Doc; // determines whether viewDoc should be created using placeholder Doc or default if (placeholderDoc) { placeholderDoc._height = options._height !== undefined ? Number(options._height) : undefined; placeholderDoc._width = options._width !== undefined ? Number(options._width) : undefined; viewDoc = Doc.assign(placeholderDoc, viewFirstProps, true, true); Array.from(Object.keys(placeholderDoc)) .filter(key => key.startsWith('acl')) .forEach(key => (dataDoc[key] = viewDoc[key] = placeholderDoc[key])); } else { viewDoc = Doc.assign(Doc.MakeDelegate(dataDoc, delegId), viewFirstProps, true, true); } Doc.assign(viewDoc, viewProps, true, true); if (![DocumentType.LINK, DocumentType.CONFIG, DocumentType.LABEL].includes(viewDoc.type as any)) { DocUtils.MakeLinkToActiveAudio(() => viewDoc); } updateCachedAcls(dataDoc); updateCachedAcls(viewDoc); return viewDoc; } updateCachedAcls(dataDoc); return dataDoc; } export function ImageDocument(url: string | ImageField, options: DocumentOptions = {}, overwriteDoc?: Doc) { const imgField = url instanceof ImageField ? url : new ImageField(url); return InstanceFromProto(Prototypes.get(DocumentType.IMG), imgField, { title: basename(imgField.url.href), ...options }, undefined, undefined, undefined, overwriteDoc); } export function PresDocument(options: DocumentOptions = {}) { return InstanceFromProto(Prototypes.get(DocumentType.PRES), new List(), options); } /** * Creates a Doc to edit a script and write the compiled script into the specified field. * Typically, this would be used to create a template that can then be applied to some other Doc * in order to customize a behavior, such as onClick. * @param script * @param options * @param fieldKey the field that the compiled script is written into. * @returns the Scripting Doc */ export function ScriptingDocument(script: Opt | null, options: DocumentOptions = {}, fieldKey?: string) { return InstanceFromProto(Prototypes.get(DocumentType.SCRIPTING), script ? script : undefined, { ...options, layout: fieldKey ? ScriptingBox.LayoutString(fieldKey) : undefined }); } export function VideoDocument(url: string, options: DocumentOptions = {}, overwriteDoc?: Doc) { return InstanceFromProto(Prototypes.get(DocumentType.VID), new VideoField(url), options, undefined, undefined, undefined, overwriteDoc); } export function YoutubeDocument(url: string, options: DocumentOptions = {}, overwriteDoc?: Doc) { return InstanceFromProto(Prototypes.get(DocumentType.YOUTUBE), new YoutubeField(url), options, undefined, undefined, undefined, overwriteDoc); } export function WebCamDocument(url: string, options: DocumentOptions = {}) { return InstanceFromProto(Prototypes.get(DocumentType.WEBCAM), '', options); } export function ScreenshotDocument(options: DocumentOptions = {}) { return InstanceFromProto(Prototypes.get(DocumentType.SCREENSHOT), '', options); } export function ComparisonDocument(options: DocumentOptions = { title: 'Comparison Box' }) { return InstanceFromProto(Prototypes.get(DocumentType.COMPARISON), undefined, options); } export function DiagramDocument(options: DocumentOptions = { title: 'bruh box' }) { return InstanceFromProto(Prototypes.get(DocumentType.DIAGRAM), undefined, options); } export function AudioDocument(url: string, options: DocumentOptions = {}, overwriteDoc?: Doc) { return InstanceFromProto(Prototypes.get(DocumentType.AUDIO), new AudioField(url), options, undefined, undefined, undefined, overwriteDoc); } export function RecordingDocument(url: string, options: DocumentOptions = {}) { return InstanceFromProto(Prototypes.get(DocumentType.REC), '', options); } export function SearchDocument(options: DocumentOptions = {}) { return InstanceFromProto(Prototypes.get(DocumentType.SEARCH), new List([]), options); } export function LoadingDocument(file: File | string, options: DocumentOptions) { return InstanceFromProto(Prototypes.get(DocumentType.LOADING), undefined, { _height: 150, _width: 200, title: typeof file == 'string' ? file : file.name, ...options }, undefined, ''); } export function RTFDocument(field: RichTextField, options: DocumentOptions = {}, fieldKey: string = 'text') { return InstanceFromProto(Prototypes.get(DocumentType.RTF), field, options, undefined, fieldKey); } export function TextDocument(text: string, options: DocumentOptions = {}, fieldKey: string = 'text') { const rtf = { doc: { type: 'doc', content: [ { type: 'paragraph', content: [ { type: 'text', text, }, ], }, ], }, selection: { type: 'text', anchor: 1, head: 1 }, storedMarks: [], }; const field = text ? new RichTextField(JSON.stringify(rtf), text) : undefined; return InstanceFromProto(Prototypes.get(DocumentType.RTF), field, options, undefined, fieldKey); } export function LinkDocument(source: Doc, target: Doc, options: DocumentOptions = {}, id?: string) { const linkDoc = InstanceFromProto( Prototypes.get(DocumentType.LINK), undefined, { link_anchor_1: source, link_anchor_2: target, ...options, }, id, 'link' ); LinkManager.Instance.addLink(linkDoc); return linkDoc; } export function InkDocument( points: PointData[], options: DocumentOptions = {}, strokeWidth = ActiveInkWidth(), color = ActiveInkColor(), stroke_bezier = ActiveInkBezierApprox(), fillColor = ActiveFillColor(), arrowStart = ActiveArrowStart(), arrowEnd = ActiveArrowEnd(), dash = ActiveDash(), isInkMask = ActiveIsInkMask() ) { const ink = InstanceFromProto(Prototypes.get(DocumentType.INK), '', { title: 'ink', ...options }); const I = Doc.GetProto(ink); // I.layout_hideOpenButton = true; // don't show open full screen button when selected I.color = color; I.fillColor = fillColor; I.stroke = new InkField(points); I.stroke_width = strokeWidth; I.stroke_bezier = stroke_bezier; I.stroke_startMarker = arrowStart; I.stroke_endMarker = arrowEnd; I.stroke_dash = dash; I.stroke_isInkMask = isInkMask; I.text_align = 'center'; I.rotation = 0; I.defaultDoubleClick = 'ignore'; I.author_date = new DateField(); I['acl-Guest'] = Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.View; //I['acl-Override'] = SharingPermissions.Unset; I[Initializing] = false; return ink; } export function PdfDocument(url: string, options: DocumentOptions = {}, overwriteDoc?: Doc) { const width = options._width || undefined; const height = options._height || undefined; const nwid = options._nativeWidth || undefined; const nhght = options._nativeHeight || undefined; if (!nhght && width && height && nwid) options._nativeHeight = (Number(nwid) * Number(height)) / Number(width); return InstanceFromProto(Prototypes.get(DocumentType.PDF), new PdfField(url), options, undefined, undefined, undefined, overwriteDoc); } export function WebDocument(url: string, options: DocumentOptions = {}) { const width = options._width || undefined; const height = options._height || undefined; const nwid = options._nativeWidth || undefined; const nhght = options._nativeHeight || undefined; if (!nhght && width && height && nwid) options._nativeHeight = (Number(nwid) * Number(height)) / Number(width); return InstanceFromProto(Prototypes.get(DocumentType.WEB), new WebField(url ? url : 'https://www.wikipedia.org/'), options); } export function HtmlDocument(html: string, options: DocumentOptions = {}) { return InstanceFromProto(Prototypes.get(DocumentType.WEB), new HtmlField(html), options); } export function MapDocument(documents: Array, options: DocumentOptions = {}) { return InstanceFromProto(Prototypes.get(DocumentType.MAP), new List(documents), options); } export function PushpinDocument(latitude: number, longitude: number, infoWindowOpen: boolean, documents: Array, options: DocumentOptions, id?: string) { return InstanceFromProto(Prototypes.get(DocumentType.PUSHPIN), new List(documents), { latitude, longitude, infoWindowOpen, ...options }, id); } export function MapRouteDocument(infoWindowOpen: boolean, documents: Array, options: DocumentOptions, id?: string) { return InstanceFromProto(Prototypes.get(DocumentType.MAPROUTE), new List(documents), { infoWindowOpen, ...options }, id); } export function CalendarDocument(options: DocumentOptions, documents: Array) { return InstanceFromProto(Prototypes.get(DocumentType.CALENDAR), new List(documents), { ...options }); } // shouldn't ever need to create a KVP document-- instead set the LayoutTemplateString to be a KeyValueBox for the DocumentView (see addDocTab in TabDocView) // export function KVPDocument(document: Doc, options: DocumentOptions = {}) { // return InstanceFromProto(Prototypes.get(DocumentType.KVP), document, { title: document.title + '.kvp', ...options }); // } export function FreeformDocument(documents: Array, options: DocumentOptions, id?: string) { const inst = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Freeform }, id); documents.forEach(d => Doc.SetContainer(d, inst)); return inst; } export function ConfigDocument(options: DocumentOptions, id?: string) { return InstanceFromProto(Prototypes.get(DocumentType.CONFIG), options?.data, options, id, '', undefined, undefined, true); } export function HTMLMarkerDocument(documents: Array, options: DocumentOptions, id?: string) { return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Freeform }, id); } export function PileDocument(documents: Array, options: DocumentOptions, id?: string) { return InstanceFromProto( Prototypes.get(DocumentType.COL), new List(documents), { backgroundColor: 'transparent', dropAction: dropActionType.move, _forceActive: true, _freeform_noZoom: true, _freeform_noAutoPan: true, ...options, _type_collection: CollectionViewType.Pile }, id ); } export function LinearDocument(documents: Array, options: DocumentOptions, id?: string) { return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Linear }, id); } export function MapCollectionDocument(documents: Array, options: DocumentOptions = {}) { return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Map }); } export function CarouselDocument(documents: Array, options: DocumentOptions) { return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Carousel }); } export function Carousel3DDocument(documents: Array, options: DocumentOptions) { return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Carousel3D }); } export function SchemaDocument(schemaHeaders: SchemaHeaderField[], documents: Array, options: DocumentOptions) { return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { schemaHeaders: new List(schemaHeaders), ...options, _type_collection: CollectionViewType.Schema }); } export function TreeDocument(documents: Array, options: DocumentOptions, id?: string, protoId?: string) { const doc = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _xMargin: 5, _yMargin: 5, ...options, _type_collection: CollectionViewType.Tree }, id, undefined, protoId); Doc.GetProto(doc).treeView = ''; /// not really needed, but makes keyvalue pane look better return doc; } export function CalendarCollectionDocument(documents: Array, options: DocumentOptions) { return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Calendar }); } export function StackingDocument(documents: Array, options: DocumentOptions, id?: string, protoId?: string) { return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Stacking }, id, undefined, protoId); } export function NoteTakingDocument(documents: Array, options: DocumentOptions, id?: string, protoId?: string) { return InstanceFromProto( Prototypes.get(DocumentType.COL), new List(documents), { columnHeaders: new List([new SchemaHeaderField('Untitled')]), ...options, _type_collection: CollectionViewType.NoteTaking }, id, undefined, protoId ); } export function MulticolumnDocument(documents: Array, options: DocumentOptions) { return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Multicolumn }); } export function MultirowDocument(documents: Array, options: DocumentOptions) { return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Multirow }); } export function MasonryDocument(documents: Array, options: DocumentOptions) { return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Masonry }); } export function LabelDocument(options?: DocumentOptions) { return InstanceFromProto(Prototypes.get(DocumentType.LABEL), undefined, { ...(options || {}) }); } export function EquationDocument(text?: string, options?: DocumentOptions) { return InstanceFromProto(Prototypes.get(DocumentType.EQUATION), text, { ...(options || {}) }, undefined, 'text'); } export function FunctionPlotDocument(documents: Array, options?: DocumentOptions) { return InstanceFromProto(Prototypes.get(DocumentType.FUNCPLOT), new List(documents), { title: 'func plot', ...(options || {}) }); } export function ButtonDocument(options?: DocumentOptions) { return InstanceFromProto(Prototypes.get(DocumentType.BUTTON), undefined, { ...(options || {}) }); } export function FontIconDocument(options?: DocumentOptions) { return InstanceFromProto(Prototypes.get(DocumentType.FONTICON), undefined, { ...(options || {}) }); } export function PresElementBoxDocument() { return Prototypes.get(DocumentType.PRESELEMENT); } export function DataVizDocument(url: string, options?: DocumentOptions, overwriteDoc?: Doc) { return InstanceFromProto(Prototypes.get(DocumentType.DATAVIZ), new CsvField(url), { title: 'Data Viz', type: 'dataviz', ...options }, undefined, undefined, undefined, overwriteDoc); } export function DockDocument(documents: Array, config: string, options: DocumentOptions, id?: string) { const ret = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { treeView_FreezeChildren: 'remove|add', ...options, type_collection: CollectionViewType.Docking, dockingConfig: config }, id); documents.map(c => Doc.SetContainer(c, ret)); return ret; } export type DocConfig = { doc: Doc; initialWidth?: number; path?: Doc[]; }; export function StandardCollectionDockingDocument(configs: Array, options: DocumentOptions, id?: string, type: string = 'row') { const layoutConfig = { content: [ { type: type, content: [...configs.map(config => CollectionDockingView.makeDocumentConfig(config.doc, undefined, config.initialWidth))], }, ], }; const doc = DockDocument( configs.map(c => c.doc), JSON.stringify(layoutConfig), Doc.CurrentUserEmail === 'guest' ? options : { 'acl-Guest': SharingPermissions.View, ...options }, id ); configs.map(c => { Doc.SetContainer(c.doc, doc); inheritParentAcls(doc, c.doc, false); }); return doc; } export function DelegateDocument(proto: Doc, options: DocumentOptions = {}) { return InstanceFromProto(proto, undefined, options); } export function SimulationDocument(options?: DocumentOptions) { return InstanceFromProto(Prototypes.get(DocumentType.SIMULATION), undefined, { ...(options || {}) }); } } } export namespace DocUtils { function matchFieldValue(doc: Doc, key: string, value: any): boolean { const hasFunctionFilter = Utils.HasFunctionFilter(value); if (hasFunctionFilter) { return hasFunctionFilter(StrCast(doc[key])); } if (key === LinkedTo) { // links are not a field value, so handled here. value is an expression of form ([field=]idToDoc("...")) const allLinks = LinkManager.Instance.getAllRelatedLinks(doc); const matchLink = (value: string, anchor: Doc) => { const linkedToExp = (value ?? '').split('='); if (linkedToExp.length === 1) return Field.toScriptString(anchor) === value; return Field.toScriptString(DocCast(anchor[linkedToExp[0]])) === linkedToExp[1]; }; // prettier-ignore return (value === Doc.FilterNone && !allLinks.length) || (value === Doc.FilterAny && !!allLinks.length) || (allLinks.some(link => matchLink(value,DocCast(link.link_anchor_1)) || matchLink(value,DocCast(link.link_anchor_2)) )); } if (typeof value === 'string') { value = value.replace(`,${Utils.noRecursionHack}`, ''); } const fieldVal = doc[key]; // prettier-ignore if ((value === Doc.FilterAny && fieldVal !== undefined) || (value === Doc.FilterNone && fieldVal === undefined)) { return true; } const vals = StrListCast(fieldVal); // list typing is very imperfect. casting to a string list doesn't mean that the entries will actually be strings if (vals.length) { return vals.some(v => typeof v === 'string' && v.includes(value)); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring } return Field.toString(fieldVal as Field).includes(value); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring } /** * @param docs * @param childFilters * @param childFiltersByRanges * @param parentCollection * Given a list of docs and childFilters, @returns the list of Docs that match those filters */ export function FilterDocs(childDocs: Doc[], childFilters: string[], childFiltersByRanges: string[], parentCollection?: Doc) { if (!childFilters?.length && !childFiltersByRanges?.length) { return childDocs.filter(d => !d.cookies); // remove documents that need a cookie if there are no filters to provide one } const filterFacets: { [key: string]: { [value: string]: string } } = {}; // maps each filter key to an object with value=>modifier fields childFilters.forEach(filter => { const fields = filter.split(Doc.FilterSep); const key = fields[0]; const value = fields[1]; const modifiers = fields[2]; if (!filterFacets[key]) { filterFacets[key] = {}; } filterFacets[key][value] = modifiers; }); const filteredDocs = childFilters.length ? childDocs.filter(d => { if (d.z) return true; // if the document needs a cookie but no filter provides the cookie, then the document does not pass the filter if (d.cookies && (!filterFacets.cookies || !Object.keys(filterFacets.cookies).some(key => d.cookies === key))) { return false; } for (const facetKey of Object.keys(filterFacets).filter(fkey => fkey !== 'cookies' && fkey !== Utils.noDragDocsFilter.split(Doc.FilterSep)[0])) { const facet = filterFacets[facetKey]; // facets that match some value in the field of the document (e.g. some text field) const matches = Object.keys(facet).filter(value => value !== 'cookies' && facet[value] === 'match'); // facets that have a check next to them const checks = Object.keys(facet).filter(value => facet[value] === 'check'); // metadata facets that exist const exists = Object.keys(facet).filter(value => facet[value] === 'exists'); // facets that unset metadata (a hack for making cookies work) const unsets = Object.keys(facet).filter(value => facet[value] === 'unset'); // facets that specify that a field must not match a specific value const xs = Object.keys(facet).filter(value => facet[value] === 'x'); if (!unsets.length && !exists.length && !xs.length && !checks.length && !matches.length) return true; const failsNotEqualFacets = !xs.length ? false : xs.some(value => matchFieldValue(d, facetKey, value)); const satisfiesCheckFacets = !checks.length ? true : checks.some(value => matchFieldValue(d, facetKey, value)); const satisfiesExistsFacets = !exists.length ? true : exists.some(value => (facetKey !== LinkedTo ? d[facetKey] !== undefined : LinkManager.Instance.getAllRelatedLinks(d).length)); const satisfiesUnsetsFacets = !unsets.length ? true : unsets.some(value => d[facetKey] === undefined); const satisfiesMatchFacets = !matches.length ? true : matches.some(value => { if (facetKey.startsWith('*')) { // fields starting with a '*' are used to match families of related fields. ie, *modificationDate will match text_modificationDate, data_modificationDate, etc const allKeys = Array.from(Object.keys(d)); allKeys.push(...Object.keys(Doc.GetProto(d))); const keys = allKeys.filter(key => key.includes(facetKey.substring(1))); return keys.some(key => Field.toString(d[key] as Field).includes(value)); } return Field.toString(d[facetKey] as Field).includes(value); }); // if we're ORing them together, the default return is false, and we return true for a doc if it satisfies any one set of criteria if (parentCollection?.childFilters_boolean === 'OR') { if (satisfiesUnsetsFacets && satisfiesExistsFacets && satisfiesCheckFacets && !failsNotEqualFacets && satisfiesMatchFacets) return true; } // if we're ANDing them together, the default return is true, and we return false for a doc if it doesn't satisfy any set of criteria else { if (!satisfiesUnsetsFacets || !satisfiesExistsFacets || !satisfiesCheckFacets || failsNotEqualFacets || (matches.length && !satisfiesMatchFacets)) return false; } } return parentCollection?.childFilters_boolean === 'OR' ? false : true; }) : childDocs; const rangeFilteredDocs = filteredDocs.filter(d => { for (let i = 0; i < childFiltersByRanges.length; i += 3) { const key = childFiltersByRanges[i]; const min = Number(childFiltersByRanges[i + 1]); const max = Number(childFiltersByRanges[i + 2]); const val = typeof d[key] === 'string' ? (Number(StrCast(d[key])).toString() === StrCast(d[key]) ? Number(StrCast(d[key])) : undefined) : Cast(d[key], 'number', null); if (val === undefined) { //console.log("Should 'undefined' pass range filter or not?") } else if (val < min || val > max) return false; } return true; }); return rangeFilteredDocs; } export let ActiveRecordings: { props: FieldViewProps; getAnchor: (addAsAnnotation: boolean) => Doc }[] = []; export function MakeLinkToActiveAudio(getSourceDoc: () => Doc | undefined, broadcastEvent = true) { broadcastEvent && runInAction(() => (Doc.RecordingEvent = Doc.RecordingEvent + 1)); return DocUtils.ActiveRecordings.map(audio => { const sourceDoc = getSourceDoc(); return sourceDoc && DocUtils.MakeLink(sourceDoc, audio.getAnchor(true) || audio.props.Document, { link_displayLine: false, link_relationship: 'recording annotation:linked recording', link_description: 'recording timeline' }); }); } export function MakeLink(source: Doc, target: Doc, linkSettings: { link_relationship?: string; link_description?: string; link_displayLine?: boolean }, id?: string, showPopup?: number[]) { if (!linkSettings.link_relationship) linkSettings.link_relationship = target.type === DocumentType.RTF ? 'Commentary:Comments On' : 'link'; if (target.doc === Doc.UserDoc()) return undefined; const makeLink = action((linkDoc: Doc, showPopup?: number[]) => { if (showPopup) { LinkManager.Instance.currentLink = linkDoc; TaskCompletionBox.textDisplayed = 'Link Created'; TaskCompletionBox.popupX = showPopup[0]; TaskCompletionBox.popupY = showPopup[1] - 33; TaskCompletionBox.taskCompleted = true; LinkDescriptionPopup.Instance.popupX = showPopup[0]; LinkDescriptionPopup.Instance.popupY = showPopup[1]; LinkDescriptionPopup.Instance.display = true; const rect = document.body.getBoundingClientRect(); if (LinkDescriptionPopup.Instance.popupX + 200 > rect.width) { LinkDescriptionPopup.Instance.popupX -= 190; TaskCompletionBox.popupX -= 40; } if (LinkDescriptionPopup.Instance.popupY + 100 > rect.height) { LinkDescriptionPopup.Instance.popupY -= 40; TaskCompletionBox.popupY -= 40; } setTimeout( action(() => (TaskCompletionBox.taskCompleted = false)), 2500 ); } return linkDoc; }); return makeLink( Docs.Create.LinkDocument( source, target, { 'acl-Guest': SharingPermissions.Augment, '_acl-Guest': SharingPermissions.Augment, title: ComputedField.MakeFunction('generateLinkTitle(this)') as any, link_anchor_1_useSmallAnchor: source.useSmallAnchor ? true : undefined, link_anchor_2_useSmallAnchor: target.useSmallAnchor ? true : undefined, link_displayLine: linkSettings.link_displayLine, link_relationship: linkSettings.link_relationship, link_description: linkSettings.link_description, link_autoMoveAnchors: true, _layout_showCaption: '', // removed since they conflict with showing a link with a LinkBox (ie, line, not comparison box) _layout_showTitle: '', // _layout_showCaption: 'link_description', // _layout_showTitle: 'link_relationship', }, id ), showPopup ); } export function AssignScripts(doc: Doc, scripts?: { [key: string]: string | undefined }, funcs?: { [key: string]: string }) { scripts && Object.keys(scripts).map(key => { const script = scripts[key]; if (ScriptCast(doc[key])?.script.originalScript !== scripts[key] && script) { (key.startsWith('_') ? doc : Doc.GetProto(doc))[key] = ScriptField.MakeScript(script, { self: Doc.name, this: Doc.name, dragData: DragManager.DocumentDragData.name, value: 'any', _readOnly_: 'boolean', scriptContext: 'any', documentView: Doc.name, heading: Doc.name, checked: 'boolean', containingTreeView: Doc.name, altKey: 'boolean', ctrlKey: 'boolean', shiftKey: 'boolean', }); } }); funcs && Object.keys(funcs) .filter(key => !key.endsWith('-setter')) .map(key => { const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); if (ScriptCast(cfield)?.script.originalScript !== funcs[key]) { const setFunc = Cast(funcs[key + '-setter'], 'string', null); (key.startsWith('_') ? doc : Doc.GetProto(doc))[key] = funcs[key] ? ComputedField.MakeFunction(funcs[key], { dragData: DragManager.DocumentDragData.name }, { _readOnly_: true }, setFunc) : undefined; } }); return doc; } export function AssignOpts(doc: Doc | undefined, reqdOpts: DocumentOptions, items?: Doc[]) { if (doc) { const compareValues = (val1: any, val2: any) => { if (val1 instanceof List && val2 instanceof List && val1.length === val2.length) { return !val1.some(v => !val2.includes(v)) || !val2.some(v => val1.includes(v)); } return val1 === val2; }; Object.entries(reqdOpts).forEach(pair => { const targetDoc = pair[0].startsWith('_') ? doc : Doc.GetProto(doc as Doc); if (!Object.getOwnPropertyNames(targetDoc).includes(pair[0].replace(/^_/, '')) || !compareValues(pair[1], targetDoc[pair[0]])) { targetDoc[pair[0]] = pair[1]; } }); items?.forEach(item => !DocListCast(doc.data).includes(item) && Doc.AddDocToList(Doc.GetProto(doc), 'data', item)); items && DocListCast(doc.data).forEach(item => !items.includes(item) && Doc.RemoveDocFromList(Doc.GetProto(doc), 'data', item)); } return doc; } export function AssignDocField(doc: Doc, field: string, creator: (reqdOpts: DocumentOptions, items?: Doc[]) => Doc, reqdOpts: DocumentOptions, items?: Doc[], scripts?: { [key: string]: string }, funcs?: { [key: string]: string }) { return DocUtils.AssignScripts(DocUtils.AssignOpts(DocCast(doc[field]), reqdOpts, items) ?? (doc[field] = creator(reqdOpts, items)), scripts, funcs); } export function DocumentFromField(target: Doc, fieldKey: string, proto?: Doc, options?: DocumentOptions): Doc | undefined { let created: Doc | undefined; const field = target[fieldKey]; const resolved = options ?? {}; if (field instanceof ImageField) { created = Docs.Create.ImageDocument(field.url.href, resolved); created.layout = ImageBox.LayoutString(fieldKey); } else if (field instanceof Doc) { created = field; } else if (field instanceof VideoField) { created = Docs.Create.VideoDocument(field.url.href, resolved); created.layout = VideoBox.LayoutString(fieldKey); } else if (field instanceof PdfField) { created = Docs.Create.PdfDocument(field.url.href, resolved); created.layout = PDFBox.LayoutString(fieldKey); } else if (field instanceof AudioField) { created = Docs.Create.AudioDocument(field.url.href, resolved); created.layout = AudioBox.LayoutString(fieldKey); } else if (field instanceof InkField) { created = Docs.Create.InkDocument(field.inkData, resolved); created.layout = InkingStroke.LayoutString(fieldKey); } else if (field instanceof List && field[0] instanceof Doc) { created = Docs.Create.StackingDocument(DocListCast(field), resolved); created.layout = CollectionView.LayoutString(fieldKey); } else { created = Docs.Create.TextDocument('', { ...{ _width: 200, _height: 25, _layout_autoHeight: true }, ...resolved }); created.layout = FormattedTextBox.LayoutString(fieldKey); } if (created) { created.title = fieldKey; proto && created.proto && (created.proto = Doc.GetProto(proto)); } return created; } /** * * @param type the type of file. * @param path the path to the file. * @param options the document options. * @param overwriteDoc the placeholder loading doc. * @returns */ export async function DocumentFromType(type: string, path: string, options: DocumentOptions, overwriteDoc?: Doc): Promise> { let ctor: ((path: string, options: DocumentOptions, overwriteDoc?: Doc) => Doc | Promise) | undefined = undefined; if (type.indexOf('image') !== -1) { ctor = Docs.Create.ImageDocument; if (!options._width) options._width = 300; } if (type.indexOf('video') !== -1) { ctor = Docs.Create.VideoDocument; if (!options._width) options._width = 600; if (!options._height) options._height = ((options._width as number) * 2) / 3; } if (type.indexOf('audio') !== -1) { ctor = Docs.Create.AudioDocument; } if (type.indexOf('pdf') !== -1) { ctor = Docs.Create.PdfDocument; if (!options._width) options._width = 400; if (!options._height) options._height = ((options._width as number) * 1200) / 927; } if (type.indexOf('csv') !== -1) { ctor = Docs.Create.DataVizDocument; if (!options._width) options._width = 400; if (!options._height) options._height = ((options._width as number) * 1200) / 927; } //TODO:al+glr // if (type.indexOf("map") !== -1) { // ctor = Docs.Create.MapDocument; // if (!options._width) options._width = 800; // if (!options._height) options._height = (options._width as number) * 3 / 4; // } if (type.indexOf('html') !== -1) { if (path.includes(window.location.hostname)) { const s = path.split('/'); const id = s[s.length - 1]; return DocServer.GetRefField(id).then(field => { if (field instanceof Doc) { const embedding = Doc.MakeEmbedding(field); embedding.x = (options.x as number) || 0; embedding.y = (options.y as number) || 0; embedding._width = (options._width as number) || 300; embedding._height = (options._height as number) || (options._width as number) || 300; return embedding; } return undefined; }); } ctor = Docs.Create.WebDocument; options = { ...options, _width: 400, _height: 512, title: path }; } return ctor ? ctor(path, overwriteDoc ? { ...options, title: StrCast(overwriteDoc.title, path) } : options, overwriteDoc) : undefined; } export function addDocumentCreatorMenuItems(docTextAdder: (d: Doc) => void, docAdder: (d: Doc) => void, x: number, y: number, simpleMenu: boolean = false, pivotField?: string, pivotValue?: string): void { const documentList: ContextMenuProps[] = DocListCast(DocListCast(Doc.MyTools?.data)[0]?.data) .filter(btnDoc => !btnDoc.hidden) .map(btnDoc => Cast(btnDoc?.dragFactory, Doc, null)) .filter(doc => doc && doc !== Doc.UserDoc().emptyTrail && doc.title) .map((dragDoc, i) => ({ description: ':' + StrCast(dragDoc.title).replace('Untitled ', ''), event: undoable((args: { x: number; y: number }) => { const newDoc = DocUtils.copyDragFactory(dragDoc); if (newDoc) { newDoc.author = Doc.CurrentUserEmail; newDoc.x = x; newDoc.y = y; EquationBox.SelectOnLoad = newDoc[Id]; if (newDoc.type === DocumentType.RTF) FormattedTextBox.SetSelectOnLoad(newDoc); if (pivotField) { newDoc[pivotField] = pivotValue; } docAdder?.(newDoc); } }, StrCast(dragDoc.title)), icon: Doc.toIcon(dragDoc), })) as ContextMenuProps[]; ContextMenu.Instance.addItem({ description: 'Create document', subitems: documentList, icon: 'file', }); !simpleMenu && ContextMenu.Instance.addItem({ description: 'Styled Notes', subitems: DocListCast((Doc.UserDoc().template_notes as Doc).data).map((note, i) => ({ description: ':' + StrCast(note.title), event: undoable((args: { x: number; y: number }) => { const textDoc = Docs.Create.TextDocument('', { _width: 200, x, y, _layout_autoHeight: note._layout_autoHeight !== false, title: StrCast(note.title) + '#' + (note.embeddingCount = NumCast(note.embeddingCount) + 1), }); textDoc.layout_fieldKey = 'layout_' + note.title; textDoc[textDoc.layout_fieldKey] = note; if (pivotField) { textDoc[pivotField] = pivotValue; } docTextAdder(textDoc); }, 'create quick note'), icon: StrCast(note.icon) as IconProp, })) as ContextMenuProps[], icon: 'sticky-note', }); const userDocList: ContextMenuProps[] = DocListCast(DocListCast(Doc.MyTools?.data)[1]?.data) .filter(btnDoc => !btnDoc.hidden) .map(btnDoc => Cast(btnDoc?.dragFactory, Doc, null)) .filter(doc => doc && doc !== Doc.UserDoc().emptyTrail && doc !== Doc.UserDoc().emptyNote && doc.title) .map((dragDoc, i) => ({ description: ':' + StrCast(dragDoc.title).replace('Untitled ', ''), event: undoable((args: { x: number; y: number }) => { const newDoc = DocUtils.delegateDragFactory(dragDoc); if (newDoc) { newDoc.author = Doc.CurrentUserEmail; newDoc.x = x; newDoc.y = y; EquationBox.SelectOnLoad = newDoc[Id]; if (newDoc.type === DocumentType.RTF) FormattedTextBox.SetSelectOnLoad(newDoc); if (pivotField) { newDoc[pivotField] = pivotValue; } docAdder?.(newDoc); } }, StrCast(dragDoc.title)), icon: Doc.toIcon(dragDoc), })) as ContextMenuProps[]; ContextMenu.Instance.addItem({ description: 'User Templates', subitems: userDocList, icon: 'file', }); } // applies a custom template to a document. the template is identified by it's short name (e.g, slideView not layout_slideView) export function makeCustomViewClicked(doc: Doc, creator: Opt<(documents: Array, options: DocumentOptions, id?: string) => Doc>, templateSignature: string = 'custom', docLayoutTemplate?: Doc) { const batch = UndoManager.StartBatch('makeCustomViewClicked'); runInAction(() => { doc.layout_fieldKey = 'layout_' + templateSignature; createCustomView(doc, creator, templateSignature, docLayoutTemplate); }); batch.end(); return doc; } export function findTemplate(templateName: string, type: string, signature: string) { let docLayoutTemplate: Opt; 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()['template_clickFuncs'], Doc, null)?.data); const allTemplates = iconViews .concat(templBtns) .concat(noteTypes) .concat(clickFuncs) .map(btnDoc => (btnDoc.dragFactory as Doc) || btnDoc) .filter(doc => doc.isTemplateDoc); // bcz: this is hacky -- want to have different templates be applied depending on the "type" of a document. but type is not reliable and there could be other types of template searches so this should be generalized // first try to find a template that matches the specific document type (_). otherwise, fallback to a general match on !docLayoutTemplate && allTemplates.forEach(tempDoc => StrCast(tempDoc.title) === templateName + '_' + type && (docLayoutTemplate = tempDoc)); !docLayoutTemplate && allTemplates.forEach(tempDoc => StrCast(tempDoc.title) === templateName && (docLayoutTemplate = tempDoc)); return docLayoutTemplate; } export function createCustomView(doc: Doc, creator: Opt<(documents: Array, options: DocumentOptions, id?: string) => Doc>, templateSignature: string = 'custom', docLayoutTemplate?: Doc) { const templateName = templateSignature.replace(/\(.*\)/, ''); docLayoutTemplate = docLayoutTemplate || findTemplate(templateName, StrCast(doc.isGroup && doc.transcription ? 'transcription' : doc.type), templateSignature); const customName = 'layout_' + templateSignature; const _width = NumCast(doc._width); const _height = NumCast(doc._height); const options = { title: 'data', backgroundColor: StrCast(doc.backgroundColor), _layout_autoHeight: true, _width, x: -_width / 2, y: -_height / 2, _layout_showSidebar: false }; if (docLayoutTemplate) { if (docLayoutTemplate !== doc[customName]) { Doc.ApplyTemplateTo(docLayoutTemplate, doc, customName, undefined); } } else { let fieldTemplate: Opt; if (doc.data instanceof RichTextField || typeof doc.data === 'string') { fieldTemplate = Docs.Create.TextDocument('', options); } else if (doc.data instanceof PdfField) { fieldTemplate = Docs.Create.PdfDocument('http://www.msn.com', options); } else if (doc.data instanceof VideoField) { fieldTemplate = Docs.Create.VideoDocument('http://www.cs.brown.edu', options); } else if (doc.data instanceof AudioField) { fieldTemplate = Docs.Create.AudioDocument('http://www.cs.brown.edu', options); } else if (doc.data instanceof ImageField) { fieldTemplate = Docs.Create.ImageDocument('http://www.cs.brown.edu', options); } const docTemplate = creator?.(fieldTemplate ? [fieldTemplate] : [], { title: customName + '(' + doc.title + ')', isTemplateDoc: true, _width: _width + 20, _height: Math.max(100, _height + 45) }); fieldTemplate && Doc.MakeMetadataFieldTemplate(fieldTemplate, docTemplate ? Doc.GetProto(docTemplate) : docTemplate); docTemplate && Doc.ApplyTemplateTo(docTemplate, doc, customName, undefined); } } export function makeCustomView(doc: Doc, custom: boolean, layout: string) { Doc.setNativeView(doc); if (custom) { makeCustomViewClicked(doc, Docs.Create.StackingDocument, layout, undefined); } } export function iconify(doc: Doc) { const layout_fieldKey = Cast(doc.layout_fieldKey, 'string', null); DocUtils.makeCustomViewClicked(doc, Docs.Create.StackingDocument, 'icon', undefined); if (layout_fieldKey && layout_fieldKey !== 'layout' && layout_fieldKey !== 'layout_icon') doc.deiconifyLayout = layout_fieldKey.replace('layout_', ''); } export function pileup(docList: Doc[], x?: number, y?: number, size: number = 55, create: boolean = true) { runInAction(() => { docList.forEach((d, i) => { DocUtils.iconify(d); d.x = Math.cos((Math.PI * 2 * i) / docList.length) * size - size; d.y = Math.sin((Math.PI * 2 * i) / docList.length) * size - size; d._timecodeToShow = undefined; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection }); }); if (create) { const newCollection = Docs.Create.PileDocument(docList, { title: 'pileup', _freeform_noZoom: true, x: (x || 0) - size, y: (y || 0) - size, _width: size * 2, _height: size * 2, dragWhenActive: true, _layout_fitWidth: false }); newCollection.x = NumCast(newCollection.x) + NumCast(newCollection._width) / 2 - size; newCollection.y = NumCast(newCollection.y) + NumCast(newCollection._height) / 2 - size; newCollection._width = newCollection._height = size * 2; return newCollection; } } export function makeIntoPortal(doc: Doc, layoutDoc: Doc, allLinks: Doc[]) { const portalLink = allLinks.find(d => d.link_anchor_1 === doc && d.link_relationship === 'portal to:portal from'); if (!portalLink) { DocUtils.MakeLink( doc, Docs.Create.FreeformDocument([], { _width: NumCast(layoutDoc._width) + 10, _height: Math.max(NumCast(layoutDoc._height), NumCast(layoutDoc._width) + 10), _isLightbox: true, _layout_fitWidth: true, title: StrCast(doc.title) + ' [Portal]', }), { link_relationship: 'portal to:portal from' } ); } doc.followLinkLocation = OpenWhere.lightbox; doc.onClick = FollowLinkScript(); } export function LeavePushpin(doc: Doc, annotationField: string) { if (doc.followLinkToggle) return undefined; const context = Cast(doc.embedContainer, Doc, null) ?? Cast(doc.annotationOn, Doc, null); const hasContextAnchor = LinkManager.Links(doc).some(l => (l.link_anchor_2 === doc && Cast(l.link_anchor_1, Doc, null)?.annotationOn === context) || (l.link_anchor_1 === doc && Cast(l.link_anchor_2, Doc, null)?.annotationOn === context)); if (context && !hasContextAnchor && (context.type === DocumentType.VID || context.type === DocumentType.WEB || context.type === DocumentType.PDF || context.type === DocumentType.IMG)) { const pushpin = Docs.Create.FontIconDocument({ title: '', annotationOn: Cast(doc.annotationOn, Doc, null), followLinkToggle: true, icon: 'map-pin', x: Cast(doc.x, 'number', null), y: Cast(doc.y, 'number', null), backgroundColor: '#ACCEF7', layout_hideAllLinks: true, _width: 15, _height: 15, _xPadding: 0, onClick: FollowLinkScript(), _timecodeToShow: Cast(doc._timecodeToShow, 'number', null), }); Doc.AddDocToList(context, annotationField, pushpin); const pushpinLink = DocUtils.MakeLink(pushpin, doc, { link_relationship: 'pushpin' }, ''); doc._timecodeToShow = undefined; return pushpin; } return undefined; } // /** // * // * @param dms Degree Minute Second format exif gps data // * @param ref ref that determines negativity of decimal coordinates // * @returns a decimal format of gps latitude / longitude // */ // function getDecimalfromDMS(dms?: number[], ref?: string) { // if (dms && ref) { // let degrees = dms[0] / dms[1]; // let minutes = dms[2] / dms[3] / 60.0; // let seconds = dms[4] / dms[5] / 3600.0; // if (['S', 'W'].includes(ref)) { // degrees = -degrees; minutes = -minutes; seconds = -seconds // } // return (degrees + minutes + seconds).toFixed(5); // } // } function ConvertDMSToDD(degrees: number, minutes: number, seconds: number, direction: string) { var dd = degrees + minutes / 60 + seconds / (60 * 60); if (direction === 'S' || direction === 'W') { dd = dd * -1; } // Don't do anything for N or E return dd; } 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 = result.accessPaths.agnostic.client; const doc = await DocUtils.DocumentFromType(type, pathname, full, overwriteDoc); if (doc) { const proto = Doc.GetProto(doc); proto.text = result.rawText; if (Upload.isImageInformation(result)) { const maxNativeDim = Math.min(Math.max(result.nativeHeight, result.nativeWidth), defaultNativeImageDim); const exifRotation = StrCast((result.exifData?.data as any)?.Orientation).toLowerCase(); proto['data-nativeOrientation'] = result.exifData?.data?.image?.Orientation ?? (exifRotation.includes('rotate 90') || exifRotation.includes('rotate 270') ? 5 : undefined); proto['data_nativeWidth'] = result.nativeWidth < result.nativeHeight ? (maxNativeDim * result.nativeWidth) / result.nativeHeight : maxNativeDim; proto['data_nativeHeight'] = result.nativeWidth < result.nativeHeight ? maxNativeDim : maxNativeDim / (result.nativeWidth / result.nativeHeight); if (NumCast(proto['data-nativeOrientation']) >= 5) { proto['data_nativeHeight'] = result.nativeWidth < result.nativeHeight ? (maxNativeDim * result.nativeWidth) / result.nativeHeight : maxNativeDim; proto['data_nativeWidth'] = result.nativeWidth < result.nativeHeight ? maxNativeDim : maxNativeDim / (result.nativeWidth / result.nativeHeight); } proto.data_exif = JSON.stringify(result.exifData?.data); proto.data_contentSize = result.contentSize; // exif gps data coordinates are stored in DMS (Degrees Minutes Seconds), the following operation converts that to decimal coordinates const latitude = result.exifData?.data?.GPSLatitude; const latitudeDirection = result.exifData?.data?.GPSLatitudeRef; const longitude = result.exifData?.data?.GPSLongitude; const longitudeDirection = result.exifData?.data?.GPSLongitudeRef; if (latitude !== undefined && longitude !== undefined && latitudeDirection !== undefined && longitudeDirection !== undefined) { proto.latitude = ConvertDMSToDD(latitude[0], latitude[1], latitude[2], latitudeDirection); proto.longitude = ConvertDMSToDD(longitude[0], longitude[1], longitude[2], longitudeDirection); } } if (Upload.isVideoInformation(result)) { proto.data_duration = result.duration; } if (overwriteDoc) { LoadingBox.removeCurrentlyLoading(overwriteDoc); } generatedDocuments.push(doc); } } export function GetNewTextDoc(title: string, x: number, y: number, width?: number, height?: number, annotationOn?: Doc, backgroundColor?: string) { const tbox = Docs.Create.TextDocument('', { annotationOn, backgroundColor, _width: width || 200, _height: 35, x: x, y: y, _layout_centered: BoolCast(Doc.UserDoc().layout_centered), _layout_fitWidth: true, _layout_autoHeight: true, _layout_enableAltContentUI: BoolCast(Doc.UserDoc().defaultToFlashcards), title, }); const template = Doc.UserDoc().defaultTextLayout; if (template instanceof Doc) { // if a default text template is specified tbox._width = NumCast(template._width); tbox.layout_fieldKey = 'layout_' + StrCast(template.title); Doc.GetProto(tbox)[StrCast(tbox.layout_fieldKey)] = template; // set the text doc's layout to render with the text template tbox[DocData].proto = template; // and also set the text doc to inherit from the template (this allows the template to specify default field values) } return tbox; } export function uploadYoutubeVideoLoading(videoId: string, options: DocumentOptions, overwriteDoc?: Doc) { const generatedDocuments: Doc[] = []; Networking.UploadYoutubeToServer(videoId, overwriteDoc?.[Id]).then(upfiles => { const { source: { newFilename, originalFilename, mimetype }, result, } = upfiles.lastElement(); if ((result as any).message) { if (overwriteDoc) { overwriteDoc.isLoading = false; overwriteDoc.loadingError = (result as any).message; LoadingBox.removeCurrentlyLoading(overwriteDoc); } } else newFilename && processFileupload(generatedDocuments, newFilename, mimetype ?? '', result, options, overwriteDoc); }); } /** * uploadFilesToDocs will take in an array of Files, and creates documents for the * new files. * * @param files an array of files that will be uploaded * @param options options to use while uploading * @returns */ export async function uploadFilesToDocs(files: File[], options: DocumentOptions) { const generatedDocuments: Doc[] = []; // These files do not have overwriteDocs, so we do not set the guid and let the client generate one. const fileNoGuidPairs: Networking.FileGuidPair[] = files.map(file => ({ file })); const upfiles = await Networking.UploadFilesToServer(fileNoGuidPairs); for (const { source: { newFilename, mimetype }, result, } of upfiles) { newFilename && mimetype && processFileupload(generatedDocuments, newFilename, mimetype, result, options); } return generatedDocuments; } export function uploadFileToDoc(file: File, options: DocumentOptions, overwriteDoc: Doc) { const generatedDocuments: Doc[] = []; // Since this file has an overwriteDoc, we can set the client tracking guid to the overwriteDoc's guid. Networking.UploadFilesToServer([{ file, guid: overwriteDoc[Id] }]).then(upfiles => { const { source: { newFilename, mimetype }, result, } = upfiles.lastElement() ?? { source: { newFilename: '', mimetype: '' }, result: { message: 'upload failed' } }; if ((result as any).message) { if (overwriteDoc) { overwriteDoc.loadingError = (result as any).message; LoadingBox.removeCurrentlyLoading(overwriteDoc); } } else newFilename && mimetype && processFileupload(generatedDocuments, newFilename, mimetype, result, options, overwriteDoc); }); } // copies the specified drag factory document export function copyDragFactory(dragFactory: Doc) { if (!dragFactory) return undefined; const ndoc = dragFactory.isTemplateDoc ? Doc.ApplyTemplate(dragFactory) : Doc.MakeCopy(dragFactory, true); if (ndoc && dragFactory['dragFactory_count'] !== undefined) { dragFactory['dragFactory_count'] = NumCast(dragFactory['dragFactory_count']) + 1; Doc.SetInPlace(ndoc, 'title', ndoc.title + ' ' + NumCast(dragFactory['dragFactory_count']).toString(), true); } return ndoc; } export function delegateDragFactory(dragFactory: Doc) { const ndoc = Doc.MakeDelegateWithProto(dragFactory); if (ndoc && dragFactory['dragFactory_count'] !== undefined) { dragFactory['dragFactory_count'] = NumCast(dragFactory['dragFactory_count']) + 1; Doc.GetProto(ndoc).title = ndoc.title + ' ' + NumCast(dragFactory['dragFactory_count']).toString(); } return ndoc; } } ScriptingGlobals.add('Docs', Docs); ScriptingGlobals.add(function copyDragFactory(dragFactory: Doc, asDelegate?: boolean) { return dragFactory instanceof Doc ? (asDelegate ? DocUtils.delegateDragFactory(dragFactory) : DocUtils.copyDragFactory(dragFactory)) : dragFactory; }); ScriptingGlobals.add(function makeDelegate(proto: any) { const d = Docs.Create.DelegateDocument(proto, { title: 'child of ' + proto.title }); return d; }); ScriptingGlobals.add(function generateLinkTitle(link: Doc) { const link_anchor_1title = link.link_anchor_1 && link.link_anchor_1 !== link ? Cast(link.link_anchor_1, Doc, null)?.title : ''; const link_anchor_2title = link.link_anchor_2 && link.link_anchor_2 !== link ? Cast(link.link_anchor_2, Doc, null)?.title : ''; const relation = link.link_relationship || 'to'; return `${link_anchor_1title} (${relation}) ${link_anchor_2title}`; });