diff options
Diffstat (limited to 'src/client/documents/Documents.ts')
-rw-r--r-- | src/client/documents/Documents.ts | 597 |
1 files changed, 381 insertions, 216 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index c49ce4e17..811bb5fb2 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -1,60 +1,53 @@ -import { HistogramField } from "../northstar/dash-fields/HistogramField"; -import { HistogramBox } from "../northstar/dash-nodes/HistogramBox"; -import { HistogramOperation } from "../northstar/operations/HistogramOperation"; import { CollectionView } from "../views/collections/CollectionView"; import { CollectionViewType } from "../views/collections/CollectionView"; import { AudioBox } from "../views/nodes/AudioBox"; -import { FormattedTextBox } from "../views/nodes/FormattedTextBox"; +import { FormattedTextBox } from "../views/nodes/formattedText/FormattedTextBox"; import { ImageBox } from "../views/nodes/ImageBox"; import { KeyValueBox } from "../views/nodes/KeyValueBox"; import { PDFBox } from "../views/nodes/PDFBox"; +import { ScriptingBox } from "../views/nodes/ScriptingBox"; import { VideoBox } from "../views/nodes/VideoBox"; import { WebBox } from "../views/nodes/WebBox"; -import { Gateway } from "../northstar/manager/Gateway"; -import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils"; -import { action } from "mobx"; -import { ColumnAttributeModel } from "../northstar/core/attribute/AttributeModel"; -import { AttributeTransformationModel } from "../northstar/core/attribute/AttributeTransformationModel"; -import { AggregateFunction } from "../northstar/model/idea/idea"; -import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss"; -import { IconBox } from "../views/nodes/IconBox"; -import { OmitKeys, JSONUtils } from "../../Utils"; +import { OmitKeys, JSONUtils, Utils } from "../../Utils"; import { Field, Doc, Opt, DocListCastAsync, FieldResult, DocListCast } from "../../new_fields/Doc"; import { ImageField, VideoField, AudioField, PdfField, WebField, YoutubeField } from "../../new_fields/URLField"; import { HtmlField } from "../../new_fields/HtmlField"; import { List } from "../../new_fields/List"; -import { Cast, NumCast } from "../../new_fields/Types"; -import { IconField } from "../../new_fields/IconField"; -import { listSpec } from "../../new_fields/Schema"; +import { Cast, NumCast, StrCast } from "../../new_fields/Types"; import { DocServer } from "../DocServer"; import { dropActionType } from "../util/DragManager"; import { DateField } from "../../new_fields/DateField"; -import { UndoManager, undoBatch } from "../util/UndoManager"; import { YoutubeBox } from "../apis/youtube/YoutubeBox"; import { CollectionDockingView } from "../views/collections/CollectionDockingView"; import { LinkManager } from "../util/LinkManager"; import { DocumentManager } from "../util/DocumentManager"; import DirectoryImportBox from "../util/Import & Export/DirectoryImportBox"; import { Scripting } from "../util/Scripting"; -import { ButtonBox } from "../views/nodes/ButtonBox"; +import { LabelBox } from "../views/nodes/LabelBox"; +import { SliderBox } from "../views/nodes/SliderBox"; import { FontIconBox } from "../views/nodes/FontIconBox"; import { SchemaHeaderField } from "../../new_fields/SchemaHeaderField"; import { PresBox } from "../views/nodes/PresBox"; import { ComputedField, ScriptField } from "../../new_fields/ScriptField"; import { ProxyField } from "../../new_fields/Proxy"; import { DocumentType } from "./DocumentTypes"; -import { LinkFollowBox } from "../views/linking/LinkFollowBox"; +import { RecommendationsBox } from "../views/RecommendationsBox"; import { PresElementBox } from "../views/presentationview/PresElementBox"; +import { DashWebRTCVideo } from "../views/webcam/DashWebRTCVideo"; import { QueryBox } from "../views/nodes/QueryBox"; import { ColorBox } from "../views/nodes/ColorBox"; -import { DocuLinkBox } from "../views/nodes/DocuLinkBox"; -import { DocumentBox } from "../views/nodes/DocumentBox"; +import { LinkAnchorBox } from "../views/nodes/LinkAnchorBox"; +import { DocHolderBox } from "../views/nodes/DocumentBox"; import { InkingStroke } from "../views/InkingStroke"; import { InkField } from "../../new_fields/InkField"; import { InkingControl } from "../views/InkingControl"; import { RichTextField } from "../../new_fields/RichTextField"; -import { Networking } from "../Network"; -const requestImageSize = require('../util/request-image-size'); +import { extname } from "path"; +import { MessageStore } from "../../server/Message"; +import { ContextMenuProps } from "../views/ContextMenuItem"; +import { ContextMenu } from "../views/ContextMenu"; +import { LinkBox } from "../views/nodes/LinkBox"; +import { ScreenshotBox } from "../views/nodes/ScreenshotBox"; const path = require('path'); export interface DocumentOptions { @@ -68,77 +61,106 @@ export interface DocumentOptions { _fitWidth?: boolean; _fitToBox?: boolean; // whether a freeformview should zoom/scale to create a shrinkwrapped view of its contents _LODdisable?: boolean; - dropAction?: dropActionType; + _showTitleHover?: string; // + _showTitle?: string; // which field to display in the title area. leave empty to have no title + _showCaption?: string; // which field to display in the caption area. leave empty to have no caption + _scrollTop?: number; // scroll location for pdfs _chromeStatus?: string; - _viewType?: number; + _viewType?: string; // sub type of a collection _gridGap?: number; // gap between items in masonry view _xMargin?: number; // gap between left edge of document and start of masonry/stacking layouts _yMargin?: number; // gap between top edge of dcoument and start of masonry/stacking layouts - _textTemplate?: RichTextField; // template used by a formattedTextBox to create a text box to render + _xPadding?: number; + _yPadding?: number; _itemIndex?: number; // which item index the carousel viewer is showing _showSidebar?: boolean; //whether an annotationsidebar should be displayed for text docuemnts + _singleLine?: boolean; // whether text document is restricted to a single line (carriage returns make new document) x?: number; y?: number; z?: number; + author?: string; + dropAction?: dropActionType; + childDropAction?: dropActionType; layoutKey?: string; type?: string; title?: string; + label?: string; // short form of title for use as an icon label + style?: string; page?: number; scale?: number; isDisplayPanel?: boolean; // whether the panel functions as GoldenLayout "stack" used to display documents forceActive?: boolean; - preventTreeViewOpen?: boolean; // ignores the treeViewOpen Doc flag which allows a treeViewItem's expande/collapse state to be independent of other views of the same document in the tree view layout?: string | Doc; + hideFilterView?: boolean; // whether to hide the filter popout on collections hideHeadings?: boolean; // whether stacking view column headings should be hidden isTemplateForField?: string; // the field key for which the containing document is a rendering template isTemplateDoc?: boolean; templates?: List<string>; - backgroundColor?: string | ScriptField; + backgroundColor?: string | ScriptField; // background color for data doc + _backgroundColor?: string | ScriptField; // background color for each template layout doc ( overrides backgroundColor ) + color?: string; // foreground color data doc + _color?: string; // foreground color for each template layout doc (overrides color) + caption?: RichTextField; ignoreClick?: boolean; lockedPosition?: boolean; // lock the x,y coordinates of the document so that it can't be dragged lockedTransform?: boolean; // lock the panx,pany and scale parameters of the document so that it be panned/zoomed + isAnnotating?: boolean; // whether we web document is annotation mode where links can't be clicked to allow annotations to be created opacity?: number; defaultBackgroundColor?: string; + isBackground?: boolean; + isLinkButton?: boolean; columnWidth?: number; - fontSize?: number; + _fontSize?: number; + _fontFamily?: string; curPage?: number; currentTimecode?: number; // the current timecode of a time-based document (e.g., current time of a video) value is in seconds displayTimecode?: number; // the time that a document should be displayed (e.g., time an annotation should be displayed on a video) - documentText?: string; borderRounding?: string; boxShadow?: string; - showTitle?: string; - sectionFilter?: string; // field key used to determine headings for sections in stacking and masonry views + dontRegisterChildren?: boolean; + "onClick-rawScript"?: string; // onClick script in raw text form + "onCheckedClick-rawScript"?: string; // onChecked script in raw text form + "onCheckedClick-params"?: List<string>; // parameter list for onChecked treeview functions + _pivotField?: string; // field key used to determine headings for sections in stacking, masonry, pivot views schemaColumns?: List<SchemaHeaderField>; dockingConfig?: string; annotationOn?: Doc; removeDropProperties?: List<string>; // list of properties that should be removed from a document when it is dropped. e.g., a creator button may be forceActive to allow it be dragged, but the forceActive property can be removed from the dropped document dbDoc?: Doc; + linkRelationship?: string; // type of relatinoship a link represents ischecked?: ScriptField; // returns whether a font icon box is checked activePen?: Doc; // which pen document is currently active (used as the radio button state for the 'unhecked' pen tool scripts) onClick?: ScriptField; onChildClick?: ScriptField; // script given to children of a collection to execute when they are clicked onPointerDown?: ScriptField; onPointerUp?: ScriptField; + dropConverter?: ScriptField; // script to run when documents are dropped on this Document. dragFactory?: Doc; // document to create when dragging with a suitable onDragStart script onDragStart?: ScriptField; //script to execute at start of drag operation -- e.g., when a "creator" button is dragged this script generates a different document to drop - clipboard?: Doc; //script to execute at start of drag operation -- e.g., when a "creator" button is dragged this script generates a different document to drop + clipboard?: Doc; icon?: string; sourcePanel?: Doc; // panel to display in 'targetContainer' as the result of a button onClick script targetContainer?: Doc; // document whose proto will be set to 'panel' as the result of a onClick click script - dropConverter?: ScriptField; // script to run when documents are dropped on this Document. + searchFileTypes?: List<string>; // file types allowed in a search query strokeWidth?: number; - color?: string; + treeViewPreventOpen?: boolean; // ignores the treeViewOpen Doc flag which allows a treeViewItem's expand/collapse state to be independent of other views of the same document in the tree view treeViewHideTitle?: boolean; // whether to hide the title of a tree view + treeViewHideHeaderFields?: boolean; // whether to hide the drop down options for tree view items. treeViewOpen?: boolean; // whether this document is expanded in a tree view + treeViewExpandedView?: string; // which field/thing is displayed when this item is opened in tree view treeViewChecked?: ScriptField; // script to call when a tree view checkbox is checked - isFacetFilter?: boolean; // whether document functions as a facet filter in a tree view limitHeight?: number; // maximum height for newly created (eg, from pasting) text documents // [key: string]: Opt<Field>; pointerHack?: boolean; // for buttons, allows onClick handler to fire onPointerDown - isExpanded?: boolean; // is linear view expanded textTransform?: string; // is linear view expanded letterSpacing?: string; // is linear view expanded + flexDirection?: "unset" | "row" | "column" | "row-reverse" | "column-reverse"; + selectedIndex?: number; + syntaxColor?: string; // can be applied to text for syntax highlighting all matches in the text + searchText?: string; //for searchbox + searchQuery?: string; // for queryBox + filterQuery?: string; + linearViewIsExpanded?: boolean; // is linear view expanded } class EmptyBox { @@ -161,93 +183,106 @@ export namespace Docs { }; type TemplateMap = Map<DocumentType, PrototypeTemplate>; type PrototypeMap = Map<DocumentType, Doc>; - const data = "data"; + const defaultDataKey = "data"; const TemplateMap: TemplateMap = new Map([ - [DocumentType.TEXT, { - layout: { view: FormattedTextBox, dataField: data }, - options: { _height: 150, backgroundColor: "#f1efeb", defaultBackgroundColor: "#f1efeb" } - }], - [DocumentType.HIST, { - layout: { view: HistogramBox, dataField: data }, - options: { _height: 300, backgroundColor: "black" } + [DocumentType.RTF, { + layout: { view: FormattedTextBox, dataField: "text" }, + options: { _height: 150, _xMargin: 10, _yMargin: 10 } }], [DocumentType.QUERY, { - layout: { view: QueryBox, dataField: data }, + layout: { view: QueryBox, dataField: defaultDataKey }, options: { _width: 400 } }], [DocumentType.COLOR, { - layout: { view: ColorBox, dataField: data }, + layout: { view: ColorBox, dataField: defaultDataKey }, options: { _nativeWidth: 220, _nativeHeight: 300 } }], [DocumentType.IMG, { - layout: { view: ImageBox, dataField: data }, + layout: { view: ImageBox, dataField: defaultDataKey }, options: {} }], [DocumentType.WEB, { - layout: { view: WebBox, dataField: data }, + layout: { view: WebBox, dataField: defaultDataKey }, options: { _height: 300 } }], [DocumentType.COL, { - layout: { view: CollectionView, dataField: data }, + layout: { view: CollectionView, dataField: defaultDataKey }, options: { _panX: 0, _panY: 0, scale: 1 } // , _width: 500, _height: 500 } }], [DocumentType.KVP, { - layout: { view: KeyValueBox, dataField: data }, + layout: { view: KeyValueBox, dataField: defaultDataKey }, options: { _height: 150 } }], - [DocumentType.DOCUMENT, { - layout: { view: DocumentBox, dataField: data }, + [DocumentType.DOCHOLDER, { + layout: { view: DocHolderBox, dataField: defaultDataKey }, options: { _height: 250 } }], [DocumentType.VID, { - layout: { view: VideoBox, dataField: data }, + layout: { view: VideoBox, dataField: defaultDataKey }, options: { currentTimecode: 0 }, }], [DocumentType.AUDIO, { - layout: { view: AudioBox, dataField: data }, + layout: { view: AudioBox, dataField: defaultDataKey }, options: { _height: 35, backgroundColor: "lightGray" } }], [DocumentType.PDF, { - layout: { view: PDFBox, dataField: data }, + layout: { view: PDFBox, dataField: defaultDataKey }, options: { curPage: 1 } }], - [DocumentType.ICON, { - layout: { view: IconBox, dataField: data }, - options: { _width: Number(MINIMIZED_ICON_SIZE), _height: Number(MINIMIZED_ICON_SIZE) }, - }], [DocumentType.IMPORT, { - layout: { view: DirectoryImportBox, dataField: data }, + layout: { view: DirectoryImportBox, dataField: defaultDataKey }, options: { _height: 150 } }], - [DocumentType.LINKDOC, { + [DocumentType.LINK, { + layout: { view: LinkBox, dataField: defaultDataKey }, + options: { _height: 150 } + }], + [DocumentType.LINKDB, { data: new List<Doc>(), - layout: { view: EmptyBox, dataField: data }, + layout: { view: EmptyBox, dataField: defaultDataKey }, + options: { childDropAction: "alias", title: "Global Link Database" } + }], + [DocumentType.SCRIPTING, { + layout: { view: ScriptingBox, dataField: defaultDataKey } }], [DocumentType.YOUTUBE, { - layout: { view: YoutubeBox, dataField: data } + layout: { view: YoutubeBox, dataField: defaultDataKey } + }], + [DocumentType.LABEL, { + layout: { view: LabelBox, dataField: defaultDataKey }, }], [DocumentType.BUTTON, { - layout: { view: ButtonBox, dataField: data }, + layout: { view: LabelBox, dataField: "onClick" }, + }], + [DocumentType.SLIDER, { + layout: { view: SliderBox, dataField: defaultDataKey }, }], [DocumentType.PRES, { - layout: { view: PresBox, dataField: data }, + layout: { view: PresBox, dataField: defaultDataKey }, options: {} }], [DocumentType.FONTICON, { - layout: { view: FontIconBox, dataField: data }, + layout: { view: FontIconBox, dataField: defaultDataKey }, options: { _width: 40, _height: 40, borderRounding: "100%" }, }], - [DocumentType.LINKFOLLOW, { - layout: { view: LinkFollowBox, dataField: data } + [DocumentType.RECOMMENDATION, { + layout: { view: RecommendationsBox, dataField: defaultDataKey }, + options: { _width: 200, _height: 200 }, + }], + [DocumentType.WEBCAM, { + layout: { view: DashWebRTCVideo, dataField: defaultDataKey } }], [DocumentType.PRESELEMENT, { - layout: { view: PresElementBox, dataField: data } + layout: { view: PresElementBox, dataField: defaultDataKey } }], [DocumentType.INK, { - layout: { view: InkingStroke, dataField: data }, + layout: { view: InkingStroke, dataField: defaultDataKey }, options: { backgroundColor: "transparent" } - }] + }], + [DocumentType.SCREENSHOT, { + layout: { view: ScreenshotBox, dataField: defaultDataKey }, + }], ]); // All document prototypes are initialized with at least these values @@ -300,7 +335,7 @@ export namespace Docs { * A collection of all links in the database. Ideally, this would be a search, but for now all links are cached here. */ export function MainLinkDocument() { - return Prototypes.get(DocumentType.LINKDOC); + return Prototypes.get(DocumentType.LINKDB); } /** @@ -330,6 +365,7 @@ export namespace Docs { const options = { title, type, baseProto: true, ...defaultOptions, ...(template.options || {}) }; options.layout = layout.view.LayoutString(layout.dataField); const doc = Doc.assign(new Doc(prototypeId, true), { layoutKey: "layout", ...options }); + doc.layout_keyValue = KeyValueBox.LayoutString(""); return doc; } @@ -341,14 +377,62 @@ export namespace Docs { */ export namespace Create { - export async function Buxton() { - console.log(await Networking.FetchFromServer("/newBuxton")); + export function Buxton() { + let responded = false; + const loading = new Doc; + loading.title = "Please wait for the import script..."; + const parent = TreeDocument([loading], { + title: "The Buxton Collection", + _width: 400, + _height: 400, + _LODdisable: true + }); + const parentProto = Doc.GetProto(parent); + const { _socket } = DocServer; + _socket.off(MessageStore.BuxtonDocumentResult.Message); + _socket.off(MessageStore.BuxtonImportComplete.Message); + Utils.AddServerHandler(_socket, MessageStore.BuxtonDocumentResult, ({ device, errors }) => { + if (!responded) { + responded = true; + parentProto.data = new List<Doc>(); + } + if (device) { + const { __images } = device; + delete device.__images; + const { ImageDocument, StackingDocument } = Docs.Create; + const constructed = __images.map(({ url, nativeWidth, nativeHeight }) => ({ url: Utils.prepend(url), nativeWidth, nativeHeight })); + const deviceImages = constructed.map(({ url, nativeWidth, nativeHeight }, i) => ImageDocument(url, { + title: `image${i}.${extname(url)}`, + _nativeWidth: nativeWidth, + _nativeHeight: nativeHeight + })); + const doc = StackingDocument(deviceImages, { title: device.title, _LODdisable: true }); + const deviceProto = Doc.GetProto(doc); + deviceProto.hero = new ImageField(constructed[0].url); + Docs.Get.FromJson({ data: device, appendToExisting: { targetDoc: deviceProto } }); + Doc.AddDocToList(parentProto, "data", doc); + } else if (errors) { + console.log(errors); + } else { + alert("A Buxton document import was completely empty (??)"); + } + }); + Utils.AddServerHandler(_socket, MessageStore.BuxtonImportComplete, ({ deviceCount, errorCount }) => { + _socket.off(MessageStore.BuxtonDocumentResult.Message); + _socket.off(MessageStore.BuxtonImportComplete.Message); + alert(`Successfully imported ${deviceCount} device${deviceCount === 1 ? "" : "s"}, with ${errorCount} error${errorCount === 1 ? "" : "s"}, in ${(Date.now() - startTime) / 1000} seconds.`); + }); + const startTime = Date.now(); + Utils.Emit(_socket, MessageStore.BeginBuxtonImport, ""); + return parent; } Scripting.addGlobal(Buxton); - const delegateKeys = ["x", "y", "layoutKey", "_width", "_height", "_panX", "_panY", "_viewType", "_nativeWidth", "_nativeHeight", "dropAction", "_annotationOn", - "_chromeStatus", "_forceActive", "_autoHeight", "_fitWidth", "_LODdisable", "_itemIndex", "_showSidebar", "showTitle"]; + const delegateKeys = ["x", "y", "layoutKey", "_width", "_height", "_panX", "_panY", "_viewType", "_nativeWidth", "_nativeHeight", "dropAction", "childDropAction", "_annotationOn", + "_chromeStatus", "_autoHeight", "_fitWidth", "_LODdisable", "_itemIndex", "_showSidebar", "_showTitle", "_showCaption", "_showTitleHover", "_backgroundColor", + "_xMargin", "_yMargin", "_xPadding", "_yPadding", "_singleLine", "_scrollTop", + "_color", "isLinkButton", "isBackground", "removeDropProperties", "treeViewOpen"]; /** * This function receives the relevant document prototype and uses @@ -368,7 +452,7 @@ export namespace Docs { * only when creating a DockDocument from the current user's already existing * main document. */ - export function InstanceFromProto(proto: Doc, data: Field | undefined, options: DocumentOptions, delegId?: string) { + export function InstanceFromProto(proto: Doc, data: Field | undefined, options: DocumentOptions, delegId?: string, fieldKey: string = "data") { const { omit: protoProps, extract: delegateProps } = OmitKeys(options, delegateKeys); if (!("author" in protoProps)) { @@ -381,10 +465,10 @@ export namespace Docs { protoProps.isPrototype = true; - const dataDoc = MakeDataDelegate(proto, protoProps, data); + const dataDoc = MakeDataDelegate(proto, protoProps, data, fieldKey); const viewDoc = Doc.MakeDelegate(dataDoc, delegId); - AudioBox.ActiveRecordings.map(d => DocUtils.MakeLink({ doc: viewDoc }, { doc: d }, "audio link", "link to audio: " + d.title)); + viewDoc.type !== DocumentType.LINK && AudioBox.ActiveRecordings.map(d => DocUtils.MakeLink({ doc: viewDoc }, { doc: d }, "audio link", "audio timeline")); return Doc.assign(viewDoc, delegateProps, true); } @@ -399,10 +483,10 @@ export namespace Docs { * @param options initial values to apply to this new delegate * @param value the data to store in this new delegate */ - function MakeDataDelegate<D extends Field>(proto: Doc, options: DocumentOptions, value?: D) { + function MakeDataDelegate<D extends Field>(proto: Doc, options: DocumentOptions, value?: D, fieldKey: string = "data") { const deleg = Doc.MakeDelegate(proto); if (value !== undefined) { - deleg.data = value; + deleg[fieldKey] = value; } return Doc.assign(deleg, options); } @@ -415,23 +499,18 @@ export namespace Docs { const extension = path.extname(target); target = `${target.substring(0, target.length - extension.length)}_o${extension}`; } - requestImageSize(target) - .then((size: any) => { - const aspect = size.height / size.width; - if (!inst._nativeWidth) { - inst._nativeWidth = size.width; - } - inst._nativeHeight = NumCast(inst._nativeWidth) * aspect; - inst._height = NumCast(inst._width) * aspect; - }) - .catch((err: any) => console.log(err)); - // } return inst; } export function PresDocument(initial: List<Doc> = new List(), options: DocumentOptions = {}) { return InstanceFromProto(Prototypes.get(DocumentType.PRES), initial, options); } + export function ScriptingDocument(script: Opt<ScriptField>, options: DocumentOptions = {}, fieldKey?: string) { + const res = InstanceFromProto(Prototypes.get(DocumentType.SCRIPTING), script, options); + fieldKey && res.proto instanceof Doc && (res.proto.layout = ScriptingBox.LayoutString(fieldKey)); + return res; + } + export function VideoDocument(url: string, options: DocumentOptions = {}) { return InstanceFromProto(Prototypes.get(DocumentType.VID), new VideoField(new URL(url)), options); } @@ -440,12 +519,18 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.YOUTUBE), new YoutubeField(new URL(url)), options); } - export function AudioDocument(url: string, options: DocumentOptions = {}) { - return InstanceFromProto(Prototypes.get(DocumentType.AUDIO), new AudioField(new URL(url)), options); + export function WebCamDocument(url: string, options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocumentType.WEBCAM), "", options); } - export function HistogramDocument(histoOp: HistogramOperation, options: DocumentOptions = {}) { - return InstanceFromProto(Prototypes.get(DocumentType.HIST), new HistogramField(histoOp), options); + export function ScreenshotDocument(url: string, options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocumentType.SCREENSHOT), "", options); + } + + export function AudioDocument(url: string, options: DocumentOptions = {}) { + const instance = InstanceFromProto(Prototypes.get(DocumentType.AUDIO), new AudioField(new URL(url)), options); + Doc.GetProto(instance).backgroundColor = ComputedField.MakeFunction("this._audioState === 'playing' ? 'green':'gray'"); + return instance; } export function QueryDocument(options: DocumentOptions = {}) { @@ -457,58 +542,60 @@ export namespace Docs { } export function TextDocument(text: string, options: DocumentOptions = {}) { - return InstanceFromProto(Prototypes.get(DocumentType.TEXT), text, options); + return InstanceFromProto(Prototypes.get(DocumentType.RTF), text, options, undefined, "text"); } - export function InkDocument(color: string, tool: number, strokeWidth: number, points: { X: number, Y: number }[], options: DocumentOptions = {}) { - const doc = InstanceFromProto(Prototypes.get(DocumentType.INK), new InkField(points), options); - doc.color = color; - doc.strokeWidth = strokeWidth; - doc.tool = tool; + export function LinkDocument(source: { doc: Doc, ctx?: Doc }, target: { doc: Doc, ctx?: Doc }, options: DocumentOptions = {}, id?: string) { + const doc = InstanceFromProto(Prototypes.get(DocumentType.LINK), undefined, { isLinkButton: true, treeViewHideTitle: true, treeViewOpen: false, removeDropProperties: new List(["isBackground", "isLinkButton"]), ...options }); + const linkDocProto = Doc.GetProto(doc); + linkDocProto.anchor1 = source.doc; + linkDocProto.anchor2 = target.doc; + linkDocProto.anchor1_timecode = source.doc.currentTimecode || source.doc.displayTimecode; + linkDocProto.anchor2_timecode = target.doc.currentTimecode || target.doc.displayTimecode; + + if (linkDocProto.layout_key1 === undefined) { + Cast(linkDocProto.proto, Doc, null).layout_key1 = LinkAnchorBox.LayoutString("anchor1"); + Cast(linkDocProto.proto, Doc, null).layout_key2 = LinkAnchorBox.LayoutString("anchor2"); + Cast(linkDocProto.proto, Doc, null).linkBoxExcludedKeys = new List(["treeViewExpandedView", "treeViewHideTitle", "removeDropProperties", "linkBoxExcludedKeys", "treeViewOpen", "aliasNumber", "isPrototype", "lastOpened", "creationDate", "author"]); + Cast(linkDocProto.proto, Doc, null).layoutKey = undefined; + } + + LinkManager.Instance.addLink(doc); + + Doc.GetProto(source.doc).links = ComputedField.MakeFunction("links(self)"); + Doc.GetProto(target.doc).links = ComputedField.MakeFunction("links(self)"); return doc; } - export function IconDocument(icon: string, options: DocumentOptions = {}) { - return InstanceFromProto(Prototypes.get(DocumentType.ICON), new IconField(icon), options); + export function InkDocument(color: string, tool: number, strokeWidth: number, points: { X: number, Y: number }[], options: DocumentOptions = {}) { + const I = new Doc(); + I.type = DocumentType.INK; + I.layout = InkingStroke.LayoutString("data"); + I.color = color; + I.strokeWidth = strokeWidth; + I.tool = tool; + I.title = "ink"; + I.x = options.x; + I.y = options.y; + I._backgroundColor = "transparent"; + I._width = options._width; + I._height = options._height; + I.data = new InkField(points); + return I; + // return I; + // const doc = InstanceFromProto(Prototypes.get(DocumentType.INK), new InkField(points), options); + // doc.color = color; + // doc.strokeWidth = strokeWidth; + // doc.tool = tool; + // return doc; } export function PdfDocument(url: string, options: DocumentOptions = {}) { return InstanceFromProto(Prototypes.get(DocumentType.PDF), new PdfField(new URL(url)), options); } - export async function DBDocument(url: string, options: DocumentOptions = {}, columnOptions: DocumentOptions = {}) { - const schemaName = options.title ? options.title : "-no schema-"; - const ctlog = await Gateway.Instance.GetSchema(url, schemaName); - if (ctlog && ctlog.schemas) { - const schema = ctlog.schemas[0]; - const schemaDoc = Docs.Create.TreeDocument([], { ...options, _nativeWidth: undefined, _nativeHeight: undefined, _width: 150, _height: 100, title: schema.displayName! }); - const schemaDocuments = Cast(schemaDoc.data, listSpec(Doc), []); - if (!schemaDocuments) { - return; - } - CurrentUserUtils.AddNorthstarSchema(schema, schemaDoc); - const docs = schemaDocuments; - CurrentUserUtils.GetAllNorthstarColumnAttributes(schema).map(attr => { - DocServer.GetRefField(attr.displayName! + ".alias").then(action((field: Opt<Field>) => { - if (field instanceof Doc) { - docs.push(field); - } else { - const atmod = new ColumnAttributeModel(attr); - const histoOp = new HistogramOperation(schema.displayName!, - new AttributeTransformationModel(atmod, AggregateFunction.None), - new AttributeTransformationModel(atmod, AggregateFunction.Count), - new AttributeTransformationModel(atmod, AggregateFunction.Count)); - docs.push(Docs.Create.HistogramDocument(histoOp, { ...columnOptions, _width: 200, _height: 200, title: attr.displayName! })); - } - })); - }); - return schemaDoc; - } - return Docs.Create.TreeDocument([], { _width: 50, _height: 100, title: schemaName }); - } - export function WebDocument(url: string, options: DocumentOptions = {}) { - return InstanceFromProto(Prototypes.get(DocumentType.WEB), new WebField(new URL(url)), options); + return InstanceFromProto(Prototypes.get(DocumentType.WEB), url ? new WebField(new URL(url)) : undefined, { _fitWidth: true, _chromeStatus: url ? "disabled" : "enabled", isAnnotating: true, lockedTransform: true, ...options }); } export function HtmlDocument(html: string, options: DocumentOptions = {}) { @@ -520,17 +607,25 @@ export namespace Docs { } export function DocumentDocument(document?: Doc, options: DocumentOptions = {}) { - return InstanceFromProto(Prototypes.get(DocumentType.DOCUMENT), document, { title: document ? document.title + "" : "container", ...options }); + return InstanceFromProto(Prototypes.get(DocumentType.DOCHOLDER), document, { title: document ? document.title + "" : "container", ...options }); } export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) { return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, _viewType: CollectionViewType.Freeform }, id); } + export function PileDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) { + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", backgroundColor: "black", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, _viewType: CollectionViewType.Pile }, id); + } + export function LinearDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) { return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", backgroundColor: "black", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, _viewType: CollectionViewType.Linear }, id); } + export function MapDocument(documents: Array<Doc>, options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), options); + } + export function CarouselDocument(documents: Array<Doc>, options: DocumentOptions) { return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, _viewType: CollectionViewType.Carousel }); } @@ -539,24 +634,36 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", schemaColumns: new List(schemaColumns), ...options, _viewType: CollectionViewType.Schema }); } - export function TreeDocument(documents: Array<Doc>, options: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, _viewType: CollectionViewType.Tree }); + export function TreeDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) { + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, _viewType: CollectionViewType.Tree }, id); } - export function StackingDocument(documents: Array<Doc>, options: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, _viewType: CollectionViewType.Stacking }); + export function StackingDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) { + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, _viewType: CollectionViewType.Stacking }, id); } export function MulticolumnDocument(documents: Array<Doc>, options: DocumentOptions) { return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, _viewType: CollectionViewType.Multicolumn }); } + export function MultirowDocument(documents: Array<Doc>, options: DocumentOptions) { + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, _viewType: CollectionViewType.Multirow }); + } + export function MasonryDocument(documents: Array<Doc>, options: DocumentOptions) { return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, _viewType: CollectionViewType.Masonry }); } + export function LabelDocument(options?: DocumentOptions) { + return InstanceFromProto(Prototypes.get(DocumentType.LABEL), undefined, { ...(options || {}) }); + } + export function ButtonDocument(options?: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.BUTTON), undefined, { ...(options || {}) }); + return InstanceFromProto(Prototypes.get(DocumentType.BUTTON), undefined, { ...(options || {}), "onClick-rawScript": "-script-" }); + } + + export function SliderDocument(options?: DocumentOptions) { + return InstanceFromProto(Prototypes.get(DocumentType.SLIDER), undefined, { ...(options || {}) }); } @@ -564,10 +671,6 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.FONTICON), undefined, { ...(options || {}) }); } - export function LinkFollowBoxDocument(options?: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.LINKFOLLOW), undefined, { ...(options || {}) }); - } - export function PresElementBoxDocument(options?: DocumentOptions) { return InstanceFromProto(Prototypes.get(DocumentType.PRESELEMENT), undefined, { ...(options || {}) }); } @@ -582,6 +685,10 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.IMPORT), new List<Doc>(), options); } + export function RecommendationsDocument(data: Doc[], options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocumentType.RECOMMENDATION), new List<Doc>(data), options); + } + export type DocConfig = { doc: Doc, initialWidth?: number, @@ -594,7 +701,7 @@ export namespace Docs { { type: type, content: [ - ...configs.map(config => CollectionDockingView.makeDocumentConfig(config.doc, undefined, config.initialWidth, config.path)) + ...configs.map(config => CollectionDockingView.makeDocumentConfig(config.doc, config.initialWidth, config.path)) ] } ] @@ -607,6 +714,15 @@ export namespace Docs { const primitives = ["string", "number", "boolean"]; + export interface JsonConversionOpts { + data: any; + title?: string; + appendToExisting?: { targetDoc: Doc, fieldKey?: string }; + excludeEmptyObjects?: boolean; + } + + const defaultKey = "json"; + /** * This function takes any valid JSON(-like) data, i.e. parsed or unparsed, and at arbitrarily * deep levels of nesting, converts the data and structure into nested documents with the appropriate fields. @@ -624,26 +740,54 @@ export namespace Docs { * All TS/JS objects get converted directly to documents, directly preserving the key value structure. Everything else, * lacking the key value structure, gets stored as a field in a wrapper document. * - * @param input for convenience and flexibility, either a valid JSON string to be parsed, + * @param data for convenience and flexibility, either a valid JSON string to be parsed, * or the result of any JSON.parse() call. - * @param title an optional title to give to the highest parent document in the hierarchy + * @param title an optional title to give to the highest parent document in the hierarchy. + * If whether this function creates a new document or appendToExisting is specified and that document already has a title, + * because this title field can be left undefined for the opposite behavior, including a title will overwrite the existing title. + * @param appendToExisting **if specified**, there are two cases, both of which return the target document: + * + * 1) the json to be converted can be represented as a document, in which case the target document will act as the root + * of the tree and receive all the conversion results as new fields on itself + * 2) the json can't be represented as a document, in which case the function will assign the field-level conversion + * results to either the specified key on the target document, or to its "json" key by default. + * + * If not specified, the function creates and returns a new entirely generic document (different from the Doc.Create calls) + * to act as the root of the tree. + * + * One might choose to specify this field if you want to write to a document returned from a Document.Create function call, + * say a TreeView document that will be rendered, not just an untyped, identityless doc that would otherwise be created + * from a default call to new Doc. + * + * @param excludeEmptyObjects whether non-primitive objects (TypeScript objects and arrays) should be converted even + * if they contain no data. By default, empty objects and arrays are ignored. */ - export function DocumentHierarchyFromJson(input: any, title?: string): Opt<Doc> { - if (input === undefined || input === null || ![...primitives, "object"].includes(typeof input)) { + export function FromJson({ data, title, appendToExisting, excludeEmptyObjects }: JsonConversionOpts): Opt<Doc> { + if (excludeEmptyObjects === undefined) { + excludeEmptyObjects = true; + } + if (data === undefined || data === null || ![...primitives, "object"].includes(typeof data)) { return undefined; } - let parsed = input; - if (typeof input === "string") { - parsed = JSONUtils.tryParse(input); + let resolved: any; + try { + resolved = JSON.parse(typeof data === "string" ? data : JSON.stringify(data)); + } catch (e) { + return undefined; } - let converted: Doc; - if (typeof parsed === "object" && !(parsed instanceof Array)) { - converted = convertObject(parsed, title); + let output: Opt<Doc>; + if (typeof resolved === "object" && !(resolved instanceof Array)) { + output = convertObject(resolved, excludeEmptyObjects, title, appendToExisting?.targetDoc); } else { - (converted = new Doc).json = toField(parsed); + const result = toField(resolved, excludeEmptyObjects); + if (appendToExisting) { + (output = appendToExisting.targetDoc)[appendToExisting.fieldKey || defaultKey] = result; + } else { + (output = new Doc).json = result; + } } - title && (converted.title = title); - return converted; + title && output && (output.title = title); + return output; } /** @@ -653,12 +797,24 @@ export namespace Docs { * @returns the object mapped from JSON to field values, where each mapping * might involve arbitrary recursion (since toField might itself call convertObject) */ - const convertObject = (object: any, title?: string): Doc => { - const target = new Doc(); - let result: Opt<Field>; - Object.keys(object).map(key => (result = toField(object[key], key)) && (target[key] = result)); - title && !target.title && (target.title = title); - return target; + const convertObject = (object: any, excludeEmptyObjects: boolean, title?: string, target?: Doc): Opt<Doc> => { + const hasEntries = Object.keys(object).length; + if (hasEntries || !excludeEmptyObjects) { + const resolved = target ?? new Doc; + if (hasEntries) { + let result: Opt<Field>; + Object.keys(object).map(key => { + // if excludeEmptyObjects is true, any qualifying conversions from toField will + // be undefined, and thus the results that would have + // otherwise been empty (List or Doc)s will just not be written + if (result = toField(object[key], excludeEmptyObjects, key)) { + resolved[key] = result; + } + }); + } + title && (resolved.title = title); + return resolved; + } }; /** @@ -668,15 +824,19 @@ export namespace Docs { * @returns the list mapped from JSON to field values, where each mapping * might involve arbitrary recursion (since toField might itself call convertList) */ - const convertList = (list: Array<any>): List<Field> => { + const convertList = (list: Array<any>, excludeEmptyObjects: boolean): Opt<List<Field>> => { const target = new List(); let result: Opt<Field>; - list.map(item => (result = toField(item)) && target.push(result)); - return target; + // if excludeEmptyObjects is true, any qualifying conversions from toField will + // be undefined, and thus the results that would have + // otherwise been empty (List or Doc)s will just not be written + list.map(item => (result = toField(item, excludeEmptyObjects)) && target.push(result)); + if (target.length || !excludeEmptyObjects) { + return target; + } }; - - const toField = (data: any, title?: string): Opt<Field> => { + const toField = (data: any, excludeEmptyObjects: boolean, title?: string): Opt<Field> => { if (data === null || data === undefined) { return undefined; } @@ -684,7 +844,7 @@ export namespace Docs { return data; } if (typeof data === "object") { - return data instanceof Array ? convertList(data) : convertObject(data, title); + return data instanceof Array ? convertList(data, excludeEmptyObjects) : convertObject(data, excludeEmptyObjects, title, undefined); } throw new Error(`How did ${data} of type ${typeof data} end up in JSON?`); }; @@ -705,15 +865,9 @@ export namespace Docs { } else if (field instanceof PdfField) { created = Docs.Create.PdfDocument((field).url.href, resolved); layout = PDFBox.LayoutString; - } else if (field instanceof IconField) { - created = Docs.Create.IconDocument((field).icon, resolved); - layout = IconBox.LayoutString; } else if (field instanceof AudioField) { created = Docs.Create.AudioDocument((field).url.href, resolved); layout = AudioBox.LayoutString; - } else if (field instanceof HistogramField) { - created = Docs.Create.HistogramDocument((field).HistoOp, resolved); - layout = HistogramBox.LayoutString; } else if (field instanceof InkField) { const { selectedColor, selectedWidth, selectedTool } = InkingControl.Instance; created = Docs.Create.InkDocument(selectedColor, selectedTool, Number(selectedWidth), (field).inkData, resolved); @@ -725,9 +879,11 @@ export namespace Docs { created = Docs.Create.TextDocument("", { ...{ _width: 200, _height: 25, _autoHeight: true }, ...resolved }); layout = FormattedTextBox.LayoutString; } - created.layout = layout?.(fieldKey); - created.title = fieldKey; - proto && (created.proto = Doc.GetProto(proto)); + if (created) { + created.layout = layout?.(fieldKey); + created.title = fieldKey; + proto && created.proto && (created.proto = Doc.GetProto(proto)); + } return created; } @@ -750,10 +906,6 @@ export namespace Docs { if (!options._width) options._width = 400; if (!options._height) options._height = options._width * 1200 / 927; } - if (type.indexOf("excel") !== -1) { - ctor = Docs.Create.DBDocument; - options.dropAction = "copy"; - } if (type.indexOf("html") !== -1) { if (path.includes(window.location.hostname)) { const s = path.split('/'); @@ -771,7 +923,7 @@ export namespace Docs { }); } ctor = Docs.Create.WebDocument; - options = { _height: options._width, ...options, title: path, _nativeWidth: undefined }; + options = { ...options, _nativeWidth: 850, _nativeHeight: 962, _width: 500, _height: 566, title: path, }; } return ctor ? ctor(path, options) : undefined; } @@ -811,41 +963,54 @@ export namespace DocUtils { }); } - export function MakeLink(source: { doc: Doc, ctx?: Doc }, target: { doc: Doc, ctx?: Doc }, title: string = "", description: string = "", id?: string) { + export function MakeLink(source: { doc: Doc }, target: { doc: Doc }, linkRelationship: string = "", id?: string) { const sv = DocumentManager.Instance.getDocumentView(source.doc); if (sv && sv.props.ContainingCollectionDoc === target.doc) return; - if (target.doc === CurrentUserUtils.UserDocument) return undefined; - - const linkDocProto = new Doc(id, true); - UndoManager.RunInBatch(() => { - linkDocProto.type = DocumentType.LINK; + if (target.doc === Doc.UserDoc()) return undefined; - linkDocProto.title = title === "" ? source.doc.title + " to " + target.doc.title : title; - linkDocProto.linkDescription = description; - linkDocProto.isPrototype = true; + const linkDoc = Docs.Create.LinkDocument(source, target, { linkRelationship }, id); + Doc.GetProto(linkDoc).title = ComputedField.MakeFunction('self.anchor1.title +" (" + (self.linkRelationship||"to") +") " + self.anchor2.title'); - linkDocProto.anchor1 = source.doc; - linkDocProto.anchor2 = target.doc; - linkDocProto.anchor1Context = source.ctx; - linkDocProto.anchor2Context = target.ctx; - linkDocProto.anchor1Groups = new List<Doc>([]); - linkDocProto.anchor2Groups = new List<Doc>([]); - linkDocProto.anchor1Timecode = source.doc.currentTimecode; - linkDocProto.anchor2Timecode = target.doc.currentTimecode; - linkDocProto.layout_key1 = DocuLinkBox.LayoutString("anchor1"); - linkDocProto.layout_key2 = DocuLinkBox.LayoutString("anchor2"); - linkDocProto.width = linkDocProto.height = 0; - linkDocProto.isBackground = true; - linkDocProto.isButton = true; - - LinkManager.Instance.addLink(linkDocProto); - - Doc.GetProto(source.doc).links = ComputedField.MakeFunction("links(this)"); - Doc.GetProto(target.doc).links = ComputedField.MakeFunction("links(this)"); - }, "make link"); - return linkDocProto; + Doc.GetProto(source.doc).links = ComputedField.MakeFunction("links(self)"); + Doc.GetProto(target.doc).links = ComputedField.MakeFunction("links(self)"); + return linkDoc; } + export function addDocumentCreatorMenuItems(docTextAdder: (d: Doc) => void, docAdder: (d: Doc) => void, x: number, y: number): void { + ContextMenu.Instance.addItem({ + description: "Add Note ...", + subitems: DocListCast((Doc.UserDoc()["template-notes"] as Doc).data).map((note, i) => ({ + description: ":" + StrCast(note.title), + event: (args: { x: number, y: number }) => { + const textDoc = Docs.Create.TextDocument("", { + _width: 200, x, y, _autoHeight: note._autoHeight !== false, + title: StrCast(note.title) + "#" + (note.aliasCount = NumCast(note.aliasCount) + 1) + }); + textDoc.layoutKey = "layout_" + note.title; + textDoc[textDoc.layoutKey] = note; + docTextAdder(textDoc); + }, + icon: "eye" + })) as ContextMenuProps[], + icon: "eye" + }); + ContextMenu.Instance.addItem({ + description: "Add Template Doc ...", + subitems: DocListCast(Cast(Doc.UserDoc().dockedBtns, Doc, null)?.data).map(btnDoc => Cast(btnDoc?.dragFactory, Doc, null)).filter(doc => doc).map((dragDoc, i) => ({ + description: ":" + StrCast(dragDoc.title), + event: (args: { x: number, y: number }) => { + const newDoc = Doc.ApplyTemplate(dragDoc); + if (newDoc) { + newDoc.x = x; + newDoc.y = y; + docAdder(newDoc); + } + }, + icon: "eye" + })) as ContextMenuProps[], + icon: "eye" + }); + } } Scripting.addGlobal("Docs", Docs); |