diff options
Diffstat (limited to 'src')
103 files changed, 2069 insertions, 1570 deletions
diff --git a/src/Utils.ts b/src/Utils.ts index a8cde0624..9acdc8731 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -470,12 +470,43 @@ export function clearStyleSheetRules(sheet: any) { return false; } +export function simulateMouseClick(element: Element, x: number, y: number, sx: number, sy: number) { + ["pointerdown", "pointerup"].map(event => element.dispatchEvent( + new PointerEvent(event, { + view: window, + bubbles: true, + cancelable: true, + button: 2, + pointerType: "mouse", + clientX: x, + clientY: y, + screenX: sx, + screenY: sy, + }))); + + element.dispatchEvent( + new MouseEvent("contextmenu", { + view: window, + bubbles: true, + cancelable: true, + button: 2, + clientX: x, + clientY: y, + movementX: 0, + movementY: 0, + screenX: sx, + screenY: sy, + })); +} + export function setupMoveUpEvents( target: object, e: React.PointerEvent, moveEvent: (e: PointerEvent, down: number[], delta: number[]) => boolean, upEvent: (e: PointerEvent) => void, - clickEvent: (e: PointerEvent) => void) { + clickEvent: (e: PointerEvent) => void, + stopPropagation: boolean = true +) { (target as any)._downX = (target as any)._lastX = e.clientX; (target as any)._downY = (target as any)._lastY = e.clientY; @@ -499,8 +530,10 @@ export function setupMoveUpEvents( document.removeEventListener("pointermove", _moveEvent); document.removeEventListener("pointerup", _upEvent); }; - e.stopPropagation(); - e.preventDefault(); + if (stopPropagation) { + e.stopPropagation(); + e.preventDefault(); + } document.removeEventListener("pointermove", _moveEvent); document.removeEventListener("pointerup", _upEvent); document.addEventListener("pointermove", _moveEvent); diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index a2e59b238..de5777a62 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -8,7 +8,6 @@ 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 { CurrentUserUtils } from "../../server/authentication/models/current_user_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"; @@ -82,11 +81,13 @@ export interface DocumentOptions { 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; @@ -106,6 +107,7 @@ export interface DocumentOptions { 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; @@ -119,6 +121,8 @@ export interface DocumentOptions { boxShadow?: string; 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; @@ -139,13 +143,13 @@ export interface DocumentOptions { 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 + searchFileTypes?: List<string>; // file types allowed in a search query strokeWidth?: number; 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 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 @@ -180,7 +184,7 @@ 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.RTF, { @@ -188,100 +192,100 @@ export namespace Docs { 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.DOCHOLDER, { - layout: { view: DocHolderBox, dataField: data }, + 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.IMPORT, { - layout: { view: DirectoryImportBox, dataField: data }, + layout: { view: DirectoryImportBox, dataField: defaultDataKey }, options: { _height: 150 } }], [DocumentType.LINK, { - layout: { view: LinkBox, dataField: data }, + layout: { view: LinkBox, dataField: defaultDataKey }, options: { _height: 150 } }], [DocumentType.LINKDB, { data: new List<Doc>(), - layout: { view: EmptyBox, dataField: data }, - options: { childDropAction: "alias", title: "LINK DB" } + layout: { view: EmptyBox, dataField: defaultDataKey }, + options: { childDropAction: "alias", title: "Global Link Database" } }], [DocumentType.SCRIPTING, { - layout: { view: ScriptingBox, dataField: data } + layout: { view: ScriptingBox, dataField: defaultDataKey } }], [DocumentType.YOUTUBE, { - layout: { view: YoutubeBox, dataField: data } + layout: { view: YoutubeBox, dataField: defaultDataKey } }], [DocumentType.LABEL, { - layout: { view: LabelBox, dataField: data }, + layout: { view: LabelBox, dataField: defaultDataKey }, }], [DocumentType.BUTTON, { layout: { view: LabelBox, dataField: "onClick" }, }], [DocumentType.SLIDER, { - layout: { view: SliderBox, dataField: data }, + 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.RECOMMENDATION, { - layout: { view: RecommendationsBox, dataField: data }, + layout: { view: RecommendationsBox, dataField: defaultDataKey }, options: { _width: 200, _height: 200 }, }], [DocumentType.WEBCAM, { - layout: { view: DashWebRTCVideo, dataField: data } + 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: data }, + layout: { view: ScreenshotBox, dataField: defaultDataKey }, }], [DocumentType.COMPARISON, { - layout: { view: ComparisonBox, dataField: data }, + layout: { view: ComparisonBox, dataField: defaultDataKey }, }], ]); @@ -499,17 +503,6 @@ export namespace Docs { const extension = path.extname(target); target = `${target.substring(0, target.length - extension.length)}_o${extension}`; } - requestImageSize(Utils.CorsProxy(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 = {}) { @@ -592,6 +585,7 @@ export namespace Docs { 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); @@ -609,7 +603,7 @@ export namespace Docs { } 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 = {}) { @@ -933,7 +927,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; } @@ -976,7 +970,7 @@ export namespace DocUtils { 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; + if (target.doc === Doc.UserDoc()) return undefined; const linkDoc = Docs.Create.LinkDocument(source, target, { linkRelationship }, id); Doc.GetProto(linkDoc).title = ComputedField.MakeFunction('self.anchor1.title +" (" + (self.linkRelationship||"to") +") " + self.anchor2.title'); @@ -1006,7 +1000,7 @@ export namespace DocUtils { }); ContextMenu.Instance.addItem({ description: "Add Template Doc ...", - subitems: DocListCast(Cast(Doc.UserDoc().expandingButtons, Doc, null)?.data).map(btnDoc => Cast(btnDoc?.dragFactory, Doc, null)).filter(doc => doc).map((dragDoc, i) => ({ + 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); diff --git a/src/client/goldenLayout.js b/src/client/goldenLayout.js index b510385ff..2d4283b02 100644 --- a/src/client/goldenLayout.js +++ b/src/client/goldenLayout.js @@ -1551,7 +1551,7 @@ }, dimensions: { borderWidth: 5, - borderGrabWidth: 15, + borderGrabWidth: 5, minItemHeight: 10, minItemWidth: 10, headerHeight: 20, @@ -2796,11 +2796,13 @@ if (this._isVertical) { dragHandle.css('top', -handleExcessPos); dragHandle.css('height', this._size + handleExcessSize); + element.css('cursor', 'row-resize'); element.addClass('lm_vertical'); element['height'](this._size); } else { dragHandle.css('left', -handleExcessPos); dragHandle.css('width', this._size + handleExcessSize); + element.css('cursor', 'col-resize'); element.addClass('lm_horizontal'); element['width'](this._size); } diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 2d6078cf3..4683e77a8 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -156,7 +156,12 @@ export class DocumentManager { let annotatedDoc = await Cast(targetDoc.annotationOn, Doc); if (annotatedDoc) { const first = getFirstDocView(annotatedDoc); - if (first) annotatedDoc = first.props.Document; + if (first) { + annotatedDoc = first.props.Document; + if (docView) { + docView.props.focus(annotatedDoc, false); + } + } } if (docView) { // we have a docView already and aren't forced to create a new one ... just focus on the document. TODO move into view if necessary otherwise just highlight? docView.props.focus(docView.props.Document, willZoom, undefined, focusAndFinish); @@ -219,9 +224,9 @@ export class DocumentManager { if (linkDoc) { const target = (doc === linkDoc.anchor1 ? linkDoc.anchor2 : doc === linkDoc.anchor2 ? linkDoc.anchor1 : (Doc.AreProtosEqual(doc, linkDoc.anchor1 as Doc) ? linkDoc.anchor2 : linkDoc.anchor1)) as Doc; - const targetTimecode = (doc === linkDoc.anchor1 ? Cast(linkDoc.anchor2_timecode, "number") : - doc === linkDoc.anchor2 ? Cast(linkDoc.anchor1_timecode, "number"): - (Doc.AreProtosEqual(doc, linkDoc.anchor1 as Doc) ? Cast(linkDoc.anchor2_timecode, "number"):Cast(linkDoc.anchor1_timecode, "number"))); + const targetTimecode = (doc === linkDoc.anchor1 ? Cast(linkDoc.anchor2_timecode, "number") : + doc === linkDoc.anchor2 ? Cast(linkDoc.anchor1_timecode, "number") : + (Doc.AreProtosEqual(doc, linkDoc.anchor1 as Doc) ? Cast(linkDoc.anchor2_timecode, "number") : Cast(linkDoc.anchor1_timecode, "number"))); if (target) { const containerDoc = (await Cast(target.annotationOn, Doc)) || target; containerDoc.currentTimecode = targetTimecode; @@ -236,4 +241,4 @@ export class DocumentManager { } } } -Scripting.addGlobal(function focus(doc: any) { DocumentManager.Instance.getDocumentViews(Doc.GetProto(doc)).map(view => view.props.focus(doc, true)); });
\ No newline at end of file +Scripting.addGlobal(function DocFocus(doc: any) { DocumentManager.Instance.getDocumentViews(Doc.GetProto(doc)).map(view => view.props.focus(doc, true)); });
\ No newline at end of file diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 3e9a5b63a..42a78a4bf 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -19,7 +19,7 @@ import { DateField } from "../../new_fields/DateField"; import { DocumentView } from "../views/nodes/DocumentView"; import { UndoManager } from "./UndoManager"; -export type dropActionType = "place" | "alias" | "copy" | undefined; +export type dropActionType = "alias" | "copy" | "move" | undefined; // undefined = move export function SetupDrag( _reference: React.RefObject<HTMLElement>, docFunc: () => Doc | Promise<Doc> | undefined, @@ -205,7 +205,7 @@ export namespace DragManager { e.docDragData && (e.docDragData.droppedDocuments = dragData.draggedDocuments.map(d => !dragData.isSelectionMove && !dragData.userDropAction && ScriptCast(d.onDragStart) ? addAudioTag(ScriptCast(d.onDragStart).script.run({ this: d }).result) : dragData.userDropAction === "alias" || (!dragData.userDropAction && dragData.dropAction === "alias") ? Doc.MakeAlias(d) : - dragData.userDropAction === "copy" || (!dragData.userDropAction && dragData.dropAction === "copy") ? Doc.MakeCopy(d, true) : d) + dragData.userDropAction === "copy" || (!dragData.userDropAction && dragData.dropAction === "copy") ? Doc.MakeClone(d) : d) ); e.docDragData?.droppedDocuments.forEach((drop: Doc, i: number) => (dragData?.removeDropProperties || []).concat(Cast(dragData.draggedDocuments[i].removeDropProperties, listSpec("string"), [])).map(prop => drop[prop] = undefined) diff --git a/src/client/util/History.ts b/src/client/util/History.ts index 545e8acb4..2c53d7e52 100644 --- a/src/client/util/History.ts +++ b/src/client/util/History.ts @@ -40,8 +40,12 @@ export namespace HistoryUtil { // } } + let _lastStatePush = 0; export function pushState(state: ParsedUrl) { - history.pushState(state, "", createUrl(state)); + if (Date.now() - _lastStatePush > 1000) { + history.pushState(state, "", createUrl(state)); + } + _lastStatePush = Date.now(); } export function replaceState(state: ParsedUrl) { diff --git a/src/client/util/Import & Export/ImageUtils.ts b/src/client/util/Import & Export/ImageUtils.ts index 9fae5ff93..c8d1530b3 100644 --- a/src/client/util/Import & Export/ImageUtils.ts +++ b/src/client/util/Import & Export/ImageUtils.ts @@ -24,6 +24,7 @@ export namespace ImageUtils { const proto = Doc.GetProto(document); proto["data-nativeWidth"] = nativeWidth; proto["data-nativeHeight"] = nativeHeight; + proto["data-path"] = source; proto.contentSize = contentSize ? contentSize : undefined; return data !== undefined; }; diff --git a/src/client/util/ProsemirrorExampleTransfer.ts b/src/client/util/ProsemirrorExampleTransfer.ts index 42247f177..680f48f70 100644 --- a/src/client/util/ProsemirrorExampleTransfer.ts +++ b/src/client/util/ProsemirrorExampleTransfer.ts @@ -154,7 +154,7 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, props: any const originalDoc = layoutDoc.rootDocument || layoutDoc; if (originalDoc instanceof Doc) { const newDoc = Docs.Create.TextDocument("", { - title: "", layout: Cast(originalDoc.layout, Doc, null) || FormattedTextBox.DefaultLayout, _singleLine: BoolCast(originalDoc._singleLine), + layout: Cast(originalDoc.layout, Doc, null) || FormattedTextBox.DefaultLayout, _singleLine: BoolCast(originalDoc._singleLine), x: NumCast(originalDoc.x), y: NumCast(originalDoc.y) + NumCast(originalDoc._height) + 10, _width: NumCast(layoutDoc._width), _height: NumCast(layoutDoc._height) }); FormattedTextBox.SelectOnLoad = newDoc[Id]; @@ -172,7 +172,7 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, props: any const originalDoc = layoutDoc.rootDocument || layoutDoc; if (force || props.Document._singleLine) { const newDoc = Docs.Create.TextDocument("", { - title: "", layout: Cast(originalDoc.layout, Doc, null) || FormattedTextBox.DefaultLayout, _singleLine: BoolCast(originalDoc._singleLine), + layout: Cast(originalDoc.layout, Doc, null) || FormattedTextBox.DefaultLayout, _singleLine: BoolCast(originalDoc._singleLine), x: NumCast(originalDoc.x) + NumCast(originalDoc._width) + 10, y: NumCast(originalDoc.y), _width: NumCast(layoutDoc._width), _height: NumCast(layoutDoc._height) }); FormattedTextBox.SelectOnLoad = newDoc[Id]; diff --git a/src/client/util/RichTextMenu.tsx b/src/client/util/RichTextMenu.tsx index 3f0ec7aa5..4a9a4c10f 100644 --- a/src/client/util/RichTextMenu.tsx +++ b/src/client/util/RichTextMenu.tsx @@ -445,8 +445,8 @@ export default class RichTextMenu extends AntimodeMenu { } const button = - <button className="antimodeMenu-button" title="" onPointerDown={onBrushClick} style={this.brushMarks && this.brushMarks.size > 0 ? { backgroundColor: "121212" } : {}}> - <FontAwesomeIcon icon="paint-roller" size="lg" style={{ transition: "transform 0.1s", transform: this.brushMarks && this.brushMarks.size > 0 ? "rotate(45deg)" : "" }} /> + <button className="antimodeMenu-button" title="" onPointerDown={onBrushClick} style={this.brushMarks?.size > 0 ? { backgroundColor: "121212" } : {}}> + <FontAwesomeIcon icon="paint-roller" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.1s", transform: `rotate(${this.brushMarks?.size > 0 ? 45 : 0}deg)` }} /> </button>; const dropdownContent = @@ -790,11 +790,11 @@ export default class RichTextMenu extends AntimodeMenu { <div key="button"> <div key="collapser"> <button className="antimodeMenu-button" key="collapse menu" title="Collapse menu" onClick={this.toggleCollapse} style={{ backgroundColor: this.collapsed ? "#121212" : "", width: 25 }}> - <FontAwesomeIcon icon="chevron-left" size="lg" style={{ transition: "transform 0.3s", transform: this.collapsed ? "rotate(180deg)" : "" }} /> + <FontAwesomeIcon icon="chevron-left" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.3s", transform: `rotate(${this.collapsed ? 180 : 0}deg)` }} /> </button> </div> <button className="antimodeMenu-button" key="pin menu" title="Pin menu" onClick={this.toggleMenuPin} style={{ backgroundColor: this.Pinned ? "#121212" : "", display: this.collapsed ? "none" : undefined }}> - <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transition: "transform 0.1s", transform: this.Pinned ? "rotate(45deg)" : "" }} /> + <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.1s", transform: `rotate(${this.Pinned ? 45 : 0}deg)` }} /> </button> {this.getDragger()} </div> diff --git a/src/client/util/RichTextRules.ts b/src/client/util/RichTextRules.ts index 6bbe81115..3746199ba 100644 --- a/src/client/util/RichTextRules.ts +++ b/src/client/util/RichTextRules.ts @@ -110,7 +110,8 @@ export class RichTextRules { return state.tr; } if (value !== "" && value !== undefined) { - this.Document[DataSym][fieldKey] = value === "true" ? true : value === "false" ? false : value; + const num = value.match(/^[0-9.]/); + this.Document[DataSym][fieldKey] = value === "true" ? true : value === "false" ? false : (num ? Number(value) : value); } const fieldView = state.schema.nodes.dashField.create({ fieldKey, docid }); return state.tr.deleteRange(start, end).insert(start, fieldView); diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx index 0599b3ebe..d23962d5c 100644 --- a/src/client/util/RichTextSchema.tsx +++ b/src/client/util/RichTextSchema.tsx @@ -27,11 +27,11 @@ import ParagraphNodeSpec from "./ParagraphNodeSpec"; import { Transform } from "./Transform"; import React = require("react"); -const - blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], +const + blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"], - preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], - brDOM: DOMOutputSpecArray = ["br"], + preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], + brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0]; // :: Object @@ -852,9 +852,9 @@ export class DashDocView { }; this._renderDisposer?.(); this._renderDisposer = reaction(() => { - if (!Doc.AreProtosEqual(finalLayout, dashDoc)) { - finalLayout.rootDocument = dashDoc.aliasOf; - } + // if (!Doc.AreProtosEqual(finalLayout, dashDoc)) { + // finalLayout.rootDocument = dashDoc.aliasOf; // bcz: check on this ... why is it here? + // } const layoutKey = StrCast(finalLayout.layoutKey); const finalKey = layoutKey && StrCast(finalLayout[layoutKey]).split("'")?.[1]; if (finalLayout !== dashDoc && finalKey) { diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts index 2026bf940..6501da34a 100644 --- a/src/client/util/SearchUtil.ts +++ b/src/client/util/SearchUtil.ts @@ -44,7 +44,7 @@ export namespace SearchUtil { const { ids, highlighting } = result; const txtresult = query !== "*" && JSON.parse(await rp.get(Utils.prepend("/textsearch"), { - qs: { ...options, q: query }, + qs: { ...options, q: query.replace(/^[ \+\?\*\|]*/, "") }, // a leading '+' leads to a server crash since findInFiles doesn't handle regex failures })); const fileids = txtresult ? txtresult.ids : []; diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index 6c386d684..a49977c42 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -28,21 +28,21 @@ export namespace SelectionManager { manager.SelectedDocuments.clear(); manager.SelectedDocuments.set(docView, true); } - Doc.UserDoc().SelectedDocs = new List(SelectionManager.SelectedDocuments().map(dv => dv.props.Document)); + Doc.UserDoc().activeSelection = new List(SelectionManager.SelectedDocuments().map(dv => dv.props.Document)); } @action DeselectDoc(docView: DocumentView): void { if (manager.SelectedDocuments.get(docView)) { manager.SelectedDocuments.delete(docView); docView.props.whenActiveChanged(false); - Doc.UserDoc().SelectedDocs = new List(SelectionManager.SelectedDocuments().map(dv => dv.props.Document)); + Doc.UserDoc().activeSelection = new List(SelectionManager.SelectedDocuments().map(dv => dv.props.Document)); } } @action DeselectAll(): void { Array.from(manager.SelectedDocuments.keys()).map(dv => dv.props.whenActiveChanged(false)); manager.SelectedDocuments.clear(); - Doc.UserDoc().SelectedDocs = new List<Doc>([]); + Doc.UserDoc().activeSelection = new List<Doc>([]); } } diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index 7496ac73c..3ce6de80d 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -109,7 +109,7 @@ export default class SharingManager extends React.Component<{}> { if (isCandidate) { const userDocument = await DocServer.GetRefField(user.userDocumentId); if (userDocument instanceof Doc) { - const notificationDoc = await Cast(userDocument.optionalRightCollection, Doc); + const notificationDoc = await Cast(userDocument.rightSidebarCollection, Doc); runInAction(() => { if (notificationDoc instanceof Doc) { this.users.push({ user, notificationDoc }); diff --git a/src/client/views/AntimodeMenu.tsx b/src/client/views/AntimodeMenu.tsx index fba2fb5c6..f810361c6 100644 --- a/src/client/views/AntimodeMenu.tsx +++ b/src/client/views/AntimodeMenu.tsx @@ -16,7 +16,8 @@ export default abstract class AntimodeMenu extends React.Component { @observable protected _top: number = -300; @observable protected _left: number = -300; @observable protected _opacity: number = 1; - @observable protected _transition: string = "opacity 0.5s"; + @observable protected _transitionProperty: string = "opacity"; + @observable protected _transitionDuration: string = "0.5s"; @observable protected _transitionDelay: string = ""; @observable protected _canFade: boolean = true; @@ -34,7 +35,7 @@ export default abstract class AntimodeMenu extends React.Component { */ public jumpTo = (x: number, y: number, forceJump: boolean = false) => { if (!this.Pinned || forceJump) { - this._transition = this._transitionDelay = ""; + this._transitionProperty = this._transitionDuration = this._transitionDelay = ""; this._opacity = 1; this._left = x; this._top = y; @@ -49,14 +50,16 @@ export default abstract class AntimodeMenu extends React.Component { public fadeOut = (forceOut: boolean) => { if (!this.Pinned) { if (this._opacity === 0.2) { - this._transition = "opacity 0.1s"; + this._transitionProperty = "opacity"; + this._transitionDuration = "0.1s"; this._transitionDelay = ""; this._opacity = 0; this._left = this._top = -300; } if (forceOut) { - this._transition = ""; + this._transitionProperty = ""; + this._transitionDuration = ""; this._transitionDelay = ""; this._opacity = 0; this._left = this._top = -300; @@ -67,7 +70,8 @@ export default abstract class AntimodeMenu extends React.Component { @action protected pointerLeave = (e: React.PointerEvent) => { if (!this.Pinned && this._canFade) { - this._transition = "opacity 0.5s"; + this._transitionProperty = "opacity"; + this._transitionDuration = "0.5s"; this._transitionDelay = "1s"; this._opacity = 0.2; setTimeout(() => this.fadeOut(false), 3000); @@ -76,7 +80,8 @@ export default abstract class AntimodeMenu extends React.Component { @action protected pointerEntered = (e: React.PointerEvent) => { - this._transition = "opacity 0.1s"; + this._transitionProperty = "opacity"; + this._transitionDuration = "0.1s"; this._transitionDelay = ""; this._opacity = 1; } @@ -133,7 +138,7 @@ export default abstract class AntimodeMenu extends React.Component { protected getElement(buttons: JSX.Element[]) { return ( <div className="antimodeMenu-cont" onPointerLeave={this.pointerLeave} onPointerEnter={this.pointerEntered} ref={this._mainCont} onContextMenu={this.handleContextMenu} - style={{ left: this._left, top: this._top, opacity: this._opacity, transition: this._transition, transitionDelay: this._transitionDelay }}> + style={{ left: this._left, top: this._top, opacity: this._opacity, transitionProperty: this._transitionProperty, transitionDuration: this._transitionDuration, transitionDelay: this._transitionDelay }}> {buttons} <div className="antimodeMenu-dragger" onPointerDown={this.dragStart} style={{ width: this.Pinned ? "20px" : "0px" }} /> </div> @@ -143,7 +148,7 @@ export default abstract class AntimodeMenu extends React.Component { protected getElementWithRows(rows: JSX.Element[], numRows: number, hasDragger: boolean = true) { return ( <div className="antimodeMenu-cont with-rows" onPointerLeave={this.pointerLeave} onPointerEnter={this.pointerEntered} ref={this._mainCont} onContextMenu={this.handleContextMenu} - style={{ left: this._left, top: this._top, opacity: this._opacity, transition: this._transition, transitionDelay: this._transitionDelay, height: "auto" }}> + style={{ left: this._left, top: this._top, opacity: this._opacity, transitionProperty: this._transitionProperty, transitionDuration: this._transitionDuration, transitionDelay: this._transitionDelay, height: "auto" }}> {rows} {hasDragger ? <div className="antimodeMenu-dragger" onPointerDown={this.dragStart} style={{ width: this.Pinned ? "20px" : "0px" }} /> : <></>} </div> diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index b1bd4191c..f19f9308a 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -53,7 +53,7 @@ export function ViewBoxBaseComponent<P extends ViewBoxBaseProps, T>(schemaCtor: // key where data is stored @computed get fieldKey() { return this.props.fieldKey; } - active = (outsideReaction?: boolean) => !this.props.Document.isBackground && ((this.props.Document.forceActive && this.props.rootSelected(outsideReaction)) || this.props.isSelected(outsideReaction) || this.props.renderDepth === 0);// && !InkingControl.Instance.selectedTool; // bcz: inking state shouldn't affect static tools + active = (outsideReaction?: boolean) => !this.props.Document.isBackground && (this.props.rootSelected(outsideReaction) || this.props.isSelected(outsideReaction) || this.props.renderDepth === 0);// && !InkingControl.Instance.selectedTool; // bcz: inking state shouldn't affect static tools protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; } return Component; @@ -114,7 +114,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T whenActiveChanged = action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive)); active = (outsideReaction?: boolean) => ((InkingControl.Instance.selectedTool === InkTool.None && !this.props.Document.isBackground) && - ((this.props.Document.forceActive && this.props.rootSelected(outsideReaction)) || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false) + (this.props.rootSelected(outsideReaction) || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false) annotationsActive = (outsideReaction?: boolean) => (InkingControl.Instance.selectedTool !== InkTool.None || (this.props.Document.isBackground && this.props.active()) || (this.props.Document.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false) } diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index 5b78008ab..c02f79187 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -6,7 +6,7 @@ import { observer } from "mobx-react"; import { Doc, DocListCast } from "../../new_fields/Doc"; import { RichTextField } from '../../new_fields/RichTextField'; import { NumCast, StrCast } from "../../new_fields/Types"; -import { emptyFunction } from "../../Utils"; +import { emptyFunction, setupMoveUpEvents } from "../../Utils"; import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils'; import { UndoManager } from "../util/UndoManager"; import { CollectionDockingView, DockedFrameRenderer } from './collections/CollectionDockingView'; @@ -21,7 +21,6 @@ import { Template, Templates } from "./Templates"; import React = require("react"); import { DragManager } from '../util/DragManager'; import { MetadataEntryMenu } from './MetadataEntryMenu'; -import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils'; import GoogleAuthenticationManager from '../apis/GoogleAuthenticationManager'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; @@ -109,10 +108,8 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView | get view0() { return this.props.views?.[0]; } @action - onLinkButtonMoved = (e: PointerEvent): void => { - if (this._linkButton.current !== null && (Math.abs(e.clientX - this._downX) > 3 || Math.abs(e.clientY - this._downY) > 3)) { - document.removeEventListener("pointermove", this.onLinkButtonMoved); - document.removeEventListener("pointerup", this.onLinkButtonUp); + onLinkButtonMoved = (e: PointerEvent) => { + if (this._linkButton.current !== null) { const linkDrag = UndoManager.StartBatch("Drag Link"); this.view0 && DragManager.StartLinkDrag(this._linkButton.current, this.view0.props.Document, e.pageX, e.pageY, { dragComplete: dropEv => { @@ -132,26 +129,16 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView | }, hideSource: false }); + return true; } - e.stopPropagation(); + return false; } onLinkButtonDown = (e: React.PointerEvent): void => { - this._downX = e.clientX; - this._downY = e.clientY; - document.removeEventListener("pointermove", this.onLinkButtonMoved); - document.addEventListener("pointermove", this.onLinkButtonMoved); - document.removeEventListener("pointerup", this.onLinkButtonUp); - document.addEventListener("pointerup", this.onLinkButtonUp); - e.stopPropagation(); + setupMoveUpEvents(this, e, this.onLinkButtonMoved, emptyFunction, emptyFunction); } - onLinkButtonUp = (e: PointerEvent): void => { - document.removeEventListener("pointermove", this.onLinkButtonMoved); - document.removeEventListener("pointerup", this.onLinkButtonUp); - e.stopPropagation(); - } @computed get considerGoogleDocsPush() { @@ -202,9 +189,9 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView | @computed get pinButton() { const targetDoc = this.view0?.props.Document; - const isPinned = targetDoc && CurrentUserUtils.IsDocPinned(targetDoc); + const isPinned = targetDoc && Doc.isDocPinned(targetDoc); return !targetDoc ? (null) : <div className="documentButtonBar-linker" - title={CurrentUserUtils.IsDocPinned(targetDoc) ? "Unpin from presentation" : "Pin to presentation"} + title={Doc.isDocPinned(targetDoc) ? "Unpin from presentation" : "Pin to presentation"} style={{ backgroundColor: isPinned ? "black" : "white", color: isPinned ? "white" : "black" }} onClick={e => { @@ -258,29 +245,12 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView | }} />; } - private _downx = 0; - private _downy = 0; - onAliasButtonUp = (e: PointerEvent): void => { - document.removeEventListener("pointermove", this.onAliasButtonMoved); - document.removeEventListener("pointerup", this.onAliasButtonUp); - e.stopPropagation(); - } - + @observable _aliasDown = false; onAliasButtonDown = (e: React.PointerEvent): void => { - this._downx = e.clientX; - this._downy = e.clientY; - e.stopPropagation(); - e.preventDefault(); - document.removeEventListener("pointermove", this.onAliasButtonMoved); - document.addEventListener("pointermove", this.onAliasButtonMoved); - document.removeEventListener("pointerup", this.onAliasButtonUp); - document.addEventListener("pointerup", this.onAliasButtonUp); + setupMoveUpEvents(this, e, this.onAliasButtonMoved, emptyFunction, emptyFunction); } - onAliasButtonMoved = (e: PointerEvent): void => { - if (this._dragRef.current !== null && (Math.abs(e.clientX - this._downx) > 4 || Math.abs(e.clientY - this._downy) > 4)) { - document.removeEventListener("pointermove", this.onAliasButtonMoved); - document.removeEventListener("pointerup", this.onAliasButtonUp); - + onAliasButtonMoved = () => { + if (this._dragRef.current) { const dragDocView = this.props.views[0]!; const dragData = new DragManager.DocumentDragData([dragDocView.props.Document]); const [left, top] = dragDocView.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); @@ -291,8 +261,9 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView | offsetY: dragData.offset[1], hideSource: false }); + return true; } - e.stopPropagation(); + return false; } @computed @@ -302,9 +273,9 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView | Array.from(Object.values(Templates.TemplateList)).map(template => templates.set(template, this.props.views.reduce((checked, doc) => checked || doc?.props.Document["_show" + template.Name] ? true : false, false as boolean))); return !view0 ? (null) : <div title="Tap: Customize layout. Drag: Create alias" className="documentButtonBar-linkFlyout" ref={this._dragRef}> - <Flyout anchorPoint={anchorPoints.LEFT_TOP} - content={<TemplateMenu docViews={this.props.views.filter(v => v).map(v => v as DocumentView)} templates={templates} />}> - <div className={"documentButtonBar-linkButton-" + "empty"} ref={this._dragRef} onPointerDown={this.onAliasButtonDown} > + <Flyout anchorPoint={anchorPoints.LEFT_TOP} onOpen={action(() => this._aliasDown = true)} onClose={action(() => this._aliasDown = false)} + content={!this._aliasDown ? (null) : <TemplateMenu docViews={this.props.views.filter(v => v).map(v => v as DocumentView)} templates={templates} />}> + <div className={"documentButtonBar-linkButton-empty"} ref={this._dragRef} onPointerDown={this.onAliasButtonDown} > {<FontAwesomeIcon className="documentdecorations-icon" icon="edit" size="sm" />} </div> </Flyout> diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index c49fe157c..e1348a317 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -3,12 +3,11 @@ import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote, faT import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, computed, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DataSym } from "../../new_fields/Doc"; +import { Doc, DataSym, Field } from "../../new_fields/Doc"; import { PositionDocument } from '../../new_fields/documentSchemas'; import { ScriptField } from '../../new_fields/ScriptField'; import { Cast, StrCast, NumCast } from "../../new_fields/Types"; -import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils'; -import { Utils, setupMoveUpEvents, emptyFunction, returnFalse } from "../../Utils"; +import { Utils, setupMoveUpEvents, emptyFunction, returnFalse, simulateMouseClick } from "../../Utils"; import { DocUtils } from "../documents/Documents"; import { DocumentType } from '../documents/DocumentTypes'; import { DragManager } from "../util/DragManager"; @@ -20,6 +19,7 @@ import { DocumentView } from "./nodes/DocumentView"; import React = require("react"); import { Id } from '../../new_fields/FieldSymbols'; import e = require('express'); +import { CollectionDockingView } from './collections/CollectionDockingView'; library.add(faCaretUp); library.add(faObjectGroup); @@ -37,8 +37,6 @@ library.add(faCloudUploadAlt); library.add(faSyncAlt); library.add(faShare); -export type CloseCall = (toBeDeleted: DocumentView[]) => void; - @observer export class DocumentDecorations extends React.Component<{}, { value: string }> { static Instance: DocumentDecorations; @@ -52,7 +50,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> @observable private _titleControlString: string = "#title"; @observable private _edtingTitle = false; @observable private _hidden = false; - @observable private _addedCloseCalls: CloseCall[] = []; @observable public Interacting = false; @observable public pushIcon: IconProp = "arrow-alt-circle-up"; @@ -69,7 +66,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> get Bounds(): { x: number, y: number, b: number, r: number } { return SelectionManager.SelectedDocuments().reduce((bounds, documentView) => { if (documentView.props.renderDepth === 0 || - Doc.AreProtosEqual(documentView.props.Document, CurrentUserUtils.UserDocument)) { + Doc.AreProtosEqual(documentView.props.Document, Doc.UserDoc())) { return bounds; } const transform = (documentView.props.ScreenToLocalTransform().scale(documentView.props.ContentScaling())).inverse(); @@ -92,14 +89,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> }, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: Number.MIN_VALUE, b: Number.MIN_VALUE }); } - addCloseCall = (handler: CloseCall) => { - const currentOffset = this._addedCloseCalls.length - 1; - this._addedCloseCalls.push((toBeDeleted: DocumentView[]) => { - this._addedCloseCalls.splice(currentOffset, 1); - handler(toBeDeleted); - }); - } - titleBlur = action((commit: boolean) => { this._edtingTitle = false; if (commit) { @@ -142,40 +131,15 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> @action onSettingsDown = (e: React.PointerEvent): void => { setupMoveUpEvents(this, e, () => false, (e) => { }, this.onSettingsClick); } - - simulateMouseClick(element: Element, x: number, y: number, sx: number, sy: number) { - ["pointerdown", "pointerup"].map(event => element.dispatchEvent( - new PointerEvent(event, { - view: window, - bubbles: true, - cancelable: true, - button: 2, - pointerType: "mouse", - clientX: x, - clientY: y, - screenX: sx, - screenY: sy, - }))); - - element.dispatchEvent( - new MouseEvent("contextmenu", { - view: window, - bubbles: true, - cancelable: true, - button: 2, - clientX: x, - clientY: y, - movementX: 0, - movementY: 0, - screenX: sx, - screenY: sy, - })); - } @action onSettingsClick = (e: PointerEvent): void => { if (e.button === 0 && !e.altKey && !e.ctrlKey) { let child = SelectionManager.SelectedDocuments()[0].ContentDiv!.children[0]; - while (child.children.length && child.className !== "jsx-parser") child = child.children[0]; - this.simulateMouseClick(child.children[0], e.clientX, e.clientY + 30, e.screenX, e.screenY + 30); + while (child.children.length) { + const next = Array.from(child.children).find(c => !c.className.includes("collectionViewChrome")); + if (next) child = next; + else break; + } + simulateMouseClick(child, e.clientX, e.clientY + 30, e.screenX, e.screenY + 30); } } @@ -193,7 +157,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> dragData.isSelectionMove = true; this.Interacting = true; this._hidden = true; - DragManager.StartDocumentDrag(SelectionManager.SelectedDocuments().map(documentView => documentView.ContentDiv!), dragData, e.x, e.y, { + DragManager.StartDocumentDrag(SelectionManager.SelectedDocuments().map(dv => dv.ContentDiv!), dragData, e.x, e.y, { dragComplete: action(e => this._hidden = this.Interacting = false), hideSource: true }); @@ -201,26 +165,36 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> } onCloseDown = (e: React.PointerEvent): void => { - setupMoveUpEvents(this, e, (e, d) => false, (e) => { }, this.onCloseClick); + setupMoveUpEvents(this, e, (e, d) => false, (e) => { }, this.onMinimizeClick); } @undoBatch @action onCloseClick = async (e: PointerEvent) => { if (e.button === 0) { - const recent = Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc) as Doc; + const recent = Cast(Doc.UserDoc().myRecentlyClosed, Doc) as Doc; const selected = SelectionManager.SelectedDocuments().slice(); SelectionManager.DeselectAll(); - this._addedCloseCalls.forEach(handler => handler(selected)); selected.map(dv => { recent && Doc.AddDocToList(recent, "data", dv.props.Document, undefined, true, true); - dv.props.removeDocument && dv.props.removeDocument(dv.props.Document); + dv.props.removeDocument?.(dv.props.Document); }); } } @action - onMinimizeDown = (e: React.PointerEvent): void => { - setupMoveUpEvents(this, e, (e, d) => false, (e) => { }, this.onMinimizeClick); + onMaximizeDown = (e: React.PointerEvent): void => { + setupMoveUpEvents(this, e, (e, d) => false, (e) => { }, this.onMaximizeClick); + } + @undoBatch + @action + onMaximizeClick = (e: PointerEvent): void => { + if (e.button === 0) { + const selectedDocs = SelectionManager.SelectedDocuments(); + if (selectedDocs.length) { + CollectionDockingView.Instance?.OpenFullScreen(selectedDocs[0], selectedDocs[0].props.LibraryPath); + } + } + SelectionManager.DeselectAll(); } @undoBatch @action @@ -396,7 +370,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> return ScriptField.MakeFunction(this._titleControlString.substring(1), { doc: Doc.name })!.script.run({ self: selected.rootDoc, this: selected.layoutDoc }, console.log).result?.toString() || ""; } if (this._titleControlString.startsWith("#")) { - return selected.props.Document[this._titleControlString.substring(1)]?.toString() || "-unset-"; + return Field.toString(selected.props.Document[this._titleControlString.substring(1)] as Field) || "-unset-"; } return this._accumulatedTitle; } else if (SelectionManager.SelectedDocuments().length > 1) { @@ -428,11 +402,11 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> return (null); } const minimal = bounds.r - bounds.x < 100 ? true : false; - const minimizeIcon = minimal ? ( + const maximizeIcon = minimal ? ( <div className="documentDecorations-contextMenu" title="Show context menu" onPointerDown={this.onSettingsDown}> <FontAwesomeIcon size="lg" icon="cog" /> </div>) : ( - <div className="documentDecorations-minimizeButton" title="Iconify" onPointerDown={this.onMinimizeDown}> + <div className="documentDecorations-minimizeButton" title="Iconify" onPointerDown={this.onMaximizeDown}> {/* Currently, this is set to be enabled if there is no ink selected. It might be interesting to think about minimizing ink if it's useful? -syip2*/} {SelectionManager.SelectedDocuments().length === 1 ? DocumentDecorations.DocumentIcon(StrCast(seldoc.props.Document.layout, "...")) : "..."} </div>); @@ -487,7 +461,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> left: bounds.x - this._resizeBorderWidth / 2, top: bounds.y - this._resizeBorderWidth / 2 - this._titleHeight, }}> - {minimizeIcon} + {maximizeIcon} {titleArea} <div className="documentDecorations-closeButton" title="Close Document" onPointerDown={this.onCloseDown}> <FontAwesomeIcon className="documentdecorations-times" icon={faTimes} size="lg" /> diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index d01f3f1e5..185222541 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -7,7 +7,6 @@ import { action, runInAction } from "mobx"; import { Doc } from "../../new_fields/Doc"; import { DictationManager } from "../util/DictationManager"; import SharingManager from "../util/SharingManager"; -import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils"; import { Cast, PromiseValue, NumCast } from "../../new_fields/Types"; import { ScriptField } from "../../new_fields/ScriptField"; import { InkingControl } from "./InkingControl"; @@ -194,7 +193,7 @@ export default class KeyManager { } break; case "t": - PromiseValue(Cast(CurrentUserUtils.UserDocument.Create, Doc)).then(pv => pv && (pv.onClick as ScriptField).script.run({ this: pv })); + PromiseValue(Cast(Doc.UserDoc()["tabs-button-tools"], Doc)).then(pv => pv && (pv.onClick as ScriptField).script.run({ this: pv })); if (MainView.Instance.flyoutWidth === 240) { MainView.Instance.flyoutWidth = 0; } else { @@ -202,7 +201,7 @@ export default class KeyManager { } break; case "l": - PromiseValue(Cast(CurrentUserUtils.UserDocument.Library, Doc)).then(pv => pv && (pv.onClick as ScriptField).script.run({ this: pv })); + PromiseValue(Cast(Doc.UserDoc()["tabs-button-library"], Doc)).then(pv => pv && (pv.onClick as ScriptField).script.run({ this: pv })); if (MainView.Instance.flyoutWidth === 250) { MainView.Instance.flyoutWidth = 0; } else { @@ -210,7 +209,7 @@ export default class KeyManager { } break; case "f": - PromiseValue(Cast(CurrentUserUtils.UserDocument.Search, Doc)).then(pv => pv && (pv.onClick as ScriptField).script.run({ this: pv })); + PromiseValue(Cast(Doc.UserDoc()["tabs-button-search"], Doc)).then(pv => pv && (pv.onClick as ScriptField).script.run({ this: pv })); if (MainView.Instance.flyoutWidth === 400) { MainView.Instance.flyoutWidth = 0; } else { diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx index 645c7fa54..172c1864a 100644 --- a/src/client/views/InkingControl.tsx +++ b/src/client/views/InkingControl.tsx @@ -12,9 +12,9 @@ import { FormattedTextBox } from "./nodes/FormattedTextBox"; export class InkingControl { @observable static Instance: InkingControl; - @computed private get _selectedTool(): InkTool { return FieldValue(NumCast(CurrentUserUtils.UserDocument.inkTool)) ?? InkTool.None; } - @computed private get _selectedColor(): string { return GestureOverlay.Instance.Color ?? FieldValue(StrCast(CurrentUserUtils.UserDocument.inkColor)) ?? "rgb(244, 67, 54)"; } - @computed private get _selectedWidth(): string { return GestureOverlay.Instance.Width?.toString() ?? FieldValue(StrCast(CurrentUserUtils.UserDocument.inkWidth)) ?? "5"; } + @computed private get _selectedTool(): InkTool { return FieldValue(NumCast(Doc.UserDoc().inkTool)) ?? InkTool.None; } + @computed private get _selectedColor(): string { return GestureOverlay.Instance.Color ?? FieldValue(StrCast(Doc.UserDoc().inkColor)) ?? "rgb(244, 67, 54)"; } + @computed private get _selectedWidth(): string { return GestureOverlay.Instance.Width?.toString() ?? FieldValue(StrCast(Doc.UserDoc().inkWidth)) ?? "5"; } @observable public _open: boolean = false; constructor() { @@ -23,7 +23,7 @@ export class InkingControl { switchTool = action((tool: InkTool): void => { // this._selectedTool = tool; - CurrentUserUtils.UserDocument.inkTool = tool; + Doc.UserDoc().inkTool = tool; }); decimalToHexString(number: number) { if (number < 0) { @@ -34,7 +34,7 @@ export class InkingControl { @undoBatch switchColor = action((color: ColorState): void => { - CurrentUserUtils.UserDocument.inkColor = color.hex + (color.rgb.a !== undefined ? this.decimalToHexString(Math.round(color.rgb.a * 255)) : "ff"); + Doc.UserDoc().inkColor = color.hex + (color.rgb.a !== undefined ? this.decimalToHexString(Math.round(color.rgb.a * 255)) : "ff"); if (InkingControl.Instance.selectedTool === InkTool.None) { const selected = SelectionManager.SelectedDocuments(); @@ -44,9 +44,9 @@ export class InkingControl { view.props.Document.isTemplateForField ? view.props.Document : Doc.GetProto(view.props.Document); if (targetDoc) { if (StrCast(Doc.Layout(view.props.Document).layout).indexOf("FormattedTextBox") !== -1 && FormattedTextBox.HadSelection) { - Doc.Layout(view.props.Document).color = CurrentUserUtils.UserDocument.inkColor; + Doc.Layout(view.props.Document).color = Doc.UserDoc().inkColor; } else { - Doc.Layout(view.props.Document)._backgroundColor = CurrentUserUtils.UserDocument.inkColor; // '_backgroundColor' is template specific. 'backgroundColor' would apply to all templates, but has no UI at the moment + Doc.Layout(view.props.Document)._backgroundColor = Doc.UserDoc().inkColor; // '_backgroundColor' is template specific. 'backgroundColor' would apply to all templates, but has no UI at the moment } } }); @@ -57,7 +57,7 @@ export class InkingControl { @action switchWidth = (width: string): void => { // this._selectedWidth = width; - CurrentUserUtils.UserDocument.inkWidth = width; + Doc.UserDoc().inkWidth = width; } @computed @@ -73,7 +73,7 @@ export class InkingControl { @action updateSelectedColor(value: string) { // this._selectedColor = value; - CurrentUserUtils.UserDocument.inkColor = value; + Doc.UserDoc().inkColor = value; } @computed diff --git a/src/client/views/InkingStroke.scss b/src/client/views/InkingStroke.scss index cdbfdcff3..433433a42 100644 --- a/src/client/views/InkingStroke.scss +++ b/src/client/views/InkingStroke.scss @@ -1,3 +1,7 @@ -.inkingStroke-marker { - mix-blend-mode: multiply +.inkingStroke { + mix-blend-mode: multiply; + stroke-linejoin: round; + stroke-linecap: round; + overflow: visible !important; + transform-origin: top left; }
\ No newline at end of file diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index f66c04e1f..7a318d5c2 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -1,4 +1,3 @@ -import { computed } from "mobx"; import { observer } from "mobx-react"; import { documentSchema } from "../../new_fields/documentSchemas"; import { InkData, InkField, InkTool } from "../../new_fields/InkField"; @@ -47,14 +46,12 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume const scaleX = this.props.PanelWidth() / width; const scaleY = this.props.PanelHeight() / height; return ( - <svg + <svg className="inkingStroke" width={width} height={height} style={{ - transformOrigin: "top left", transform: `scale(${scaleX}, ${scaleY})`, mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? "multiply" : "unset", - pointerEvents: "all" }} onContextMenu={() => { ContextMenu.Instance.addItem({ diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss index 4709e7ef2..a2a9ceca5 100644 --- a/src/client/views/Main.scss +++ b/src/client/views/Main.scss @@ -24,7 +24,6 @@ body { .jsx-parser { width: 100%; height: 100%; - pointer-events: none; border-radius: inherit; position: inherit; // background: inherit; diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss index e95802e54..81d427f64 100644 --- a/src/client/views/MainView.scss +++ b/src/client/views/MainView.scss @@ -5,6 +5,7 @@ .mainView-tabButtons { position: relative; width: 100%; + margin-top: 10px; } .mainContent-div { @@ -28,6 +29,7 @@ width: 100%; height: 100%; position: absolute; + pointer-events: all; top: 0; left: 0; z-index: 1; @@ -71,6 +73,7 @@ flex-direction: column; position: relative; height: 100%; + background: dimgray; .documentView-node-topmost { background: lightgrey; diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index aec1f960a..8fb67c435 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -1,5 +1,5 @@ import { library } from '@fortawesome/fontawesome-svg-core'; -import { faTerminal, faArrowDown, faArrowUp, faBolt, faBullseye, faCaretUp, faCat, faCheck, faChevronRight, faClipboard, faClone, faCloudUploadAlt, faCommentAlt, faCompressArrowsAlt, faCut, faEllipsisV, faEraser, faExclamation, faFileAlt, faFileAudio, faFilePdf, faFilm, faFilter, faFont, faGlobeAsia, faHighlighter, faLongArrowAltRight, faMicrophone, faMousePointer, faMusic, faObjectGroup, faPause, faPen, faPenNib, faPhone, faPlay, faPortrait, faRedoAlt, faStamp, faStickyNote, faThumbtack, faTree, faTv, faUndoAlt, faVideo } from '@fortawesome/free-solid-svg-icons'; +import { faTerminal, faCalculator, faWindowMaximize, faAddressCard, faQuestionCircle, faArrowDown, faArrowUp, faBolt, faBullseye, faCaretUp, faCat, faCheck, faChevronRight, faClipboard, faClone, faCloudUploadAlt, faCommentAlt, faCompressArrowsAlt, faCut, faEllipsisV, faEraser, faExclamation, faFileAlt, faFileAudio, faFilePdf, faFilm, faFilter, faFont, faGlobeAsia, faHighlighter, faLongArrowAltRight, faMicrophone, faMousePointer, faMusic, faObjectGroup, faPause, faPen, faPenNib, faPhone, faPlay, faPortrait, faRedoAlt, faStamp, faStickyNote, faThumbtack, faTree, faTv, faUndoAlt, faVideo } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, configure, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; @@ -41,11 +41,12 @@ import { RadialMenu } from './nodes/RadialMenu'; import { OverlayView } from './OverlayView'; import PDFMenu from './pdf/PDFMenu'; import { PreviewCursor } from './PreviewCursor'; +import { ScriptField } from '../../new_fields/ScriptField'; @observer export class MainView extends React.Component { public static Instance: MainView; - private _buttonBarHeight = 35; + private _buttonBarHeight = 26; private _flyoutSizeOnDown = 0; private _urlState: HistoryUtil.DocUrl; private _docBtnRef = React.createRef<HTMLDivElement>(); @@ -60,7 +61,7 @@ export class MainView extends React.Component { @computed private get userDoc() { return Doc.UserDoc(); } @computed private get mainContainer() { return this.userDoc ? FieldValue(Cast(this.userDoc.activeWorkspace, Doc)) : CurrentUserUtils.GuestWorkspace; } @computed public get mainFreeform(): Opt<Doc> { return (docs => (docs && docs.length > 1) ? docs[1] : undefined)(DocListCast(this.mainContainer!.data)); } - @computed public get sidebarButtonsDoc() { return Cast(CurrentUserUtils.UserDocument.sidebarButtons, Doc) as Doc; } + @computed public get sidebarButtonsDoc() { return Cast(this.userDoc["tabs-buttons"], Doc) as Doc; } public isPointerDown = false; @@ -102,7 +103,11 @@ export class MainView extends React.Component { } library.add(faTerminal); + library.add(faCalculator); + library.add(faWindowMaximize); library.add(faFileAlt); + library.add(faAddressCard); + library.add(faQuestionCircle); library.add(faStickyNote); library.add(faFont); library.add(faExclamation); @@ -200,7 +205,7 @@ export class MainView extends React.Component { @action createNewWorkspace = async (id?: string) => { - const workspaces = Cast(this.userDoc.workspaces, Doc) as Doc; + const workspaces = Cast(this.userDoc.myWorkspaces, Doc) as Doc; const workspaceCount = DocListCast(workspaces.data).length + 1; const freeformOptions: DocumentOptions = { x: 0, @@ -210,8 +215,13 @@ export class MainView extends React.Component { title: "Collection " + workspaceCount, }; const freeformDoc = CurrentUserUtils.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions); - Doc.AddDocToList(Doc.GetProto(CurrentUserUtils.UserDocument.documents as Doc), "data", freeformDoc); - const mainDoc = Docs.Create.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600, path: [Doc.UserDoc().documents as Doc] }], { title: `Workspace ${workspaceCount}` }, id, "row"); + Doc.AddDocToList(Doc.GetProto(Doc.UserDoc().myDocuments as Doc), "data", freeformDoc); + const mainDoc = Docs.Create.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600, path: [Doc.UserDoc().myDocuments as Doc] }], { title: `Workspace ${workspaceCount}` }, id, "row"); + + const toggleTheme = ScriptField.MakeScript(`self.darkScheme = !self.darkScheme`); + mainDoc.contextMenuScripts = new List<ScriptField>([toggleTheme!]); + mainDoc.contextMenuLabels = new List<string>(["Toggle Theme Colors"]); + Doc.AddDocToList(workspaces, "data", mainDoc); // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container) setTimeout(() => this.openWorkspace(mainDoc), 0); @@ -252,7 +262,7 @@ export class MainView extends React.Component { } // if there is a pending doc, and it has new data, show it (syip: we use a timeout to prevent collection docking view from being uninitialized) setTimeout(async () => { - const col = this.userDoc && await Cast(this.userDoc.optionalRightCollection, Doc); + const col = this.userDoc && await Cast(this.userDoc.rightSidebarCollection, Doc); col && Cast(col.data, listSpec(Doc)) && runInAction(() => MainViewNotifs.NotifsCol = col); }, 100); return true; @@ -390,15 +400,14 @@ export class MainView extends React.Component { mainContainerXf = () => new Transform(0, -this._buttonBarHeight, 1); @computed get flyout() { - const sidebarContent = this.userDoc?.sidebarContainer; + const sidebarContent = this.userDoc?.["tabs-panelContainer"]; if (!(sidebarContent instanceof Doc)) { return (null); } - const sidebarButtonsDoc = Cast(CurrentUserUtils.UserDocument.sidebarButtons, Doc) as Doc; return <div className="mainView-flyoutContainer" > - <div className="mainView-tabButtons" style={{ height: `${this._buttonBarHeight}px`, backgroundColor: StrCast(sidebarButtonsDoc.backgroundColor) }}> + <div className="mainView-tabButtons" style={{ height: `${this._buttonBarHeight}px`, backgroundColor: StrCast(this.sidebarButtonsDoc.backgroundColor) }}> <DocumentView - Document={sidebarButtonsDoc} + Document={this.sidebarButtonsDoc} DataDoc={undefined} LibraryPath={emptyPath} addDocument={undefined} @@ -459,7 +468,7 @@ export class MainView extends React.Component { } @computed get mainContent() { - const sidebar = this.userDoc && this.userDoc.sidebarContainer; + const sidebar = this.userDoc?.["tabs-panelContainer"]; return !this.userDoc || !(sidebar instanceof Doc) ? (null) : ( <div className="mainView-mainContent" style={{ color: this.darkScheme ? "rgb(205,205,205)" : "black" }} > <div className="mainView-flyoutContainer" onPointerLeave={this.pointerLeaveDragger} style={{ width: this.flyoutWidth }}> @@ -496,8 +505,8 @@ export class MainView extends React.Component { return !this._flyoutTranslate ? (<div className="mainView-expandFlyoutButton" title="Re-attach sidebar" onPointerDown={MainView.expandFlyout}><FontAwesomeIcon icon="chevron-right" color="grey" size="lg" /></div>) : (null); } - addButtonDoc = (doc: Doc) => Doc.AddDocToList(CurrentUserUtils.UserDocument.expandingButtons as Doc, "data", doc); - remButtonDoc = (doc: Doc) => Doc.RemoveDocFromList(CurrentUserUtils.UserDocument.expandingButtons as Doc, "data", doc); + addButtonDoc = (doc: Doc) => Doc.AddDocToList(Doc.UserDoc().dockedBtns as Doc, "data", doc); + remButtonDoc = (doc: Doc) => Doc.RemoveDocFromList(Doc.UserDoc().dockedBtns as Doc, "data", doc); moveButtonDoc = (doc: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean) => this.remButtonDoc(doc) && addDocument(doc); buttonBarXf = () => { @@ -506,13 +515,13 @@ export class MainView extends React.Component { return new Transform(-translateX, -translateY, 1 / scale); } @computed get docButtons() { - const expandingBtns = Doc.UserDoc()?.expandingButtons; - if (expandingBtns instanceof Doc) { + const dockedBtns = Doc.UserDoc()?.dockedBtns; + if (dockedBtns instanceof Doc) { return <div className="mainView-docButtons" ref={this._docBtnRef} - style={{ height: !expandingBtns.linearViewIsExpanded ? "42px" : undefined }} > + style={{ height: !dockedBtns.linearViewIsExpanded ? "42px" : undefined }} > <MainViewNotifs /> <CollectionLinearView - Document={expandingBtns} + Document={dockedBtns} DataDoc={undefined} LibraryPath={emptyPath} fieldKey={"data"} diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx index 4000cade5..20aa14f84 100644 --- a/src/client/views/OverlayView.tsx +++ b/src/client/views/OverlayView.tsx @@ -1,16 +1,16 @@ -import * as React from "react"; +import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; -import { observable, action, trace, computed } from "mobx"; -import { Utils, emptyFunction, returnOne, returnTrue, returnEmptyString, returnZero, returnFalse, emptyPath } from "../../Utils"; - -import './OverlayView.scss'; -import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils"; -import { DocListCast, Doc } from "../../new_fields/Doc"; +import * as React from "react"; +import { Doc, DocListCast } from "../../new_fields/Doc"; import { Id } from "../../new_fields/FieldSymbols"; -import { DocumentView } from "./nodes/DocumentView"; -import { Transform } from "../util/Transform"; import { NumCast } from "../../new_fields/Types"; +import { emptyFunction, emptyPath, returnEmptyString, returnFalse, returnOne, returnTrue, returnZero, Utils } from "../../Utils"; +import { Transform } from "../util/Transform"; import { CollectionFreeFormLinksView } from "./collections/collectionFreeForm/CollectionFreeFormLinksView"; +import { DocumentView } from "./nodes/DocumentView"; +import './OverlayView.scss'; +import { Scripting } from "../util/Scripting"; +import { ScriptingRepl } from './ScriptingRepl'; export type OverlayDisposer = () => void; @@ -140,10 +140,11 @@ export class OverlayView extends React.Component { } @computed get overlayDocs() { - if (!CurrentUserUtils.UserDocument) { + const userDocOverlays = Doc.UserDoc().myOverlayDocuments; + if (!userDocOverlays) { return (null); } - return CurrentUserUtils.UserDocument.overlays instanceof Doc && DocListCast(CurrentUserUtils.UserDocument.overlays.data).map(d => { + return userDocOverlays instanceof Doc && DocListCast(userDocOverlays.data).map(d => { setTimeout(() => d.inOverlay = true, 0); let offsetx = 0, offsety = 0; const onPointerMove = action((e: PointerEvent) => { @@ -195,7 +196,7 @@ export class OverlayView extends React.Component { addDocTab={returnFalse} pinToPres={emptyFunction} ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined}/> + ContainingCollectionDoc={undefined} /> </div>; }); } @@ -211,4 +212,6 @@ export class OverlayView extends React.Component { </div> ); } -}
\ No newline at end of file +} +// bcz: ugh ... want to be able to pass ScriptingRepl as tag argument, but that doesn't seem to work.. runtime error +Scripting.addGlobal(function addOverlayWindow(Tag: string, options: OverlayElementOptions) { const x = <ScriptingRepl />; OverlayView.Instance.addWindow(x, options); });
\ No newline at end of file diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx index ecebadd2a..df30c1215 100644 --- a/src/client/views/PreviewCursor.tsx +++ b/src/client/views/PreviewCursor.tsx @@ -13,7 +13,7 @@ export class PreviewCursor extends React.Component<{}> { static _getTransform: () => Transform; static _addLiveTextDoc: (doc: Doc) => void; static _addDocument: (doc: Doc) => boolean; - static _nudge: (x: number, y: number) => void; + static _nudge: (x: number, y: number) => boolean; @observable static _clickPoint = [0, 0]; @observable public static Visible = false; constructor(props: any) { @@ -91,17 +91,13 @@ export class PreviewCursor extends React.Component<{}> { } } else if (PreviewCursor.Visible) { if (e.key === "ArrowRight") { - PreviewCursor._nudge?.(1 * (e.shiftKey ? 2 : 1), 0); - e.stopPropagation(); + PreviewCursor._nudge?.(1 * (e.shiftKey ? 2 : 1), 0) && e.stopPropagation(); } else if (e.key === "ArrowLeft") { - PreviewCursor._nudge?.(-1 * (e.shiftKey ? 2 : 1), 0); - e.stopPropagation(); + PreviewCursor._nudge?.(-1 * (e.shiftKey ? 2 : 1), 0) && e.stopPropagation(); } else if (e.key === "ArrowUp") { - PreviewCursor._nudge?.(0, 1 * (e.shiftKey ? 2 : 1)); - e.stopPropagation(); + PreviewCursor._nudge?.(0, 1 * (e.shiftKey ? 2 : 1)) && e.stopPropagation(); } else if (e.key === "ArrowDown") { - PreviewCursor._nudge?.(0, -1 * (e.shiftKey ? 2 : 1)); - e.stopPropagation(); + PreviewCursor._nudge?.(0, -1 * (e.shiftKey ? 2 : 1)) && e.stopPropagation(); } } } @@ -117,7 +113,7 @@ export class PreviewCursor extends React.Component<{}> { addLiveText: (doc: Doc) => void, getTransform: () => Transform, addDocument: (doc: Doc) => boolean, - nudge: (nudgeX: number, nudgeY: number) => void) { + nudge: (nudgeX: number, nudgeY: number) => boolean) { this._clickPoint = [x, y]; this._onKeyPress = onKeyPress; this._addLiveTextDoc = addLiveText; diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx index 4d7f1e443..6894500dd 100644 --- a/src/client/views/TemplateMenu.tsx +++ b/src/client/views/TemplateMenu.tsx @@ -124,7 +124,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { templateMenu.push(<OtherToggle key={"chrome"} name={"Chrome"} checked={layout._chromeStatus !== "disabled"} toggle={this.toggleChrome} />); templateMenu.push(<OtherToggle key={"default"} name={"Default"} checked={templateName === "layout"} toggle={this.toggleDefault} />); if (noteTypesDoc) { - addedTypes.concat(noteTypes).map(template => template.treeViewChecked = ComputedField.MakeFunction(`templateIsUsed(self)`)); + addedTypes.concat(noteTypes).map(template => template.treeViewChecked = ComputedField.MakeFunction(`templateIsUsed(self,firstDoc)`, {}, { firstDoc })); this._addedKeys && Array.from(this._addedKeys).filter(key => !noteTypes.some(nt => nt.title === key)).forEach(template => templateMenu.push( <OtherToggle key={template} name={template} checked={templateName === template} toggle={e => this.toggleLayout(e, template)} />)); } @@ -167,19 +167,18 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { } } -Scripting.addGlobal(function switchView(doc: Doc, template: Doc) { - if (template.dragFactory) { +Scripting.addGlobal(function switchView(doc: Doc, template: Doc | undefined) { + if (template?.dragFactory) { template = Cast(template.dragFactory, Doc, null); } const templateTitle = StrCast(template?.title); return templateTitle && DocumentView.makeCustomViewClicked(doc, Docs.Create.FreeformDocument, templateTitle, template); }); -Scripting.addGlobal(function templateIsUsed(templateDoc: Doc) { - const firstDoc = SelectionManager.SelectedDocuments().length ? SelectionManager.SelectedDocuments()[0].props.Document : undefined; - if (firstDoc) { +Scripting.addGlobal(function templateIsUsed(templateDoc: Doc, selDoc: Doc) { + if (selDoc) { const template = StrCast(templateDoc.dragFactory ? Cast(templateDoc.dragFactory, Doc, null)?.title : templateDoc.title); - return StrCast(firstDoc.layoutKey) === "layout_" + template ? 'check' : 'unchecked'; + return StrCast(selDoc.layoutKey) === "layout_" + template ? 'check' : 'unchecked'; } return false; });
\ No newline at end of file diff --git a/src/client/views/Templates.tsx b/src/client/views/Templates.tsx index 8c60f1c36..a6dbaa650 100644 --- a/src/client/views/Templates.tsx +++ b/src/client/views/Templates.tsx @@ -1,45 +1,23 @@ -import React = require("react"); - -export enum TemplatePosition { - InnerTop, - InnerBottom, - InnerRight, - InnerLeft, - TopRight, - OutterTop, - OutterBottom, - OutterRight, - OutterLeft, -} - export class Template { - constructor(name: string, position: TemplatePosition, layout: string) { + constructor(name: string, layout: string) { this._name = name; - this._position = position; this._layout = layout; } private _name: string; - private _position: TemplatePosition; private _layout: string; get Name(): string { return this._name; } - get Position(): TemplatePosition { - return this._position; - } - get Layout(): string { return this._layout; } } export namespace Templates { - // export const BasicLayout = new Template("Basic layout", "{layout}"); - - export const Caption = new Template("Caption", TemplatePosition.OutterBottom, + export const Caption = new Template("Caption", `<div> <div style="height:100%; width:100%;">{layout}</div> <div style="bottom: 0; font-size:14px; width:100%; position:absolute"> @@ -47,16 +25,7 @@ export namespace Templates { </div> </div>` ); - export const Title = new Template("Title", TemplatePosition.InnerTop, - `<div> - <div style="height:25px; width:100%; background-color: rgba(0, 0, 0, .4); color: white; z-index: 100"> - <span style="text-align:center;width:100%;font-size:20px;position:absolute;overflow:hidden;white-space:nowrap;text-overflow:ellipsis">{props.Document.title}</span> - </div> - <div style="height:calc(100% - 25px);"> - <div style="width:100%;overflow:auto">{layout}</div> - </div> - </div>` ); - export const TitleHover = new Template("TitleHover", TemplatePosition.InnerTop, + export const Title = new Template("Title", `<div> <div style="height:25px; width:100%; background-color: rgba(0, 0, 0, .4); color: white; z-index: 100"> <span style="text-align:center;width:100%;font-size:20px;position:absolute;overflow:hidden;white-space:nowrap;text-overflow:ellipsis">{props.Document.title}</span> @@ -65,14 +34,8 @@ export namespace Templates { <div style="width:100%;overflow:auto">{layout}</div> </div> </div>` ); + export const TitleHover = new Template("TitleHover", Title.Layout); export const TemplateList: Template[] = [Title, TitleHover, Caption]; - - export function sortTemplates(a: Template, b: Template) { - if (a.Position < b.Position) { return -1; } - if (a.Position > b.Position) { return 1; } - return 0; - } - } diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index d77ef812f..0d859c3f1 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -14,11 +14,9 @@ import { List } from '../../../new_fields/List'; import { FieldId } from "../../../new_fields/RefField"; import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; import { TraceMobx } from '../../../new_fields/util'; -import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; import { emptyFunction, returnOne, returnTrue, Utils, returnZero } from "../../../Utils"; import { DocServer } from "../../DocServer"; import { Docs } from '../../documents/Documents'; -import { DocumentType } from '../../documents/DocumentTypes'; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager, dropActionType } from "../../util/DragManager"; import { Scripting } from '../../util/Scripting'; @@ -31,6 +29,7 @@ import "./CollectionDockingView.scss"; import { SubCollectionViewProps } from "./CollectionSubView"; import { DockingViewButtonSelector } from './ParentDocumentSelector'; import React = require("react"); +import { CollectionViewType } from './CollectionView'; library.add(faFile); const _global = (window /* browser */ || global /* node */) as any; @@ -95,6 +94,9 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp @undoBatch @action public OpenFullScreen(docView: DocumentView, libraryPath?: Doc[]) { + if (docView.props.Document._viewType === CollectionViewType.Docking && docView.props.Document.layoutKey === "layout") { + return MainView.Instance.openWorkspace(docView.props.Document); + } const document = Doc.MakeAlias(docView.props.Document); const newItemStackConfig = { type: 'stack', @@ -376,8 +378,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp // Because this is in a set timeout, if this component unmounts right after mounting, // we will leak a GoldenLayout, because we try to destroy it before we ever create it setTimeout(() => this.setupGoldenLayout(), 1); - const userDoc = CurrentUserUtils.UserDocument; - userDoc && DocListCast((userDoc.workspaces as Doc).data).map(d => d.workspaceBrush = false); + DocListCast((Doc.UserDoc().myWorkspaces as Doc).data).map(d => d.workspaceBrush = false); this.props.Document.workspaceBrush = true; } this._ignoreStateChange = ""; @@ -544,9 +545,8 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp const theDoc = doc; CollectionDockingView.Instance._removedDocs.push(theDoc); - const userDoc = CurrentUserUtils.UserDocument; - let recent: Doc | undefined; - if (userDoc && (recent = await Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc))) { + const recent = await Cast(Doc.UserDoc().myRecentlyClosed, Doc); + if (recent) { Doc.AddDocToList(recent, "data", doc, undefined, true, true); } SelectionManager.DeselectAll(); @@ -606,7 +606,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp const doc = await DocServer.GetRefField(contentItem.config.props.documentId); if (doc instanceof Doc) { let recent: Doc | undefined; - if (CurrentUserUtils.UserDocument && (recent = await Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc))) { + if (recent = await Cast(Doc.UserDoc().myRecentlyClosed, Doc)) { Doc.AddDocToList(recent, "data", doc, undefined, true, true); } const theDoc = doc; @@ -681,7 +681,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { @action public static PinDoc(doc: Doc) { //add this new doc to props.Document - const curPres = Cast(CurrentUserUtils.UserDocument.curPresentation, Doc) as Doc; + const curPres = Cast(Doc.UserDoc().activePresentation, Doc) as Doc; if (curPres) { const pinDoc = Doc.MakeAlias(doc); pinDoc.presentationTargetDoc = doc; @@ -698,7 +698,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { @action public static UnpinDoc(doc: Doc) { //add this new doc to props.Document - const curPres = Cast(CurrentUserUtils.UserDocument.curPresentation, Doc) as Doc; + const curPres = Cast(Doc.UserDoc().activePresentation, Doc) as Doc; if (curPres) { const ind = DocListCast(curPres.data).findIndex((val) => Doc.AreProtosEqual(val, doc)); ind !== -1 && Doc.RemoveDocFromList(curPres, "data", DocListCast(curPres.data)[ind]); @@ -739,19 +739,27 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { nativeHeight = () => !this.layoutDoc!._fitWidth ? NumCast(this.layoutDoc!._nativeHeight) || this._panelHeight : 0; contentScaling = () => { - if (this.layoutDoc!.type === DocumentType.PDF) { - if ((this.layoutDoc && this.layoutDoc._fitWidth) || - this._panelHeight / NumCast(this.layoutDoc!._nativeHeight) > this._panelWidth / NumCast(this.layoutDoc!._nativeWidth)) { - return this._panelWidth / NumCast(this.layoutDoc!._nativeWidth); - } else { - return this._panelHeight / NumCast(this.layoutDoc!._nativeHeight); - } - } const nativeH = this.nativeHeight(); const nativeW = this.nativeWidth(); - if (!nativeW || !nativeH) return 1; - const wscale = this.panelWidth() / nativeW; - return wscale * nativeH > this._panelHeight ? this._panelHeight / nativeH : wscale; + let scaling = 1; + if (!this.layoutDoc?._fitWidth && (!nativeW || !nativeH)) { + scaling = 1; + } else if ((this.layoutDoc?._fitWidth) || + this._panelHeight / NumCast(this.layoutDoc!._nativeHeight) > this._panelWidth / NumCast(this.layoutDoc!._nativeWidth)) { + scaling = this._panelWidth / NumCast(this.layoutDoc!._nativeWidth); + } else { + // if (this.layoutDoc!.type === DocumentType.PDF || this.layoutDoc!.type === DocumentType.WEB) { + // if ((this.layoutDoc?._fitWidth) || + // this._panelHeight / NumCast(this.layoutDoc!._nativeHeight) > this._panelWidth / NumCast(this.layoutDoc!._nativeWidth)) { + // return this._panelWidth / NumCast(this.layoutDoc!._nativeWidth); + // } else { + // return this._panelHeight / NumCast(this.layoutDoc!._nativeHeight); + // } + // } + const wscale = this.panelWidth() / nativeW; + scaling = wscale * nativeH > this._panelHeight ? this._panelHeight / nativeH : wscale; + } + return scaling; } ScreenToLocalTransform = () => { @@ -767,7 +775,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { addDocTab = (doc: Doc, location: string, libraryPath?: Doc[]) => { SelectionManager.DeselectAll(); - if (doc.dockingConfig) { + if (doc._viewType === CollectionViewType.Docking && doc.layoutKey === "layout") { return MainView.Instance.openWorkspace(doc); } else if (location === "onRight") { return CollectionDockingView.AddRightSplit(doc, libraryPath); diff --git a/src/client/views/collections/CollectionMapView.scss b/src/client/views/collections/CollectionMapView.scss index c74433902..870b7fda8 100644 --- a/src/client/views/collections/CollectionMapView.scss +++ b/src/client/views/collections/CollectionMapView.scss @@ -1,4 +1,30 @@ -.collectionMapView-contents { +.collectionMapView { width: 100%; height: 100%; + + .collectionMapView-contents { + width: 100%; + height: 100%; + > div { + position: unset !important; // when the sidebar filter flys out, this prevents the map from extending outside the document box + } + } +} + +.loadingWrapper { + width: 100%; + height: 100%; + background-color: pink; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; + + .loadingGif { + align-self: center; + justify-self: center; + width: 50px; + height: 50px; + } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionMapView.tsx b/src/client/views/collections/CollectionMapView.tsx index b67daeb53..7b7828d7d 100644 --- a/src/client/views/collections/CollectionMapView.tsx +++ b/src/client/views/collections/CollectionMapView.tsx @@ -1,71 +1,263 @@ +import { GoogleApiWrapper, Map as GeoMap, MapProps, Marker } from "google-maps-react"; import { observer } from "mobx-react"; -import { makeInterface } from "../../../new_fields/Schema"; +import { Doc, Opt, DocListCast, FieldResult, Field } from "../../../new_fields/Doc"; import { documentSchema } from "../../../new_fields/documentSchemas"; -import React = require("react"); -import { Map, Marker, MapProps, GoogleApiWrapper } from "google-maps-react"; -import { NumCast, StrCast } from "../../../new_fields/Types"; +import { Id } from "../../../new_fields/FieldSymbols"; +import { makeInterface } from "../../../new_fields/Schema"; +import { Cast, NumCast, ScriptCast, StrCast } from "../../../new_fields/Types"; +import "./CollectionMapView.scss"; import { CollectionSubView } from "./CollectionSubView"; -import { Utils } from "../../../Utils"; -import { Opt } from "../../../new_fields/Doc"; +import React = require("react"); +import { DocumentManager } from "../../util/DocumentManager"; +import { UndoManager, undoBatch } from "../../util/UndoManager"; +import { computed, runInAction, Lambda, action } from "mobx"; +import requestPromise = require("request-promise"); + +type MapSchema = makeInterface<[typeof documentSchema]>; +const MapSchema = makeInterface(documentSchema); -type MapDocument = makeInterface<[typeof documentSchema]>; -const MapDocument = makeInterface(documentSchema); +export type LocationData = google.maps.LatLngLiteral & { + address?: string + resolvedAddress?: string; + zoom?: number; +}; -export type LocationData = google.maps.LatLngLiteral & { address?: string }; +interface DocLatLng { + lat: FieldResult<Field>; + lng: FieldResult<Field>; +} + +// Nowhere, Oklahoma +const defaultLocation = { lat: 35.1592238, lng: -98.444512, zoom: 15 }; +const noResults = "ZERO_RESULTS"; + +const query = async (data: string | google.maps.LatLngLiteral) => { + const contents = typeof data === "string" ? `address=${data.replace(/\s+/g, "+")}` : `latlng=${data.lat},${data.lng}`; + const target = `https://maps.googleapis.com/maps/api/geocode/json?${contents}&key=${process.env.GOOGLE_MAPS_GEO}`; + try { + return JSON.parse(await requestPromise.get(target)); + } catch { + return undefined; + } +}; @observer -class CollectionMapView extends CollectionSubView<MapDocument, Partial<MapProps> & { google: any }>(MapDocument) { +class CollectionMapView extends CollectionSubView<MapSchema, Partial<MapProps> & { google: any }>(MapSchema) { + + private _cancelAddrReq = new Map<string, boolean>(); + private _cancelLocReq = new Map<string, boolean>(); + private _initialLookupPending = new Map<string, boolean>(); + private responders: { location: Lambda, address: Lambda }[] = []; + + /** + * Note that all the uses of runInAction below are not included + * as a way to update observables (documents handle this already + * in their property setters), but rather to create a single bulk + * update and thus prevent uneeded invocations of the location- + * and address–updating reactions. + */ + + private getLocation = (doc: Opt<Doc>, fieldKey: string, returnDefault: boolean = true): Opt<LocationData> => { + if (doc) { + const titleLoc = StrCast(doc.title).startsWith("@") ? StrCast(doc.title).substring(1) : undefined; + const lat = Cast(doc[`${fieldKey}-lat`], "number", null) || (Cast(doc[`${fieldKey}-lat`], "string", null) && Number(Cast(doc[`${fieldKey}-lat`], "string", null))) || undefined; + const lng = Cast(doc[`${fieldKey}-lng`], "number", null) || (Cast(doc[`${fieldKey}-lng`], "string", null) && Number(Cast(doc[`${fieldKey}-lng`], "string", null))) || undefined; + const zoom = Cast(doc[`${fieldKey}-zoom`], "number", null) || (Cast(doc[`${fieldKey}-zoom`], "string", null) && Number(Cast(doc[`${fieldKey}-zoom`], "string", null))) || undefined; + const address = titleLoc || StrCast(doc[`${fieldKey}-address`], StrCast(doc.title).replace(/^-/, "")); + if (titleLoc || (address && (lat === undefined || lng === undefined))) { + const id = doc[Id]; + if (!this._initialLookupPending.get(id)) { + this._initialLookupPending.set(id, true); + setTimeout(() => { + titleLoc && Doc.SetInPlace(doc, "title", titleLoc, true); + this.respondToAddressChange(doc, fieldKey, address).then(() => this._initialLookupPending.delete(id)); + }); + } + } + return (lat === undefined || lng === undefined) ? (returnDefault ? defaultLocation : undefined) : { lat, lng, zoom }; + } + return undefined; + } + + private markerClick = async (layout: Doc, { lat, lng, zoom }: LocationData) => { + const batch = UndoManager.StartBatch("marker click"); + const { fieldKey } = this.props; + runInAction(() => { + this.layoutDoc[`${fieldKey}-mapCenter-lat`] = lat; + this.layoutDoc[`${fieldKey}-mapCenter-lng`] = lng; + zoom && (this.layoutDoc[`${fieldKey}-mapCenter-zoom`] = zoom); + }); + if (layout.isLinkButton && DocListCast(layout.links).length) { + await DocumentManager.Instance.FollowLink(undefined, layout, (doc: Doc, where: string, finished?: () => void) => { + this.props.addDocTab(doc, where); + finished?.(); + }, false, this.props.ContainingCollectionDoc, batch.end, undefined); + } else { + ScriptCast(layout.onClick)?.script.run({ this: layout, self: Cast(layout.rootDocument, Doc, null) || layout }); + batch.end(); + } + } + + private renderMarkerIcon = (layout: Doc) => { + const { Document } = this.props; + const fieldKey = Doc.LayoutFieldKey(layout); + const iconUrl = StrCast(layout.mapIconUrl, StrCast(Document.mapIconUrl)); + if (iconUrl) { + const iconWidth = NumCast(layout[`${fieldKey}-iconWidth`], 45); + const iconHeight = NumCast(layout[`${fieldKey}-iconHeight`], 45); + const iconSize = new google.maps.Size(iconWidth, iconHeight); + return { + size: iconSize, + scaledSize: iconSize, + url: iconUrl + }; + } + } + + private renderMarker = (layout: Doc) => { + const location = this.getLocation(layout, Doc.LayoutFieldKey(layout)); + return !location ? (null) : + <Marker + key={layout[Id]} + label={StrCast(layout.title)} + position={location} + onClick={() => this.markerClick(layout, location)} + icon={this.renderMarkerIcon(layout)} + />; + } + + private respondToAddressChange = async (doc: Doc, fieldKey: string, newAddress: string, oldAddress?: string) => { + if (newAddress === oldAddress) { + return false; + } + const response = await query(newAddress); + const id = doc[Id]; + if (!response || response.status === noResults) { + this._cancelAddrReq.set(id, true); + doc[`${fieldKey}-address`] = oldAddress; + return false; + } + const { geometry, formatted_address } = response.results[0]; + const { lat, lng } = geometry.location; + runInAction(() => { + if (doc[`${fieldKey}-lat`] !== lat || doc[`${fieldKey}-lng`] !== lng) { + this._cancelLocReq.set(id, true); + Doc.SetInPlace(doc, `${fieldKey}-lat`, lat, true); + Doc.SetInPlace(doc, `${fieldKey}-lng`, lng, true); + } + if (formatted_address !== newAddress) { + this._cancelAddrReq.set(id, true); + Doc.SetInPlace(doc, `${fieldKey}-address`, formatted_address, true); + } + }); + return true; + } + + private respondToLocationChange = async (doc: Doc, fieldKey: string, newLatLng: DocLatLng, oldLatLng: Opt<DocLatLng>) => { + if (newLatLng === oldLatLng) { + return false; + } + const response = await query({ lat: NumCast(newLatLng.lat), lng: NumCast(newLatLng.lng) }); + const id = doc[Id]; + if (!response || response.status === noResults) { + this._cancelLocReq.set(id, true); + runInAction(() => { + doc[`${fieldKey}-lat`] = oldLatLng?.lat; + doc[`${fieldKey}-lng`] = oldLatLng?.lng; + }); + return false; + } + const { formatted_address } = response.results[0]; + if (formatted_address !== doc[`${fieldKey}-address`]) { + this._cancelAddrReq.set(doc[Id], true); + Doc.SetInPlace(doc, `${fieldKey}-address`, formatted_address, true); + } + return true; + } + + @computed get reactiveContents() { + this.responders.forEach(({ location, address }) => { location(); address(); }); + this.responders = []; + return this.childLayoutPairs.map(({ layout }) => { + const fieldKey = Doc.LayoutFieldKey(layout); + const id = layout[Id]; + this.responders.push({ + location: computed(() => ({ lat: layout[`${fieldKey}-lat`], lng: layout[`${fieldKey}-lng`] })) + .observe(({ oldValue, newValue }) => { + if (this._cancelLocReq.get(id)) { + this._cancelLocReq.set(id, false); + } else if (newValue.lat !== undefined && newValue.lng !== undefined) { + this.respondToLocationChange(layout, fieldKey, newValue, oldValue); + } + }), + address: computed(() => Cast(layout[`${fieldKey}-address`], "string", null)) + .observe(({ oldValue, newValue }) => { + if (this._cancelAddrReq.get(id)) { + this._cancelAddrReq.set(id, false); + } else if (newValue?.length) { + this.respondToAddressChange(layout, fieldKey, newValue, oldValue); + } + }) + }); + return this.renderMarker(layout); + }); + } render() { - const { childLayoutPairs, props } = this; - const { Document } = props; - const center: LocationData = { lat: NumCast(Document.mapCenterLat), lng: NumCast(Document.mapCenterLng) }; - if (!center.lat) { - center.lat = childLayoutPairs.length ? NumCast(childLayoutPairs[0].layout.locationLat, 0) : 0; - center.lng = childLayoutPairs.length ? NumCast(childLayoutPairs[0].layout.locationLng, 0) : 0; + const { childLayoutPairs } = this; + const { Document, fieldKey, active, google } = this.props; + let center = this.getLocation(Document, `${fieldKey}-mapCenter`, false); + if (center === undefined) { + const childLocations = childLayoutPairs.map(({ layout }) => this.getLocation(layout, Doc.LayoutFieldKey(layout), false)); + center = childLocations.find(location => location) || defaultLocation; } - return ( - <div - className={"collectionMapView-contents"} - > - <Map - {...props} - zoom={NumCast(Document.zoom, 10)} - center={center} + return <div className="collectionMapView" ref={this.createDashEventsTarget}> + <div className={"collectionMapView-contents"} + style={{ pointerEvents: active() ? undefined : "none" }} + onWheel={e => e.stopPropagation()} + onPointerDown={e => (e.button === 0 && !e.ctrlKey) && e.stopPropagation()} > + <GeoMap + google={google} + zoom={center.zoom || 10} initialCenter={center} - > - {childLayoutPairs.map(({ layout }) => { - const location: LocationData = { - lat: NumCast(childLayoutPairs[0].layout.locationLat, 0), - lng: NumCast(childLayoutPairs[0].layout.locationLng, 0) - }; - let icon: Opt<google.maps.Icon>, iconUrl: Opt<string>; - if ((iconUrl = StrCast(Document.mapIconUrl, null))) { - const iconSize = new google.maps.Size(NumCast(layout.mapIconWidth, 45), NumCast(layout.mapIconHeight, 45)); - icon = { - size: iconSize, - scaledSize: iconSize, - url: iconUrl - }; + center={center} + onIdle={(_props?: MapProps, map?: google.maps.Map) => { + if (this.layoutDoc.lockedTransform) { + // reset zoom (ideally, we could probably can tell the map to disallow zooming somehow instead) + map?.setZoom(center?.zoom || 10); + map?.setCenter({ lat: center?.lat!, lng: center?.lng! }); + } else { + const zoom = map?.getZoom(); + (center?.zoom !== zoom) && undoBatch(action(() => { + Document[`${fieldKey}-mapCenter-zoom`] = zoom; + }))(); } - return ( - <Marker - key={Utils.GenerateGuid()} - label={StrCast(layout.title)} - position={location} - onClick={() => { - Document.mapCenterLat = location.lat; - Document.mapCenterLng = location.lng; - }} - icon={icon} - /> - ); - })} - </Map> + }} + onDragend={(_props?: MapProps, map?: google.maps.Map) => { + if (this.layoutDoc.lockedTransform) { + // reset the drag (ideally, we could probably can tell the map to disallow dragging somehow instead) + map?.setCenter({ lat: center?.lat!, lng: center?.lng! }); + } else { + undoBatch(action(({ lat, lng }) => { + Document[`${fieldKey}-mapCenter-lat`] = lat(); + Document[`${fieldKey}-mapCenter-lng`] = lng(); + }))(map?.getCenter()); + } + }} + > + {this.reactiveContents} + </GeoMap> </div> - ); + </div>; } } -export default GoogleApiWrapper({ apiKey: process.env.GOOGLE_MAPS! })(CollectionMapView) as any;
\ No newline at end of file +export default GoogleApiWrapper({ + apiKey: process.env.GOOGLE_MAPS!, + LoadingContainer: () => ( + <div className={"loadingWrapper"}> + <img className={"loadingGif"} src={"/assets/loading.gif"} /> + </div> + ) +})(CollectionMapView) as any;
\ No newline at end of file diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 57be77fdd..380d91d2f 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -736,7 +736,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { return (doc as any)[key][row + ${row}][(doc as any).schemaColumns[col + ${col}].heading]; } return ${script}`; - const compiled = CompileScript(script, { params: { this: Doc.name }, capturedVariables: { doc: this.props.Document, key: this.props.fieldKey }, typecheck: true, transformer: this.createTransformer(row, col) }); + const compiled = CompileScript(script, { params: { this: Doc.name }, capturedVariables: { doc: this.props.Document, key: this.props.fieldKey }, typecheck: false, transformer: this.createTransformer(row, col) }); if (compiled.compiled) { doc[field] = new ComputedField(compiled); return true; diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index dd84c4d6e..24a3119cc 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -63,7 +63,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { const dxf = () => this.getDocTransform(d, dref.current!); this._docXfs.push({ dxf: dxf, width: width, height: height }); const rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap); - const style = this.isStackingView ? { width: width(), marginTop: this.gridGap, height: height() } : { gridRowEnd: `span ${rowSpan}` }; + const style = this.isStackingView ? { width: width(), marginTop: i ? this.gridGap : 0, height: height() } : { gridRowEnd: `span ${rowSpan}` }; return <div className={`collectionStackingView-${this.isStackingView ? "columnDoc" : "masonryDoc"}`} key={d[Id]} ref={dref} style={style} > {this.getDisplayDoc(d, (!d.isTemplateDoc && !d.isTemplateForField && !d.PARAMS) ? undefined : this.props.DataDoc, dxf, width)} </div>; @@ -397,7 +397,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { if (!e.isPropagationStopped()) { const subItems: ContextMenuProps[] = []; subItems.push({ description: `${this.props.Document.fillColumn ? "Variable Size" : "Autosize"} Column`, event: () => this.props.Document.fillColumn = !this.props.Document.fillColumn, icon: "plus" }); - ContextMenu.Instance.addItem({ description: "Stacking Options ...", subitems: subItems, icon: "eye" }); + ContextMenu.Instance.addItem({ description: "Options...", subitems: subItems, icon: "eye" }); } } diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 6b7c3de56..ca38a21b8 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -100,7 +100,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: } rootSelected = (outsideReaction?: boolean) => { - return this.props.isSelected(outsideReaction) || (this.props.Document.rootDocument || this.props.Document.forceActive ? this.props.rootSelected(outsideReaction) : false); + return this.props.isSelected(outsideReaction) || (this.rootDoc && this.props.rootSelected(outsideReaction)); } // The data field for rendering this collection will be on the this.props.Document unless we're rendering a template in which case we try to use props.DataDoc. @@ -388,6 +388,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: continue; } const proto = Doc.GetProto(doc); + proto.text = result.rawText; proto.fileUpload = basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, ""); if (Upload.isImageInformation(result)) { proto["data-nativeWidth"] = (result.nativeWidth > result.nativeHeight) ? 400 * result.nativeWidth / result.nativeHeight : 400; diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx index 53de2fbbe..06ebf6d2d 100644 --- a/src/client/views/collections/CollectionTimeView.tsx +++ b/src/client/views/collections/CollectionTimeView.tsx @@ -1,11 +1,11 @@ import { action, computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { Doc } from "../../../new_fields/Doc"; +import { Doc, Opt, DocCastAsync } from "../../../new_fields/Doc"; import { List } from "../../../new_fields/List"; import { ObjectField } from "../../../new_fields/ObjectField"; import { RichTextField } from "../../../new_fields/RichTextField"; import { ComputedField, ScriptField } from "../../../new_fields/ScriptField"; -import { NumCast, StrCast, BoolCast } from "../../../new_fields/Types"; +import { NumCast, StrCast, BoolCast, Cast } from "../../../new_fields/Types"; import { emptyFunction, returnFalse, setupMoveUpEvents } from "../../../Utils"; import { Scripting } from "../../util/Scripting"; import { ContextMenu } from "../ContextMenu"; @@ -19,23 +19,22 @@ const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; import React = require("react"); +import { DocumentView } from "../nodes/DocumentView"; @observer export class CollectionTimeView extends CollectionSubView(doc => doc) { _changing = false; @observable _layoutEngine = "pivot"; @observable _collapsed: boolean = false; - componentWillUnmount() { - this.props.Document.onChildClick = undefined; - } - componentDidMount() { - const childDetailed = this.props.Document.childDetailed; // bcz: needs to be here to make sure the childDetailed layout template has been loaded when the first item is clicked; - const childText = "const alias = getAlias(this); Doc.ApplyTemplateTo(thisContainer.childDetailed, alias, 'layout_detailView'); alias.layoutKey='layout_detailedView'; alias.dropAction='alias'; alias.removeDropProperties=new List<string>(['dropAction']); useRightSplit(alias, shiftKey); "; - this.props.Document.onChildClick = ScriptField.MakeScript(childText, { this: Doc.name, heading: "string", thisContainer: Doc.name, shiftKey: "boolean" }); - this.props.Document._fitToBox = true; - if (!this.props.Document.onViewDefClick) { - this.props.Document.onViewDefDivClick = ScriptField.MakeScript("pivotColumnClick(this,payload)", { payload: "any" }); - } + @observable _childClickedScript: Opt<ScriptField>; + @observable _viewDefDivClick: Opt<ScriptField>; + async componentDidMount() { + const detailView = (await DocCastAsync(this.props.Document.childDetailView)) || DocumentView.findTemplate("detailView", StrCast(this.props.Document.type), ""); + const childText = "const alias = getAlias(self); switchView(alias, detailView); alias.dropAction='alias'; alias.removeDropProperties=new List<string>(['dropAction']); useRightSplit(alias, shiftKey); "; + runInAction(() => { + this._childClickedScript = ScriptField.MakeScript(childText, { this: Doc.name, shiftKey: "boolean" }, { detailView: detailView! }); + this._viewDefDivClick = ScriptField.MakeScript("pivotColumnClick(this,payload)", { payload: "any" }); + }); } layoutEngine = () => this._layoutEngine; @@ -70,15 +69,29 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) { }), returnFalse, emptyFunction); } + contentsDown = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, returnFalse, returnFalse, action(() => { + let prevFilterIndex = NumCast(this.props.Document._prevFilterIndex); + if (prevFilterIndex > 0) { + prevFilterIndex--; + this.props.Document._docFilters = ObjectField.MakeCopy(this.props.Document["_prevDocFilter" + prevFilterIndex] as ObjectField); + this.props.Document._docRangeFilters = ObjectField.MakeCopy(this.props.Document["_prevDocRangeFilters" + prevFilterIndex] as ObjectField); + this.props.Document._prevFilterIndex = prevFilterIndex; + } else { + this.props.Document._docFilters = new List([]); + } + }), false); + } + @computed get contents() { - return <div className="collectionTimeView-innards" key="timeline" style={{ width: "100%" }}> - <CollectionFreeFormView {...this.props} freezeChildDimensions={BoolCast(this.layoutDoc._freezeChildDimensions, true)} layoutEngine={this.layoutEngine} /> + return <div className="collectionTimeView-innards" key="timeline" style={{ width: "100%" }} onPointerDown={this.contentsDown}> + <CollectionFreeFormView {...this.props} childClickScript={this._childClickedScript} viewDefDivClick={this._viewDefDivClick} fitToBox={true} freezeChildDimensions={BoolCast(this.layoutDoc._freezeChildDimensions, true)} layoutEngine={this.layoutEngine} /> </div>; } public static SyncTimelineToPresentation(doc: Doc) { const fieldKey = Doc.LayoutFieldKey(doc); - doc[fieldKey + "-timelineCur"] = ComputedField.MakeFunction("(curPresentationItem()[this._pivotField || 'year'] || 0)"); + doc[fieldKey + "-timelineCur"] = ComputedField.MakeFunction("(activePresentationItem()[this._pivotField || 'year'] || 0)"); } specificMenu = (e: React.MouseEvent) => { const layoutItems: ContextMenuProps[] = []; @@ -89,7 +102,7 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) { layoutItems.push({ description: "Auto Time/Pivot layout", event: () => { doc._forceRenderEngine = undefined; }, icon: "compress-arrows-alt" }); layoutItems.push({ description: "Sync with presentation", event: () => CollectionTimeView.SyncTimelineToPresentation(doc), icon: "compress-arrows-alt" }); - ContextMenu.Instance.addItem({ description: "Pivot/Time Options ...", subitems: layoutItems, icon: "eye" }); + ContextMenu.Instance.addItem({ description: "Options...", subitems: layoutItems, icon: "eye" }); } @computed get _allFacets() { const facets = new Set<string>(); @@ -130,20 +143,6 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) { color: "#f1efeb" // this.props.headingObject ? this.props.headingObject.color : "#f1efeb"; }; return <div className={"pivotKeyEntry"}> - <button className="collectionTimeView-backBtn" - onClick={action(() => { - let prevFilterIndex = NumCast(this.props.Document._prevFilterIndex); - if (prevFilterIndex > 0) { - prevFilterIndex--; - this.props.Document._docFilters = ObjectField.MakeCopy(this.props.Document["_prevDocFilter" + prevFilterIndex] as ObjectField); - this.props.Document._docRangeFilters = ObjectField.MakeCopy(this.props.Document["_prevDocRangeFilters" + prevFilterIndex] as ObjectField); - this.props.Document._prevFilterIndex = prevFilterIndex; - } else { - this.props.Document._docFilters = new List([]); - } - })}> - back - </button> <EditableView {...newEditableViewProps} display={"inline"} menuCallback={this.menuCallback} /> </div>; } diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index 8e95f7fbe..a00bb6bfb 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -82,6 +82,7 @@ text-overflow: ellipsis; white-space: pre-wrap; overflow: hidden; + min-width: 10px; // width:100%;//width: max-content; } @@ -100,10 +101,29 @@ border-left: dashed 1px #00000042; } +.treeViewItem-header { + border: transparent 1px solid; + display: flex; + + .editableView-container-editing-oneLine { + min-width: 15px; + } + .documentView-node-topmost { + width: unset; + } + > svg { + display: none; + } + +} + .treeViewItem-header:hover { .collectionTreeView-keyHeader { display: inherit; } + > svg { + display: inherit; + } .treeViewItem-openRight { display: inline-block; @@ -119,18 +139,6 @@ } } -.treeViewItem-header { - border: transparent 1px solid; - display: flex; - - .editableView-container-editing-oneLine { - min-width: 15px; - } - .documentView-node-topmost { - width: unset; - } -} - .treeViewItem-header-above { border-top: black 1px solid; } diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 23fb259fc..a052d045c 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -6,11 +6,10 @@ import { observer } from "mobx-react"; import { Doc, DocListCast, Field, HeightSym, WidthSym, DataSym, Opt } from '../../../new_fields/Doc'; import { Id } from '../../../new_fields/FieldSymbols'; import { List } from '../../../new_fields/List'; -import { Document, listSpec, createSchema, makeInterface } from '../../../new_fields/Schema'; +import { Document, listSpec } from '../../../new_fields/Schema'; import { ComputedField, ScriptField } from '../../../new_fields/ScriptField'; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../new_fields/Types'; -import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; -import { emptyFunction, emptyPath, returnFalse, Utils, returnOne, returnZero, returnTransparent, returnTrue } from '../../../Utils'; +import { emptyFunction, emptyPath, returnFalse, Utils, returnOne, returnZero, returnTransparent, returnTrue, simulateMouseClick } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from "../../documents/DocumentTypes"; import { DocumentManager } from '../../util/DocumentManager'; @@ -19,7 +18,7 @@ import { makeTemplate } from '../../util/DropConverter'; import { Scripting } from '../../util/Scripting'; import { SelectionManager } from '../../util/SelectionManager'; import { Transform } from '../../util/Transform'; -import { undoBatch } from '../../util/UndoManager'; +import { undoBatch, UndoManager } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { EditableView } from "../EditableView"; @@ -32,7 +31,7 @@ import { Templates } from '../Templates'; import { CollectionSubView, SubCollectionViewProps } from "./CollectionSubView"; import "./CollectionTreeView.scss"; import React = require("react"); -import { CollectionViewType } from './CollectionView'; +import { CollectionViewType, CollectionView } from './CollectionView'; import { RichTextField } from '../../../new_fields/RichTextField'; import { DocumentView } from '../nodes/DocumentView'; @@ -139,13 +138,15 @@ class TreeView extends React.Component<TreeViewProps> { @undoBatch @action remove = (document: Document, key: string) => { return Doc.RemoveDocFromList(this.dataDoc, key, document); } + @undoBatch @action removeDoc = (document: Document) => { + return Doc.RemoveDocFromList(this.props.containingCollection, Doc.LayoutFieldKey(this.props.containingCollection), document); + } protected createTreeDropTarget = (ele: HTMLDivElement) => { this._treedropDisposer && this._treedropDisposer(); ele && (this._treedropDisposer = DragManager.MakeDropTarget(ele, this.treeDrop.bind(this))); } - onPointerDown = (e: React.PointerEvent) => e.stopPropagation(); onPointerEnter = (e: React.PointerEvent): void => { this.props.active(true) && Doc.BrushDoc(this.dataDoc); if (e.buttons === 1 && SelectionManager.GetIsDragging()) { @@ -183,21 +184,17 @@ class TreeView extends React.Component<TreeViewProps> { SetValue={undoBatch((value: string) => { Doc.SetInPlace(this.props.document, key, value, false) || true; Doc.SetInPlace(this.props.document, "editTitle", undefined, false); - //this.props.document.editTitle = undefined; })} OnFillDown={undoBatch((value: string) => { Doc.SetInPlace(this.props.document, key, value, false); const doc = Docs.Create.FreeformDocument([], { title: "-", x: 0, y: 0, _width: 100, _height: 25, templates: new List<string>([Templates.Title.Layout]) }); - //EditableView.loadId = doc[Id]; Doc.SetInPlace(this.props.document, "editTitle", undefined, false); - // this.props.document.editTitle = undefined; - Doc.SetInPlace(this.props.document, "editTitle", true, false); - //doc.editTitle = true; + Doc.SetInPlace(doc, "editTitle", true, false); return this.props.addDocument(doc); })} onClick={() => { SelectionManager.DeselectAll(); - Doc.UserDoc().SelectedDocs = new List([this.props.document]); + Doc.UserDoc().activeSelection = new List([this.props.document]); return false; }} OnTab={undoBatch((shift?: boolean) => { @@ -211,33 +208,6 @@ class TreeView extends React.Component<TreeViewProps> { })} />) - onWorkspaceContextMenu = (e: React.MouseEvent): void => { - if (!e.isPropagationStopped()) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view - const sort = this.props.document[`${this.fieldKey}-sortAscending`]; - if (this.props.document === CurrentUserUtils.UserDocument.recentlyClosed) { - ContextMenu.Instance.addItem({ description: "Clear All", event: () => Doc.GetProto(CurrentUserUtils.UserDocument.recentlyClosed as Doc).data = new List<Doc>(), icon: "plus" }); - } else if (this.props.document !== CurrentUserUtils.UserDocument.workspaces) { - ContextMenu.Instance.addItem({ description: "Pin to Presentation", event: () => this.props.pinToPres(this.props.document), icon: "tv" }); - ContextMenu.Instance.addItem({ description: "Open Tab", event: () => this.props.addDocTab(this.props.document, "inTab", this.props.libraryPath), icon: "folder" }); - ContextMenu.Instance.addItem({ description: "Open Right", event: () => this.props.addDocTab(this.props.document, "onRight", this.props.libraryPath), icon: "caret-square-right" }); - if (DocumentManager.Instance.getDocumentViews(this.dataDoc).length) { - ContextMenu.Instance.addItem({ description: "Focus", event: () => (view => view && view.props.focus(this.props.document, true))(DocumentManager.Instance.getFirstDocumentView(this.props.document)), icon: "camera" }); - } - ContextMenu.Instance.addItem({ description: "Delete Item", event: () => this.props.deleteDoc(this.props.document), icon: "trash-alt" }); - } else { - ContextMenu.Instance.addItem({ description: "Delete Workspace", event: () => this.props.deleteDoc(this.props.document), icon: "trash-alt" }); - ContextMenu.Instance.addItem({ description: "Create New Workspace", event: () => MainView.Instance.createNewWorkspace(), icon: "plus" }); - } - ContextMenu.Instance.addItem({ description: (sort ? "Sort Descending" : (sort === false ? "Unsort" : "Sort Ascending")), event: () => this.props.document[`${this.fieldKey}-sortAscending`] = (sort ? false : (sort === false ? undefined : true)), icon: "minus" }); - ContextMenu.Instance.addItem({ description: "Toggle Theme Colors", event: () => this.props.document.darkScheme = !this.props.document.darkScheme, icon: "minus" }); - ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { const kvp = Docs.Create.KVPDocument(this.props.document, { _width: 300, _height: 300 }); this.props.addDocTab(kvp, "onRight"); }, icon: "layer-group" }); - ContextMenu.Instance.addItem({ description: "Publish", event: () => DocUtils.Publish(this.props.document, StrCast(this.props.document.title), () => { }, () => { }), icon: "file" }); - ContextMenu.Instance.displayMenu(e.pageX > 156 ? e.pageX - 156 : 0, e.pageY - 15); - e.stopPropagation(); - e.preventDefault(); - } - } - @undoBatch treeDrop = (e: Event, de: DragManager.DropEvent) => { const pt = [de.x, de.y]; @@ -258,7 +228,8 @@ class TreeView extends React.Component<TreeViewProps> { addDoc = (doc: Doc) => Doc.AddDocToList(this.dataDoc, this.fieldKey, doc) || addDoc(doc); } const movedDocs = (de.complete.docDragData.treeViewId === this.props.treeViewId[Id] ? de.complete.docDragData.draggedDocuments : de.complete.docDragData.droppedDocuments); - return ((de.complete.docDragData.dropAction && (de.complete.docDragData.treeViewId !== this.props.treeViewId[Id])) || de.complete.docDragData.userDropAction) ? + const move = de.complete.docDragData.dropAction === "move" || de.complete.docDragData.dropAction; + return ((!move && (de.complete.docDragData.treeViewId !== this.props.treeViewId[Id])) || de.complete.docDragData.userDropAction) ? de.complete.docDragData.droppedDocuments.reduce((added, d) => addDoc(d) || added, false) : de.complete.docDragData.moveDocument ? movedDocs.reduce((added, d) => de.complete.docDragData?.moveDocument?.(d, undefined, addDoc) || added, false) @@ -357,11 +328,15 @@ class TreeView extends React.Component<TreeViewProps> { const remDoc = (doc: Doc) => this.remove(doc, expandKey); const addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.dataDoc, expandKey, doc, addBefore, before, false, true); const docs = expandKey === "links" ? this.childLinks : this.childDocs; - return <ul key={expandKey + "more"}> + const sortKey = `${this.fieldKey}-sortAscending`; + return <ul key={expandKey + "more"} onClick={(e) => { + this.props.document[sortKey] = (this.props.document[sortKey] ? false : (this.props.document[sortKey] === false ? undefined : true)); + e.stopPropagation(); + }}> {!docs ? (null) : TreeView.GetChildElements(docs, this.props.treeViewId, Doc.Layout(this.props.document), this.templateDataDoc, expandKey, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move, - this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform, + StrCast(this.props.document.childDropAction, this.props.dropAction) as dropActionType, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.treeViewHideHeaderFields, this.props.treeViewPreventOpen, [...this.props.renderedIds, this.props.document[Id]], this.props.libraryPath, this.props.onCheckedClick, this.props.onChildClick, this.props.ignoreFields)} </ul >; @@ -397,11 +372,13 @@ class TreeView extends React.Component<TreeViewProps> { } } + get onCheckedClick() { return this.props.onCheckedClick || ScriptCast(this.props.document.onCheckedClick); } + @action bulletClick = (e: React.MouseEvent) => { - if (this.props.onCheckedClick && this.props.document.type !== DocumentType.COL) { + if (this.onCheckedClick && this.props.document.type !== DocumentType.COL) { // this.props.document.treeViewChecked = this.props.document.treeViewChecked === "check" ? "x" : this.props.document.treeViewChecked === "x" ? undefined : "check"; - ScriptCast(this.props.onCheckedClick).script.run({ + this.onCheckedClick.script.run({ this: this.props.document.isTemplateForField && this.props.dataDoc ? this.props.dataDoc : this.props.document, heading: this.props.containingCollection.title, checked: this.props.document.treeViewChecked === "check" ? "x" : this.props.document.treeViewChecked === "x" ? undefined : "check", @@ -415,11 +392,22 @@ class TreeView extends React.Component<TreeViewProps> { @computed get renderBullet() { - const checked = this.props.document.type === DocumentType.COL ? undefined : this.props.onCheckedClick ? (this.props.document.treeViewChecked ? this.props.document.treeViewChecked : "unchecked") : undefined; + const checked = this.props.document.type === DocumentType.COL ? undefined : this.onCheckedClick ? (this.props.document.treeViewChecked ? this.props.document.treeViewChecked : "unchecked") : undefined; return <div className="bullet" title="view inline" onClick={this.bulletClick} style={{ color: StrCast(this.props.document.color, checked === "unchecked" ? "white" : "inherit"), opacity: checked === "unchecked" ? undefined : 0.4 }}> {<FontAwesomeIcon icon={checked === "check" ? "check" : (checked === "x" ? "times" : checked === "unchecked" ? "square" : !this.treeViewOpen ? (this.childDocs ? "caret-square-right" : "caret-right") : (this.childDocs ? "caret-square-down" : "caret-down"))} />} </div>; } + + showContextMenu = (e: React.MouseEvent) => { + simulateMouseClick(this._docRef.current!.ContentDiv!, e.clientX, e.clientY + 30, e.screenX, e.screenY + 30); + e.stopPropagation(); + } + focusOnDoc = (doc: Doc) => DocumentManager.Instance.getFirstDocumentView(doc)?.props.focus(doc, true); + contextMenuItems = () => { + const focusScript = ScriptField.MakeFunction(`DocFocus(self)`); + return [{ script: focusScript!, label: "Focus" }]; + } + _docRef = React.createRef<DocumentView>(); /** * Renders the EditableView title element for placement into the tree. */ @@ -429,19 +417,22 @@ class TreeView extends React.Component<TreeViewProps> { const editTitle = ScriptField.MakeFunction("setInPlace(this, 'editTitle', true)"); const headerElements = ( - <span className="collectionTreeView-keyHeader" key={this.treeViewExpandedView} - onPointerDown={action(() => { - if (this.treeViewOpen) { - this.props.document.treeViewExpandedView = this.treeViewExpandedView === this.fieldKey ? "fields" : - this.treeViewExpandedView === "fields" && Doc.Layout(this.props.document) ? "layout" : - this.treeViewExpandedView === "layout" && this.props.document.links ? "links" : - this.childDocs ? this.fieldKey : "fields"; - } - this.treeViewOpen = true; - })}> - {this.treeViewExpandedView} - </span>); - const openRight = (<div className="treeViewItem-openRight" onPointerDown={this.onPointerDown} onClick={this.openRight}> + <> + <FontAwesomeIcon icon="cog" size="sm" onClick={e => this.showContextMenu(e)}></FontAwesomeIcon> + <span className="collectionTreeView-keyHeader" key={this.treeViewExpandedView} + onPointerDown={action(() => { + if (this.treeViewOpen) { + this.props.document.treeViewExpandedView = this.treeViewExpandedView === this.fieldKey ? "fields" : + this.treeViewExpandedView === "fields" && Doc.Layout(this.props.document) ? "layout" : + this.treeViewExpandedView === "layout" && this.props.document.links ? "links" : + this.childDocs ? this.fieldKey : "fields"; + } + this.treeViewOpen = true; + })}> + {this.treeViewExpandedView} + </span> + </>); + const openRight = (<div className="treeViewItem-openRight" onClick={this.openRight}> <FontAwesomeIcon title="open in pane on right" icon="angle-right" size="lg" /> </div>); return <> @@ -451,11 +442,12 @@ class TreeView extends React.Component<TreeViewProps> { fontWeight: this.props.document.searchMatch ? "bold" : undefined, textDecoration: Doc.GetT(this.props.document, "title", "string", true) ? "underline" : undefined, outline: BoolCast(this.props.document.workspaceBrush) ? "dashed 1px #06123232" : undefined, - pointerEvents: this.props.active() || SelectionManager.GetIsDragging() ? "all" : "none" + pointerEvents: this.props.active() || SelectionManager.GetIsDragging() ? undefined : "none" }} > {Doc.GetT(this.props.document, "editTitle", "boolean", true) ? this.editableView("title") : <DocumentView + ref={this._docRef} Document={this.props.document} DataDoc={undefined} LibraryPath={this.props.libraryPath || []} @@ -465,14 +457,15 @@ class TreeView extends React.Component<TreeViewProps> { pinToPres={emptyFunction} onClick={this.props.onChildClick || editTitle} dropAction={this.props.dropAction} - moveDocument={this.props.moveDocument} - removeDocument={undefined} + moveDocument={this.move} + removeDocument={this.removeDoc} ScreenToLocalTransform={this.getTransform} ContentScaling={returnOne} PanelWidth={returnZero} PanelHeight={returnZero} NativeHeight={returnZero} NativeWidth={returnZero} + contextMenuItems={this.contextMenuItems} renderDepth={1} focus={emptyFunction} parentActive={returnTrue} @@ -480,7 +473,7 @@ class TreeView extends React.Component<TreeViewProps> { bringToFront={emptyFunction} dontRegisterView={BoolCast(this.props.treeViewId.dontRegisterChildren)} ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} + ContainingCollectionDoc={this.props.containingCollection} />} </div > {this.props.treeViewHideHeaderFields() ? (null) : headerElements} @@ -489,14 +482,25 @@ class TreeView extends React.Component<TreeViewProps> { } render() { + const sorting = this.props.document[`${this.fieldKey}-sortAscending`]; setTimeout(() => runInAction(() => untracked(() => this._overrideTreeViewOpen = this.treeViewOpen)), 0); - return <div className="treeViewItem-container" ref={this.createTreeDropTarget} onContextMenu={this.onWorkspaceContextMenu}> + return <div className="treeViewItem-container" ref={this.createTreeDropTarget}> <li className="collection-child"> - <div className="treeViewItem-header" ref={this._header} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}> + <div className="treeViewItem-header" ref={this._header} onClick={e => { + if (this.props.active(true)) { + e.stopPropagation(); + e.preventDefault(); + } + }} onPointerDown={e => { + if (this.props.active(true)) { + e.stopPropagation(); + e.preventDefault(); + } + }} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}> {this.renderBullet} {this.renderTitle} </div> - <div className="treeViewItem-border"> + <div className="treeViewItem-border" style={{ borderColor: sorting === undefined ? undefined : sorting ? "crimson" : "blue" }}> {!this.treeViewOpen || this.props.renderedIds.indexOf(this.props.document[Id]) !== -1 ? (null) : this.renderContent} </div> </li> @@ -651,14 +655,14 @@ export type collectionTreeViewProps = { }; @observer -export class CollectionTreeView extends CollectionSubView(Document, undefined as any as collectionTreeViewProps) { +export class CollectionTreeView extends CollectionSubView<Document, Partial<collectionTreeViewProps>>(Document) { private treedropDisposer?: DragManager.DragDropDisposer; private _mainEle?: HTMLDivElement; @computed get dataDoc() { return this.props.DataDoc || this.props.Document; } protected createTreeDropTarget = (ele: HTMLDivElement) => { - this.treedropDisposer && this.treedropDisposer(); + this.treedropDisposer?.(); if (this._mainEle = ele) { this.treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this)); } @@ -666,7 +670,7 @@ export class CollectionTreeView extends CollectionSubView(Document, undefined as componentWillUnmount() { super.componentWillUnmount(); - this.treedropDisposer && this.treedropDisposer(); + this.treedropDisposer?.(); } @action @@ -691,14 +695,14 @@ export class CollectionTreeView extends CollectionSubView(Document, undefined as } onContextMenu = (e: React.MouseEvent): void => { // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout - if (!e.isPropagationStopped() && this.props.Document === CurrentUserUtils.UserDocument.workspaces) { + if (!e.isPropagationStopped() && this.props.Document === Doc.UserDoc().myWorkspaces) { ContextMenu.Instance.addItem({ description: "Create Workspace", event: () => MainView.Instance.createNewWorkspace(), icon: "plus" }); ContextMenu.Instance.addItem({ description: "Delete Workspace", event: () => this.remove(this.props.Document), icon: "minus" }); e.stopPropagation(); e.preventDefault(); ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); - } else if (!e.isPropagationStopped() && this.props.Document === CurrentUserUtils.UserDocument.recentlyClosed) { - ContextMenu.Instance.addItem({ description: "Clear All", event: () => CurrentUserUtils.UserDocument.recentlyClosed = new List<Doc>(), icon: "plus" }); + } else if (!e.isPropagationStopped() && this.props.Document === Doc.UserDoc().myRecentlyClosed) { + ContextMenu.Instance.addItem({ description: "Clear All", event: () => Doc.UserDoc().myRecentlyClosed = new List<Doc>(), icon: "plus" }); e.stopPropagation(); e.preventDefault(); ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); @@ -707,7 +711,7 @@ export class CollectionTreeView extends CollectionSubView(Document, undefined as layoutItems.push({ description: (this.props.Document.treeViewPreventOpen ? "Persist" : "Abandon") + "Treeview State", event: () => this.props.Document.treeViewPreventOpen = !this.props.Document.treeViewPreventOpen, icon: "paint-brush" }); layoutItems.push({ description: (this.props.Document.treeViewHideHeaderFields ? "Show" : "Hide") + " Header Fields", event: () => this.props.Document.treeViewHideHeaderFields = !this.props.Document.treeViewHideHeaderFields, icon: "paint-brush" }); layoutItems.push({ description: (this.props.Document.treeViewHideTitle ? "Show" : "Hide") + " Title", event: () => this.props.Document.treeViewHideTitle = !this.props.Document.treeViewHideTitle, icon: "paint-brush" }); - ContextMenu.Instance.addItem({ description: "Treeview Options ...", subitems: layoutItems, icon: "eye" }); + ContextMenu.Instance.addItem({ description: "Options...", subitems: layoutItems, icon: "eye" }); } ContextMenu.Instance.addItem({ description: "Buxton Layout", icon: "eye", event: () => { @@ -739,14 +743,14 @@ export class CollectionTreeView extends CollectionSubView(Document, undefined as heroView._showTitle = "title"; heroView._showTitleHover = "titlehover"; - Doc.AddDocToList(Doc.UserDoc().expandingButtons as Doc, "data", + Doc.AddDocToList(Doc.UserDoc().dockedBtns as Doc, "data", Docs.Create.FontIconDocument({ title: "hero view", _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, dropAction: "alias", dragFactory: heroView, removeDropProperties: new List<string>(["dropAction"]), icon: "portrait", onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'), })); - Doc.AddDocToList(Doc.UserDoc().expandingButtons as Doc, "data", + Doc.AddDocToList(Doc.UserDoc().dockedBtns as Doc, "data", Docs.Create.FontIconDocument({ title: "detail view", _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, dropAction: "alias", dragFactory: detailView, removeDropProperties: new List<string>(["dropAction"]), icon: "file-alt", @@ -754,7 +758,7 @@ export class CollectionTreeView extends CollectionSubView(Document, undefined as })); Document.childLayout = heroView; - Document.childDetailed = detailView; + Document.childDetailView = detailView; Document._viewType = CollectionViewType.Time; Document._forceActive = true; Document._pivotField = "company"; @@ -764,8 +768,7 @@ export class CollectionTreeView extends CollectionSubView(Document, undefined as const existingOnClick = ContextMenu.Instance.findByDescription("OnClick..."); const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; onClicks.push({ - description: "Edit onChecked Script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Checked Changed ...", this.props.Document, - "onCheckedClick", obj.x, obj.y, { heading: "boolean", checked: "boolean", treeViewContainer: Doc.name }) + description: "Edit onChecked Script", event: () => UndoManager.RunInBatch(() => DocumentView.makeCustomViewClicked(this.props.Document, undefined, "onCheckedClick"), "edit onCheckedClick"), icon: "edit" }); !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" }); } @@ -782,6 +785,7 @@ export class CollectionTreeView extends CollectionSubView(Document, undefined as } render() { + if (!(this.props.Document instanceof Doc)) return (null); const dropAction = StrCast(this.props.Document.childDropAction) as dropActionType; const addDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => this.addDoc(doc, relativeTo, before); const moveDoc = (d: Doc, target: Doc | undefined, addDoc: (doc: Doc) => boolean) => this.props.moveDocument(d, target, addDoc); @@ -810,6 +814,7 @@ export class CollectionTreeView extends CollectionSubView(Document, undefined as Doc.SetInPlace(this.dataDoc, "title", value, false); const doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, _width: 100, _height: 25, templates: new List<string>([Templates.Title.Layout]) }); EditableView.loadId = doc[Id]; + Doc.SetInPlace(doc, "editTitle", true, false); this.addDoc(doc, childDocs.length ? childDocs[0] : undefined, true); })} />)} {this.props.Document.allowClear ? this.renderClearButton : (null)} @@ -818,7 +823,7 @@ export class CollectionTreeView extends CollectionSubView(Document, undefined as TreeView.GetChildElements(childDocs, this.props.Document, this.props.Document, this.props.DataDoc, this.props.fieldKey, this.props.ContainingCollectionDoc, undefined, addDoc, this.remove, moveDoc, dropAction, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform, this.outerXf, this.props.active, this.props.PanelWidth, this.props.ChromeHeight, this.props.renderDepth, () => this.props.treeViewHideHeaderFields || BoolCast(this.props.Document.treeViewHideHeaderFields), - BoolCast(this.props.Document.treeViewPreventOpen), [], this.props.LibraryPath, this.props.onCheckedClick || ScriptCast(this.props.Document.onCheckedClick), + BoolCast(this.props.Document.treeViewPreventOpen), [], this.props.LibraryPath, this.props.onCheckedClick, this.props.onChildClick || ScriptCast(this.props.Document.onChildClick), this.props.ignoreFields) } </ul> @@ -839,13 +844,12 @@ Scripting.addGlobal(function readFacetData(layoutDoc: Doc, dataDoc: Doc, dataKey nonNumbers++; } }); - const facetValueDocSet = (nonNumbers / facetValues.length > .1 ? facetValues.sort() : facetValues.sort((n1: string, n2: string) => Number(n1) - Number(n2))).map(facetValue => - Docs.Create.TextDocument("", { - title: facetValue.toString(), - treeViewChecked: ComputedField.MakeFunction("determineCheckedState(layoutDoc, facetHeader, facetValue)", - {}, - { layoutDoc, facetHeader, facetValue }) - })); + const facetValueDocSet = (nonNumbers / facetValues.length > .1 ? facetValues.sort() : facetValues.sort((n1: string, n2: string) => Number(n1) - Number(n2))).map(facetValue => { + const doc = new Doc(); + doc.title = facetValue.toString(); + doc.treeViewChecked = ComputedField.MakeFunction("determineCheckedState(layoutDoc, facetHeader, facetValue)", {}, { layoutDoc, facetHeader, facetValue }); + return doc; + }); return new List<Doc>(facetValueDocSet); }); diff --git a/src/client/views/collections/CollectionView.scss b/src/client/views/collections/CollectionView.scss index b92c5fdd1..d43dd387a 100644 --- a/src/client/views/collections/CollectionView.scss +++ b/src/client/views/collections/CollectionView.scss @@ -11,7 +11,6 @@ height: 100%; overflow: hidden; // bcz: used to be 'auto' which would create scrollbars when there's a floating doc that's not visible. not sure if that's better, but the scrollbars are annoying... - .collectionTimeView-dragger { background-color: lightgray; height: 40px; @@ -21,7 +20,7 @@ top: 55%; border: 1px black solid; z-index: 2; - left: -10px; + right: -10px; } .collectionTimeView-treeView { display: flex; @@ -29,7 +28,7 @@ width: 200px; height: 100%; position: absolute; - left: 0; + right: 0; top: 0; .collectionTimeView-addfacet { diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index d1dc32829..4f1242c33 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -13,7 +13,7 @@ import { List } from '../../../new_fields/List'; import { BoolCast, Cast, NumCast, StrCast, ScriptCast } from '../../../new_fields/Types'; import { ImageField } from '../../../new_fields/URLField'; import { TraceMobx } from '../../../new_fields/util'; -import { Utils, setupMoveUpEvents, returnFalse, returnZero } from '../../../Utils'; +import { Utils, setupMoveUpEvents, returnFalse, returnZero, emptyPath, emptyFunction, returnOne } from '../../../Utils'; import { DocumentType } from '../../documents/DocumentTypes'; import { DocumentManager } from '../../util/DocumentManager'; import { ImageUtils } from '../../util/Import & Export/ImageUtils'; @@ -45,8 +45,7 @@ import { ScriptField, ComputedField } from '../../../new_fields/ScriptField'; import { InteractionUtils } from '../../util/InteractionUtils'; import { ObjectField } from '../../../new_fields/ObjectField'; import CollectionMapView from './CollectionMapView'; -import { ClientUtils } from '../../util/ClientUtils'; -import { GoogleApiWrapper } from 'google-maps-react'; +import { Transform } from 'prosemirror-transform'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -106,7 +105,7 @@ export class CollectionView extends Touchable<FieldViewProps> { return viewField as any as CollectionViewType; } - active = (outsideReaction?: boolean) => this.props.isSelected(outsideReaction) || (this.props.rootSelected(outsideReaction) && BoolCast(this.props.Document.forceActive)) || this._isChildActive || this.props.renderDepth === 0; + active = (outsideReaction?: boolean) => (this.props.isSelected(outsideReaction) || this.props.rootSelected(outsideReaction) || this.props.Document.forceActive || this._isChildActive || this.props.renderDepth === 0) ? true : false; whenActiveChanged = (isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive); @@ -193,55 +192,55 @@ export class CollectionView extends Touchable<FieldViewProps> { } + setupViewTypes(category: string, func: (viewType: CollectionViewType) => Doc, addExtras: boolean) { + const existingVm = ContextMenu.Instance.findByDescription(category); + const subItems = existingVm && "subitems" in existingVm ? existingVm.subitems : []; + + subItems.push({ description: "Freeform", event: () => func(CollectionViewType.Freeform), icon: "signature" }); + if (addExtras && CollectionView._safeMode) { + ContextMenu.Instance.addItem({ description: "Test Freeform", event: () => func(CollectionViewType.Invalid), icon: "project-diagram" }); + } + subItems.push({ description: "Schema", event: () => func(CollectionViewType.Schema), icon: "th-list" }); + subItems.push({ description: "Treeview", event: () => func(CollectionViewType.Tree), icon: "tree" }); + subItems.push({ description: "Stacking", event: () => func(CollectionViewType.Stacking), icon: "ellipsis-v" }); + subItems.push({ description: "Stacking (AutoHeight)", event: () => func(CollectionViewType.Stacking)._autoHeight = true, icon: "ellipsis-v" }); + subItems.push({ description: "Staff", event: () => func(CollectionViewType.Staff), icon: "music" }); + subItems.push({ description: "Multicolumn", event: () => func(CollectionViewType.Multicolumn), icon: "columns" }); + subItems.push({ description: "Multirow", event: () => func(CollectionViewType.Multirow), icon: "columns" }); + subItems.push({ description: "Masonry", event: () => func(CollectionViewType.Masonry), icon: "columns" }); + subItems.push({ description: "Carousel", event: () => func(CollectionViewType.Carousel), icon: "columns" }); + subItems.push({ description: "Pivot/Time", event: () => func(CollectionViewType.Time), icon: "columns" }); + subItems.push({ description: "Map", event: () => func(CollectionViewType.Map), icon: "globe-americas" }); + if (addExtras && this.props.Document._viewType === CollectionViewType.Freeform) { + subItems.push({ description: "Custom", icon: "fingerprint", event: AddCustomFreeFormLayout(this.props.Document, this.props.fieldKey) }); + } + addExtras && subItems.push({ description: "lightbox", event: action(() => this._isLightboxOpen = true), icon: "eye" }); + !existingVm && ContextMenu.Instance.addItem({ description: category, subitems: subItems, icon: "eye" }); + } + onContextMenu = (e: React.MouseEvent): void => { if (!e.isPropagationStopped() && this.props.Document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 - const existingVm = ContextMenu.Instance.findByDescription("View Modes..."); - const subItems = existingVm && "subitems" in existingVm ? existingVm.subitems : []; - subItems.push({ description: "Freeform", event: () => { this.props.Document._viewType = CollectionViewType.Freeform; }, icon: "signature" }); - if (CollectionView._safeMode) { - ContextMenu.Instance.addItem({ description: "Test Freeform", event: () => this.props.Document._viewType = CollectionViewType.Invalid, icon: "project-diagram" }); - } - subItems.push({ description: "Schema", event: () => this.props.Document._viewType = CollectionViewType.Schema, icon: "th-list" }); - subItems.push({ description: "Treeview", event: () => this.props.Document._viewType = CollectionViewType.Tree, icon: "tree" }); - subItems.push({ description: "Stacking", event: () => this.props.Document._viewType = CollectionViewType.Stacking, icon: "ellipsis-v" }); - subItems.push({ - description: "Stacking (AutoHeight)", event: () => { - this.props.Document._viewType = CollectionViewType.Stacking; - this.props.Document._autoHeight = true; - }, icon: "ellipsis-v" - }); - subItems.push({ description: "Staff", event: () => this.props.Document._viewType = CollectionViewType.Staff, icon: "music" }); - subItems.push({ description: "Multicolumn", event: () => this.props.Document._viewType = CollectionViewType.Multicolumn, icon: "columns" }); - subItems.push({ description: "Multirow", event: () => this.props.Document._viewType = CollectionViewType.Multirow, icon: "columns" }); - subItems.push({ description: "Masonry", event: () => this.props.Document._viewType = CollectionViewType.Masonry, icon: "columns" }); - subItems.push({ description: "Carousel", event: () => this.props.Document._viewType = CollectionViewType.Carousel, icon: "columns" }); - subItems.push({ description: "Pivot/Time", event: () => this.props.Document._viewType = CollectionViewType.Time, icon: "columns" }); - subItems.push({ description: "Map", event: () => this.props.Document._viewType = CollectionViewType.Map, icon: "globe-americas" }); - switch (this.props.Document._viewType) { - case CollectionViewType.Freeform: { - subItems.push({ description: "Custom", icon: "fingerprint", event: AddCustomFreeFormLayout(this.props.Document, this.props.fieldKey) }); - break; - } - } - subItems.push({ description: "lightbox", event: action(() => this._isLightboxOpen = true), icon: "eye" }); - !existingVm && ContextMenu.Instance.addItem({ description: "View Modes...", subitems: subItems, icon: "eye" }); - const existing = ContextMenu.Instance.findByDescription("Layout..."); + this.setupViewTypes("Change Perspective...", (vtype => { this.props.Document._viewType = vtype; return this.props.Document; }), true); + this.setupViewTypes("Open New Perspective...", vtype => { + const newRendition = Doc.MakeAlias(this.props.Document); + newRendition._viewType = vtype; + this.props.addDocTab(newRendition, "onRight"); + return newRendition; + }, false); + + const existing = ContextMenu.Instance.findByDescription("Options..."); const layoutItems = existing && "subitems" in existing ? existing.subitems : []; layoutItems.push({ description: `${this.props.Document.forceActive ? "Select" : "Force"} Contents Active`, event: () => this.props.Document.forceActive = !this.props.Document.forceActive, icon: "project-diagram" }); if (this.props.Document.childLayout instanceof Doc) { layoutItems.push({ description: "View Child Layout", event: () => this.props.addDocTab(this.props.Document.childLayout as Doc, "onRight"), icon: "project-diagram" }); } - if (this.props.Document.childDetailed instanceof Doc) { - layoutItems.push({ description: "View Child Detailed Layout", event: () => this.props.addDocTab(this.props.Document.childDetailed as Doc, "onRight"), icon: "project-diagram" }); + if (this.props.Document.childDetailView instanceof Doc) { + layoutItems.push({ description: "View Child Detailed Layout", event: () => this.props.addDocTab(this.props.Document.childDetailView as Doc, "onRight"), icon: "project-diagram" }); } layoutItems.push({ description: `${this.props.Document.isInPlaceContainer ? "Unset" : "Set"} inPlace Container`, event: () => this.props.Document.isInPlaceContainer = !this.props.Document.isInPlaceContainer, icon: "project-diagram" }); - !existing && ContextMenu.Instance.addItem({ description: "Layout...", subitems: layoutItems, icon: "hand-point-right" }); - - const open = ContextMenu.Instance.findByDescription("Open..."); - const openItems = open && "subitems" in open ? open.subitems : []; - !open && ContextMenu.Instance.addItem({ description: "Open...", subitems: openItems, icon: "hand-point-right" }); + !existing && ContextMenu.Instance.addItem({ description: "Options...", subitems: layoutItems, icon: "hand-point-right" }); const existingOnClick = ContextMenu.Instance.findByDescription("OnClick..."); const onClicks = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; @@ -279,10 +278,10 @@ export class CollectionView extends Touchable<FieldViewProps> { onMovePrevRequest={action(() => this._curLightboxImg = (this._curLightboxImg + images.length - 1) % images.length)} onMoveNextRequest={action(() => this._curLightboxImg = (this._curLightboxImg + 1) % images.length)} />); } - @observable _facetWidth = 0; + get _facetWidth() { return NumCast(this.props.Document._facetWidth); } + set _facetWidth(value) { this.props.Document._facetWidth = value; } bodyPanelWidth = () => this.props.PanelWidth() - this.facetWidth(); - getTransform = () => this.props.ScreenToLocalTransform().translate(-this.facetWidth(), 0); facetWidth = () => Math.max(0, Math.min(this.props.PanelWidth() - 25, this._facetWidth)); @computed get dataDoc() { @@ -365,17 +364,22 @@ export class CollectionView extends Touchable<FieldViewProps> { Doc.GetProto(newFacet).type = DocumentType.COL; // forces item to show an open/close button instead ofa checkbox newFacet.treeViewExpandedView = "layout"; newFacet.treeViewOpen = true; - newFacet[newFacetField + "-min"] = ranged === undefined ? minVal : ranged[0]; - newFacet[newFacetField + "-max"] = ranged === undefined ? maxVal : ranged[1]; - Doc.GetProto(newFacet)[newFacetField + "-minThumb"] = minVal; - Doc.GetProto(newFacet)[newFacetField + "-maxThumb"] = maxVal; + const extendedMinVal = minVal - Math.min(1, Math.abs(maxVal - minVal) * .05); + const extendedMaxVal = maxVal + Math.min(1, Math.abs(maxVal - minVal) * .05); + newFacet[newFacetField + "-min"] = ranged === undefined ? extendedMinVal : ranged[0]; + newFacet[newFacetField + "-max"] = ranged === undefined ? extendedMaxVal : ranged[1]; + Doc.GetProto(newFacet)[newFacetField + "-minThumb"] = extendedMinVal; + Doc.GetProto(newFacet)[newFacetField + "-maxThumb"] = extendedMaxVal; newFacet.target = this.props.Document; const scriptText = `setDocFilterRange(this.target, "${facetHeader}", range)`; newFacet.onThumbChanged = ScriptField.MakeScript(scriptText, { this: Doc.name, range: "number" }); Doc.AddDocToList(facetCollection, this.props.fieldKey + "-filter", newFacet); } else { - newFacet = Docs.Create.TreeDocument([], { title: facetHeader, treeViewOpen: true, isFacetFilter: true }); + newFacet = new Doc(); + newFacet.title = facetHeader; + newFacet.treeViewOpen = true; + newFacet.type = DocumentType.COL; const capturedVariables = { layoutDoc: this.props.Document, dataDoc: this.dataDoc }; newFacet.data = ComputedField.MakeFunction(`readFacetData(layoutDoc, dataDoc, "${this.props.fieldKey}", "${facetHeader}")`, {}, capturedVariables); } @@ -385,11 +389,11 @@ export class CollectionView extends Touchable<FieldViewProps> { onPointerDown = (e: React.PointerEvent) => { setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => { - this._facetWidth = Math.max(this.props.ScreenToLocalTransform().transformPoint(e.clientX, 0)[0], 0); + this._facetWidth = this.props.PanelWidth() - Math.max(this.props.ScreenToLocalTransform().transformPoint(e.clientX, 0)[0], 0); return false; }), returnFalse, action(() => this._facetWidth = this.facetWidth() < 15 ? Math.min(this.props.PanelWidth() - 25, 200) : 0)); } - filterBackground = () => "dimGray"; + filterBackground = () => "rgba(105, 105, 105, 0.432)"; get ignoreFields() { return ["_docFilters", "_docRangeFilters"]; } // this makes the tree view collection ignore these filters (otherwise, the filters would filter themselves) @computed get scriptField() { const scriptText = "setDocFilter(containingTreeView, heading, this.title, checked)"; @@ -411,27 +415,44 @@ export class CollectionView extends Touchable<FieldViewProps> { <div className="collectionTimeView-addFacet" style={{ width: `${this.facetWidth()}px` }} onPointerDown={e => e.stopPropagation()}> <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout}> <div className="collectionTimeView-button"> - <span className="collectionTimeView-span">Facet Filters</span> <FontAwesomeIcon icon={faEdit} size={"lg"} /> + <span className="collectionTimeView-span">Facet Filters</span> </div> </Flyout> </div> <div className="collectionTimeView-tree" key="tree"> - <CollectionTreeView {...this.props} + <CollectionTreeView + Document={facetCollection} + DataDoc={facetCollection} + fieldKey={`${this.props.fieldKey}-filter`} CollectionView={this} - treeViewHideTitle={true} + ContainingCollectionDoc={this.props.ContainingCollectionDoc} + ContainingCollectionView={this.props.ContainingCollectionView} + PanelWidth={this.facetWidth} + PanelHeight={this.props.PanelHeight} NativeHeight={returnZero} NativeWidth={returnZero} + LibraryPath={emptyPath} + rootSelected={this.props.rootSelected} + renderDepth={1} + dropAction={this.props.dropAction} + ScreenToLocalTransform={this.props.ScreenToLocalTransform} + addDocTab={returnFalse} + pinToPres={returnFalse} + isSelected={returnFalse} + select={returnFalse} + bringToFront={emptyFunction} + active={this.props.active} + whenActiveChanged={returnFalse} + treeViewHideTitle={true} + ContentScaling={returnOne} + focus={returnFalse} treeViewHideHeaderFields={true} onCheckedClick={this.scriptField!} ignoreFields={this.ignoreFields} annotationsKey={""} dontRegisterView={true} - PanelWidth={this.facetWidth} - DataDoc={facetCollection} - Document={facetCollection} backgroundColor={this.filterBackground} - fieldKey={`${this.props.fieldKey}-filter`} moveDocument={returnFalse} removeDocument={returnFalse} addDocument={returnFalse} /> @@ -451,13 +472,13 @@ export class CollectionView extends Touchable<FieldViewProps> { }; return (<div className={"collectionView"} style={{ - pointerEvents: this.props.Document.isBackground ? "none" : "all", + pointerEvents: this.props.Document.isBackground ? "none" : undefined, boxShadow: this.props.Document.isBackground || this.collectionViewType === CollectionViewType.Linear ? undefined : `${Cast(Doc.UserDoc().activeWorkspace, Doc, null)?.darkScheme ? "rgb(30, 32, 31)" : "#9c9396"} ${StrCast(this.props.Document.boxShadow, "0.2vw 0.2vw 0.8vw")}` }} onContextMenu={this.onContextMenu}> {this.showIsTagged()} - <div style={{ width: `calc(100% - ${this.facetWidth()}px)`, marginLeft: `${this.facetWidth()}px` }}> + <div style={{ width: `calc(100% - ${this.facetWidth()}px)` }}> {this.collectionViewType !== undefined ? this.SubView(this.collectionViewType, props) : (null)} </div> {this.lightbox(DocListCast(this.props.Document[this.props.fieldKey]).filter(d => d.type === DocumentType.IMG).map(d => @@ -467,9 +488,7 @@ export class CollectionView extends Touchable<FieldViewProps> { : ""))} {!this.props.isSelected() || this.props.PanelHeight() < 100 || this.props.Document.hideFilterView ? (null) : - <div className="collectionTimeView-dragger" key="dragger" onPointerDown={this.onPointerDown} style={{ transform: `translate(${this.facetWidth()}px, 0px)` }} > - <span title="library View Dragger" style={{ width: "5px", position: "absolute", top: "0" }} /> - </div> + <div className="collectionTimeView-dragger" title="library View Dragger" onPointerDown={this.onPointerDown} style={{ right: this.facetWidth() - 10 }} /> } {this.filterView} </div>); diff --git a/src/client/views/collections/CollectionViewChromes.scss b/src/client/views/collections/CollectionViewChromes.scss index a691b4805..5203eb55f 100644 --- a/src/client/views/collections/CollectionViewChromes.scss +++ b/src/client/views/collections/CollectionViewChromes.scss @@ -61,16 +61,21 @@ pointer-events: all; // margin-top: 10px; } - .collectionViewBaseChrome-template { + .collectionViewBaseChrome-template, + .collectionViewBaseChrome-viewModes { display: grid; background: rgb(238, 238, 238); color:grey; margin-top:auto; margin-bottom:auto; + margin-left: 5px; + } + .collectionViewBaseChrome-viewModes { + margin-left: 25px; } .collectionViewBaseChrome-viewSpecs { - margin-left: 10px; + margin-left: 5px; display: grid; .collectionViewBaseChrome-filterIcon { diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx index ba95dce00..7315d2c4e 100644 --- a/src/client/views/collections/CollectionViewChromes.tsx +++ b/src/client/views/collections/CollectionViewChromes.tsx @@ -6,7 +6,6 @@ import { Doc, DocListCast } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; import { List } from "../../../new_fields/List"; import { listSpec } from "../../../new_fields/Schema"; -import { ScriptField } from "../../../new_fields/ScriptField"; import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types"; import { Utils, emptyFunction, setupMoveUpEvents } from "../../../Utils"; import { DragManager } from "../../util/DragManager"; @@ -16,8 +15,6 @@ import { COLLECTION_BORDER_WIDTH } from "../globalCssVariables.scss"; import { CollectionViewType } from "./CollectionView"; import { CollectionView } from "./CollectionView"; import "./CollectionViewChromes.scss"; -import * as Autosuggest from 'react-autosuggest'; -import KeyRestrictionRow from "./KeyRestrictionRow"; const datepicker = require('js-datepicker'); interface CollectionViewChromeProps { @@ -48,8 +45,8 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro }; _narrativeCommand = { params: ["target", "source"], title: "=> click item view", - script: "this.target.childDetailed = getDocTemplate(this.source?.[0])", - immediate: (source: Doc[]) => this.target.childDetailed = Doc.getDocTemplate(source?.[0]), + script: "this.target.childDetailView = getDocTemplate(this.source?.[0])", + immediate: (source: Doc[]) => this.target.childDetailView = Doc.getDocTemplate(source?.[0]), initialize: emptyFunction, }; _contentCommand = { @@ -84,62 +81,10 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro private _picker: any; private _commandRef = React.createRef<HTMLInputElement>(); private _viewRef = React.createRef<HTMLInputElement>(); - private _autosuggestRef = React.createRef<Autosuggest>(); @observable private _currentKey: string = ""; - @observable private _viewSpecsOpen: boolean = false; - @observable private _dateWithinValue: string = ""; - @observable private _dateValue: Date | string = ""; - @observable private _keyRestrictions: [JSX.Element, string][] = []; - @observable private suggestions: string[] = []; - @computed private get filterValue() { return Cast(this.props.CollectionView.props.Document.viewSpecScript, ScriptField); } - - getFilters = (script: string) => { - const re: any = /(!)?\(\(\(doc\.(\w+)\s+&&\s+\(doc\.\w+\s+as\s+\w+\)\.includes\(\"(\w+)\"\)/g; - const arr: any[] = re.exec(script); - const toReturn: Filter[] = []; - if (arr !== null) { - const filter: Filter = { - key: arr[2], - value: arr[3], - contains: (arr[1] === "!") ? false : true, - }; - toReturn.push(filter); - script = script.replace(arr[0], ""); - if (re.exec(script) !== null) { - toReturn.push(...this.getFilters(script)); - } - else { return toReturn; } - } - return toReturn; - } - - addKeyRestrictions = (fields: Filter[]) => { - - if (fields.length !== 0) { - for (let i = 0; i < fields.length; i++) { - this._keyRestrictions.push([<KeyRestrictionRow field={fields[i].key} value={fields[i].value} key={Utils.GenerateGuid()} contains={fields[i].contains} script={(value: string) => runInAction(() => this._keyRestrictions[i][1] = value)} />, ""]); - - } - if (this._keyRestrictions.length === 1) { - this._keyRestrictions.push([<KeyRestrictionRow field="" value="" key={Utils.GenerateGuid()} contains={true} script={(value: string) => runInAction(() => this._keyRestrictions[1][1] = value)} />, ""]); - } - } - else { - this._keyRestrictions.push([<KeyRestrictionRow field="" value="" key={Utils.GenerateGuid()} contains={true} script={(value: string) => runInAction(() => this._keyRestrictions[0][1] = value)} />, ""]); - this._keyRestrictions.push([<KeyRestrictionRow field="" value="" key={Utils.GenerateGuid()} contains={false} script={(value: string) => runInAction(() => this._keyRestrictions[1][1] = value)} />, ""]); - } - } componentDidMount = () => { - - let fields: Filter[] = []; - if (this.filterValue) { - const string = this.filterValue.script.originalScript; - fields = this.getFilters(string); - } - runInAction(() => { - this.addKeyRestrictions(fields); // chrome status is one of disabled, collapsed, or visible. this determines initial state from document const chromeStatus = this.props.CollectionView.props.Document._chromeStatus; if (chromeStatus) { @@ -158,7 +103,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro @undoBatch viewChanged = (e: React.ChangeEvent) => { //@ts-ignore - this.props.CollectionView.props.Document._viewType = e.target.selectedOptions[0].value; + this.document._viewType = e.target.selectedOptions[0].value; } commandChanged = (e: React.ChangeEvent) => { @@ -167,104 +112,93 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro } @action - openViewSpecs = (e: React.SyntheticEvent) => { - if (this._viewSpecsOpen) this.closeViewSpecs(); - else { - this._viewSpecsOpen = true; - - //@ts-ignore - if (!e.target?.classList[0]?.startsWith("qs")) { - this.closeDatePicker(); - } - - e.stopPropagation(); - document.removeEventListener("pointerdown", this.closeViewSpecs); - document.addEventListener("pointerdown", this.closeViewSpecs); - } + toggleViewSpecs = (e: React.SyntheticEvent) => { + this.document._facetWidth = this.document._facetWidth ? 0 : 200; + e.stopPropagation(); } @action closeViewSpecs = () => { - this._viewSpecsOpen = false; - document.removeEventListener("pointerdown", this.closeViewSpecs); - } - - @action - openDatePicker = (e: React.PointerEvent) => { - this.openViewSpecs(e); - if (this._picker) { - this._picker.alwaysShow = true; - this._picker.show(); - // TODO: calendar is offset when zoomed in/out - // this._picker.calendar.style.position = "absolute"; - // let transform = this.props.CollectionView.props.ScreenToLocalTransform(); - // let x = parseInt(this._picker.calendar.style.left) / transform.Scale; - // let y = parseInt(this._picker.calendar.style.top) / transform.Scale; - // this._picker.calendar.style.left = x; - // this._picker.calendar.style.top = y; - - e.stopPropagation(); - } - } - - @action - addKeyRestriction = (e: React.MouseEvent) => { - const index = this._keyRestrictions.length; - this._keyRestrictions.push([<KeyRestrictionRow field="" value="" key={Utils.GenerateGuid()} contains={true} script={(value: string) => runInAction(() => this._keyRestrictions[index][1] = value)} />, ""]); - - this.openViewSpecs(e); - } - - @action.bound - applyFilter = (e: React.MouseEvent) => { - - this.openViewSpecs(e); - - const keyRestrictionScript = "(" + this._keyRestrictions.map(i => i[1]).filter(i => i.length > 0).join(" && ") + ")"; - const yearOffset = this._dateWithinValue[1] === 'y' ? 1 : 0; - const monthOffset = this._dateWithinValue[1] === 'm' ? parseInt(this._dateWithinValue[0]) : 0; - const weekOffset = this._dateWithinValue[1] === 'w' ? parseInt(this._dateWithinValue[0]) : 0; - const dayOffset = (this._dateWithinValue[1] === 'd' ? parseInt(this._dateWithinValue[0]) : 0) + weekOffset * 7; - let dateRestrictionScript = ""; - if (this._dateValue instanceof Date) { - const lowerBound = new Date(this._dateValue.getFullYear() - yearOffset, this._dateValue.getMonth() - monthOffset, this._dateValue.getDate() - dayOffset); - const upperBound = new Date(this._dateValue.getFullYear() + yearOffset, this._dateValue.getMonth() + monthOffset, this._dateValue.getDate() + dayOffset + 1); - dateRestrictionScript = `((doc.creationDate as any).date >= ${lowerBound.valueOf()} && (doc.creationDate as any).date <= ${upperBound.valueOf()})`; - } - else { - const createdDate = new Date(this._dateValue); - if (!isNaN(createdDate.getTime())) { - const lowerBound = new Date(createdDate.getFullYear() - yearOffset, createdDate.getMonth() - monthOffset, createdDate.getDate() - dayOffset); - const upperBound = new Date(createdDate.getFullYear() + yearOffset, createdDate.getMonth() + monthOffset, createdDate.getDate() + dayOffset + 1); - dateRestrictionScript = `((doc.creationDate as any).date >= ${lowerBound.valueOf()} && (doc.creationDate as any).date <= ${upperBound.valueOf()})`; - } - } - const fullScript = dateRestrictionScript.length || keyRestrictionScript.length ? dateRestrictionScript.length ? - `${dateRestrictionScript} ${keyRestrictionScript.length ? "&&" : ""} (${keyRestrictionScript})` : - `(${keyRestrictionScript}) ${dateRestrictionScript.length ? "&&" : ""} ${dateRestrictionScript}` : - "true"; - - this.props.CollectionView.props.Document.viewSpecScript = ScriptField.MakeFunction(fullScript, { doc: Doc.name }); - } + this.document._facetWidth = 0; + } + + // @action + // openDatePicker = (e: React.PointerEvent) => { + // if (this._picker) { + // this._picker.alwaysShow = true; + // this._picker.show(); + // // TODO: calendar is offset when zoomed in/out + // // this._picker.calendar.style.position = "absolute"; + // // let transform = this.props.CollectionView.props.ScreenToLocalTransform(); + // // let x = parseInt(this._picker.calendar.style.left) / transform.Scale; + // // let y = parseInt(this._picker.calendar.style.top) / transform.Scale; + // // this._picker.calendar.style.left = x; + // // this._picker.calendar.style.top = y; + + // e.stopPropagation(); + // } + // } + + // <input className="collectionViewBaseChrome-viewSpecsMenu-rowRight" + // id={Utils.GenerateGuid()} + // ref={this.datePickerRef} + // value={this._dateValue instanceof Date ? this._dateValue.toLocaleDateString() : this._dateValue} + // onChange={(e) => runInAction(() => this._dateValue = e.target.value)} + // onPointerDown={this.openDatePicker} + // placeholder="Value" /> + // @action.bound + // applyFilter = (e: React.MouseEvent) => { + // const keyRestrictionScript = "(" + this._keyRestrictions.map(i => i[1]).filter(i => i.length > 0).join(" && ") + ")"; + // const yearOffset = this._dateWithinValue[1] === 'y' ? 1 : 0; + // const monthOffset = this._dateWithinValue[1] === 'm' ? parseInt(this._dateWithinValue[0]) : 0; + // const weekOffset = this._dateWithinValue[1] === 'w' ? parseInt(this._dateWithinValue[0]) : 0; + // const dayOffset = (this._dateWithinValue[1] === 'd' ? parseInt(this._dateWithinValue[0]) : 0) + weekOffset * 7; + // let dateRestrictionScript = ""; + // if (this._dateValue instanceof Date) { + // const lowerBound = new Date(this._dateValue.getFullYear() - yearOffset, this._dateValue.getMonth() - monthOffset, this._dateValue.getDate() - dayOffset); + // const upperBound = new Date(this._dateValue.getFullYear() + yearOffset, this._dateValue.getMonth() + monthOffset, this._dateValue.getDate() + dayOffset + 1); + // dateRestrictionScript = `((doc.creationDate as any).date >= ${lowerBound.valueOf()} && (doc.creationDate as any).date <= ${upperBound.valueOf()})`; + // } + // else { + // const createdDate = new Date(this._dateValue); + // if (!isNaN(createdDate.getTime())) { + // const lowerBound = new Date(createdDate.getFullYear() - yearOffset, createdDate.getMonth() - monthOffset, createdDate.getDate() - dayOffset); + // const upperBound = new Date(createdDate.getFullYear() + yearOffset, createdDate.getMonth() + monthOffset, createdDate.getDate() + dayOffset + 1); + // dateRestrictionScript = `((doc.creationDate as any).date >= ${lowerBound.valueOf()} && (doc.creationDate as any).date <= ${upperBound.valueOf()})`; + // } + // } + // const fullScript = dateRestrictionScript.length || keyRestrictionScript.length ? dateRestrictionScript.length ? + // `${dateRestrictionScript} ${keyRestrictionScript.length ? "&&" : ""} (${keyRestrictionScript})` : + // `(${keyRestrictionScript}) ${dateRestrictionScript.length ? "&&" : ""} ${dateRestrictionScript}` : + // "true"; + + // this.props.CollectionView.props.Document.viewSpecScript = ScriptField.MakeFunction(fullScript, { doc: Doc.name }); + // } + + // datePickerRef = (node: HTMLInputElement) => { + // if (node) { + // try { + // this._picker = datepicker("#" + node.id, { + // disabler: (date: Date) => date > new Date(), + // onSelect: (instance: any, date: Date) => runInAction(() => {}), // this._dateValue = date), + // dateSelected: new Date() + // }); + // } catch (e) { + // console.log("date picker exception:" + e); + // } + // } + // } - @action - closeDatePicker = () => { - if (this._picker) { - this._picker.alwaysShow = false; - this._picker.hide(); - } - document.removeEventListener("pointerdown", this.closeDatePicker); - } @action toggleCollapse = () => { - this.props.CollectionView.props.Document._chromeStatus = this.props.CollectionView.props.Document._chromeStatus === "enabled" ? "collapsed" : "enabled"; + this.document._chromeStatus = this.document._chromeStatus === "enabled" ? "collapsed" : "enabled"; if (this.props.collapse) { this.props.collapse(this.props.CollectionView.props.Document._chromeStatus !== "enabled"); } } subChrome = () => { - const collapsed = this.props.CollectionView.props.Document._chromeStatus !== "enabled"; + const collapsed = this.document._chromeStatus !== "enabled"; if (collapsed) return null; switch (this.props.type) { case CollectionViewType.Stacking: return (<CollectionStackingViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />); @@ -279,13 +213,6 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro return this.props.CollectionView.props.Document; } - @action.bound - clearFilter = () => { - this.props.CollectionView.props.Document.viewSpecScript = ScriptField.MakeFunction("true", { doc: Doc.name }); - this._keyRestrictions = []; - this.addKeyRestrictions([]); - } - private dropDisposer?: DragManager.DragDropDisposer; protected createDropTarget = (ele: HTMLDivElement) => { this.dropDisposer && this.dropDisposer(); @@ -304,48 +231,6 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro return true; } - datePickerRef = (node: HTMLInputElement) => { - if (node) { - try { - this._picker = datepicker("#" + node.id, { - disabler: (date: Date) => date > new Date(), - onSelect: (instance: any, date: Date) => runInAction(() => this._dateValue = date), - dateSelected: new Date() - }); - } catch (e) { - console.log("date picker exception:" + e); - } - } - } - - renderSuggestion = (suggestion: string) => { - return <p>{suggestion}</p>; - } - getSuggestionValue = (suggestion: string) => suggestion; - - @action - onKeyChange = (e: React.ChangeEvent, { newValue }: { newValue: string }) => { - this._currentKey = newValue; - } - onSuggestionFetch = async ({ value }: { value: string }) => { - const sugg = await this.getKeySuggestions(value); - runInAction(() => this.suggestions = sugg); - } - @action - onSuggestionClear = () => { - this.suggestions = []; - } - getKeySuggestions = async (value: string): Promise<string[]> => { - return this._buttonizableCommands.filter(c => c.title.indexOf(value) !== -1).map(c => c.title); - } - - autoSuggestDown = (e: React.PointerEvent) => { - e.stopPropagation(); - } - - private _startDragPosition: { x: number, y: number } = { x: 0, y: 0 }; - private _sensitivity: number = 16; - dragViewDown = (e: React.PointerEvent) => { setupMoveUpEvents(this, e, (e, down, delta) => { const vtype = this.props.CollectionView.collectionViewType; @@ -367,7 +252,6 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro { target: this.props.CollectionView.props.Document }, c.params, c.initialize, e.clientX, e.clientY)); return true; }, emptyFunction, emptyFunction); - this._startDragPosition = { x: e.clientX, y: e.clientY }; } render() { @@ -392,7 +276,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro title="Collapse collection chrome" onClick={this.toggleCollapse}> <FontAwesomeIcon icon="caret-up" size="2x" /> </button> - <div className="collectionViewBaseChrome-template" style={{ marginLeft: 25, display: collapsed ? "none" : undefined }}> + <div className="collectionViewBaseChrome-viewModes" style={{ display: collapsed ? "none" : undefined }}> <div className="commandEntry-outerDiv" title="drop document to apply or drag to create button" ref={this._viewRef} onPointerDown={this.dragViewDown}> <div className="commandEntry-drop"> <FontAwesomeIcon icon="bullseye" size="2x"></FontAwesomeIcon> @@ -402,60 +286,22 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro onPointerDown={stopPropagation} onChange={this.viewChanged} value={StrCast(this.props.CollectionView.props.Document._viewType)}> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="freeform">Freeform</option> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="schema">Schema</option> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="tree">Tree</option> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="stacking">Stacking</option> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="masonry">Masonry</option> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="multicolumn">MultiCol</option> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="multirow">MultiRow</option> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="time">Pivot/Time</option> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="carousel">Carousel</option> + {Object.values(CollectionViewType).map(type => ["invalid", "docking"].includes(type) ? (null) : ( + <option + key={Utils.GenerateGuid()} + className="collectionViewBaseChrome-viewOption" + onPointerDown={stopPropagation} + value={type}> + {type[0].toUpperCase() + type.substring(1)} + </option> + ))} </select> </div> </div> <div className="collectionViewBaseChrome-viewSpecs" title="filter documents to show" style={{ display: collapsed ? "none" : "grid" }}> - <div className="collectionViewBaseChrome-filterIcon" onPointerDown={this.openViewSpecs} > + <div className="collectionViewBaseChrome-filterIcon" onPointerDown={this.toggleViewSpecs} > <FontAwesomeIcon icon="filter" size="2x" /> </div> - <div className="collectionViewBaseChrome-viewSpecsMenu" - onPointerDown={this.openViewSpecs} - style={{ - height: this._viewSpecsOpen ? "fit-content" : "0px", - overflow: this._viewSpecsOpen ? "initial" : "hidden" - }}> - {this._keyRestrictions.map(i => i[0])} - <div className="collectionViewBaseChrome-viewSpecsMenu-row"> - <div className="collectionViewBaseChrome-viewSpecsMenu-rowLeft"> - CREATED WITHIN: - </div> - <select className="collectionViewBaseChrome-viewSpecsMenu-rowMiddle" - style={{ textTransform: "uppercase", textAlign: "center" }} - value={this._dateWithinValue} - onChange={(e) => runInAction(() => this._dateWithinValue = e.target.value)}> - <option value="1d">1 day of</option> - <option value="3d">3 days of</option> - <option value="1w">1 week of</option> - <option value="2w">2 weeks of</option> - <option value="1m">1 month of</option> - <option value="2m">2 months of</option> - <option value="6m">6 months of</option> - <option value="1y">1 year of</option> - </select> - <input className="collectionViewBaseChrome-viewSpecsMenu-rowRight" - id={Utils.GenerateGuid()} - ref={this.datePickerRef} - value={this._dateValue instanceof Date ? this._dateValue.toLocaleDateString() : this._dateValue} - onChange={(e) => runInAction(() => this._dateValue = e.target.value)} - onPointerDown={this.openDatePicker} - placeholder="Value" /> - </div> - <div className="collectionViewBaseChrome-viewSpecsMenu-lastRow"> - <button className="collectonViewBaseChrome-viewSpecsMenu-lastRowButton" onClick={this.addKeyRestriction}> ADD KEY RESTRICTION </button> - <button className="collectonViewBaseChrome-viewSpecsMenu-lastRowButton" onClick={this.applyFilter}> APPLY FILTER </button> - <button className="collectonViewBaseChrome-viewSpecsMenu-lastRowButton" onClick={this.clearFilter}> CLEAR </button> - </div> - </div> </div> <div className="collectionViewBaseChrome-template" ref={this.createDropTarget} style={{ display: collapsed ? "none" : undefined }}> <div className="commandEntry-outerDiv" title="drop document to apply or drag to create button" ref={this._commandRef} onPointerDown={this.dragCommandDown}> diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx index 5c9d2b0dd..10c6ead1a 100644 --- a/src/client/views/collections/ParentDocumentSelector.tsx +++ b/src/client/views/collections/ParentDocumentSelector.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import './ParentDocumentSelector.scss'; import { Doc } from "../../../new_fields/Doc"; import { observer } from "mobx-react"; -import { observable, action, runInAction, trace, computed } from "mobx"; +import { observable, action, runInAction, trace, computed, reaction, IReactionDisposer } from "mobx"; import { Id } from "../../../new_fields/FieldSymbols"; import { SearchUtil } from "../../util/SearchUtil"; import { CollectionDockingView } from "./CollectionDockingView"; @@ -31,13 +31,14 @@ type SelectorProps = { export class SelectorContextMenu extends React.Component<SelectorProps> { @observable private _docs: { col: Doc, target: Doc }[] = []; @observable private _otherDocs: { col: Doc, target: Doc }[] = []; + _reaction: IReactionDisposer | undefined; - constructor(props: SelectorProps) { - super(props); - - this.fetchDocuments(); + componentDidMount() { + this._reaction = reaction(() => this.props.Document, () => this.fetchDocuments(), { fireImmediately: true }); + } + componentWillUnmount() { + this._reaction?.(); } - async fetchDocuments() { const aliases = (await SearchUtil.GetAliasesOfDocument(this.props.Document)).filter(doc => doc !== this.props.Document); const { docs } = await SearchUtil.Search("", true, { fq: `data_l:"${this.props.Document[Id]}"` }); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss index 75af11537..05111adb4 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss @@ -4,6 +4,7 @@ pointer-events: all; stroke-width: 3px; transition: opacity 0.5s ease-in; + fill: transparent; } .collectionfreeformlinkview-linkCircle { stroke: rgb(0,0,0); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index 09fc5148e..cf12ef382 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -25,7 +25,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo this._anchorDisposer = reaction(() => [this.props.A.props.ScreenToLocalTransform(), this.props.B.props.ScreenToLocalTransform(), this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document), this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document)], action(() => { setTimeout(action(() => this._opacity = 1), 0); // since the render code depends on querying the Dom through getBoudndingClientRect, we need to delay triggering render() - setTimeout(action(() => this._opacity = 0.05), 750); // this will unhighlight the link line. + setTimeout(action(() => (!this.props.LinkDocs.length || !this.props.LinkDocs[0].linkDisplay) && (this._opacity = 0.05)), 750); // this will unhighlight the link line. const acont = this.props.A.props.Document.type === DocumentType.LINK ? this.props.A.ContentDiv!.getElementsByClassName("linkAnchorBox-cont") : []; const bcont = this.props.B.props.Document.type === DocumentType.LINK ? this.props.B.ContentDiv!.getElementsByClassName("linkAnchorBox-cont") : []; const adiv = (acont.length ? acont[0] : this.props.A.ContentDiv!); @@ -81,6 +81,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo } render() { + this.props.A.props.ScreenToLocalTransform().transform(this.props.B.props.ScreenToLocalTransform()); const acont = this.props.A.props.Document.type === DocumentType.LINK ? this.props.A.ContentDiv!.getElementsByClassName("linkAnchorBox-cont") : []; const bcont = this.props.B.props.Document.type === DocumentType.LINK ? this.props.B.ContentDiv!.getElementsByClassName("linkAnchorBox-cont") : []; const a = (acont.length ? acont[0] : this.props.A.ContentDiv!).getBoundingClientRect(); @@ -93,17 +94,26 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo apt.point.x, apt.point.y); const pt1 = [apt.point.x, apt.point.y]; const pt2 = [bpt.point.x, bpt.point.y]; + const pt1vec = [pt1[0] - (a.left + a.width / 2), pt1[1] - (a.top + a.height / 2)]; + const pt2vec = [pt2[0] - (b.left + b.width / 2), pt2[1] - (b.top + b.height / 2)]; + const pt1len = Math.sqrt((pt1vec[0] * pt1vec[0]) + (pt1vec[1] * pt1vec[1])); + const pt2len = Math.sqrt((pt2vec[0] * pt2vec[0]) + (pt2vec[1] * pt2vec[1])); + const ptlen = Math.sqrt((pt1[0] - pt2[0]) * (pt1[0] - pt2[0]) + (pt1[1] - pt2[1]) * (pt1[1] - pt2[1])) / 3; + const pt1norm = [pt1vec[0] / pt1len * ptlen, pt1vec[1] / pt1len * ptlen]; + const pt2norm = [pt2vec[0] / pt2len * ptlen, pt2vec[1] / pt2len * ptlen]; const aActive = this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document); const bActive = this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document); const text = StrCast(this.props.A.props.Document.linkRelationship); - return !aActive && !bActive ? (null) : (<> + return !a.width || !b.width || ((!this.props.LinkDocs.length || !this.props.LinkDocs[0].linkDisplay) && !aActive && !bActive) ? (null) : (<> <text x={(pt1[0] + pt2[0]) / 2} y={(pt1[1] + pt2[1]) / 2}> {text !== "-ungrouped-" ? text : ""} </text> - <line key="linkLine" className="collectionfreeformlinkview-linkLine" + <path className="collectionfreeformlinkview-linkLine" style={{ opacity: this._opacity, strokeDasharray: "2 2" }} + d={`M ${pt1[0]} ${pt1[1]} C ${pt1[0] + pt1norm[0]} ${pt1[1] + pt1norm[1]}, ${pt2[0] + pt2norm[0]} ${pt2[1] + pt2norm[1]}, ${pt2[0]} ${pt2[1]}`} /> + {/* <line key="linkLine" className="collectionfreeformlinkview-linkLine" style={{ opacity: this._opacity, strokeDasharray: "2 2" }} x1={`${pt1[0]}`} y1={`${pt1[1]}`} - x2={`${pt2[0]}`} y2={`${pt2[1]}`} /> + x2={`${pt2[0]}`} y2={`${pt2[1]}`} /> */} </>); } }
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx index d12f93f15..702b02a20 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx @@ -31,14 +31,14 @@ export class CollectionFreeFormLinksView extends React.Component { }, [] as { a: DocumentView, b: DocumentView, l: Doc[] }[]); return connections.filter(c => c.a.props.layoutKey && c.b.props.layoutKey && c.a.props.Document.type === DocumentType.LINK && - c.a.props.bringToFront !== emptyFunction && c.b.props.bringToFront !== emptyFunction // this prevents links to be drawn to anchors in CollectionTree views -- this is a hack that should be fixed + c.a.props.bringToFront !== emptyFunction && c.b.props.bringToFront !== emptyFunction // bcz: this prevents links to be drawn to anchors in CollectionTree views -- this is a hack that should be fixed ).map(c => <CollectionFreeFormLinkView key={Utils.GenerateGuid()} A={c.a} B={c.b} LinkDocs={c.l} />); } render() { return <div className="collectionfreeformlinksview-container"> <svg className="collectionfreeformlinksview-svgCanvas"> - {SelectionManager.GetIsDragging() ? (null) : this.uniqueConnections} + {this.uniqueConnections} </svg> {this.props.children} </div>; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index e1516b468..a00311a9c 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -36,6 +36,7 @@ height: 100%; display: flex; align-items: center; + overflow: hidden; .collectionfreeformview-placeholderSpan { font-size: 32; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 7dca19073..8ead1c300 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -15,7 +15,7 @@ import { ScriptField } from "../../../../new_fields/ScriptField"; import { BoolCast, Cast, FieldValue, NumCast, ScriptCast, StrCast } from "../../../../new_fields/Types"; import { TraceMobx } from "../../../../new_fields/util"; import { GestureUtils } from "../../../../pen-gestures/GestureUtils"; -import { aggregateBounds, intersectRect, returnOne, Utils, returnZero } from "../../../../Utils"; +import { aggregateBounds, intersectRect, returnOne, Utils, returnZero, returnFalse } from "../../../../Utils"; import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; import { DocServer } from "../../../DocServer"; import { Docs } from "../../../documents/Documents"; @@ -31,7 +31,7 @@ import { ContextMenu } from "../../ContextMenu"; import { ContextMenuProps } from "../../ContextMenuItem"; import { InkingControl } from "../../InkingControl"; import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView"; -import { DocumentViewProps } from "../../nodes/DocumentView"; +import { DocumentViewProps, DocumentView } from "../../nodes/DocumentView"; import { FormattedTextBox } from "../../nodes/FormattedTextBox"; import { pageSchema } from "../../nodes/ImageBox"; import PDFMenu from "../../pdf/PDFMenu"; @@ -43,6 +43,7 @@ import "./CollectionFreeFormView.scss"; import MarqueeOptionsMenu from "./MarqueeOptionsMenu"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); +import { CollectionViewType } from "../CollectionView"; library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload); @@ -68,10 +69,12 @@ type PanZoomDocument = makeInterface<[typeof panZoomSchema, typeof documentSchem const PanZoomDocument = makeInterface(panZoomSchema, documentSchema, positionSchema, pageSchema); export type collectionFreeformViewProps = { forceScaling?: boolean; // whether to force scaling of content (needed by ImageBox) + childClickScript?: ScriptField; + viewDefDivClick?: ScriptField; }; @observer -export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument, undefined as any as collectionFreeformViewProps) { +export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, Partial<collectionFreeformViewProps>>(PanZoomDocument) { private _lastX: number = 0; private _lastY: number = 0; private _downX: number = 0; @@ -800,7 +803,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument, u if (!annotOn) { this.props.focus(doc); } else { - const contextHgt = Doc.AreProtosEqual(annotOn, this.props.Document) && this.props.VisibleHeight ? this.props.VisibleHeight() : NumCast(annotOn.height); + const contextHgt = Doc.AreProtosEqual(annotOn, this.props.Document) && this.props.VisibleHeight ? this.props.VisibleHeight() : NumCast(annotOn._height); const offset = annotOn && (contextHgt / 2 * 96 / 72); this.props.Document.scrollY = NumCast(doc.y) - offset; } @@ -816,9 +819,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument, u const savedState = { px: this.Document._panX, py: this.Document._panY, s: this.Document.scale, pt: this.Document.panTransformType }; - if (!willZoom) { + if (!willZoom && DocumentView._focusHack.length) { Doc.BrushDoc(this.props.Document); - !doc.z && this.scaleAtPt([NumCast(doc.x), NumCast(doc.y)], 1); + !doc.z && NumCast(this.layoutDoc.scale) < 1 && this.scaleAtPt(DocumentView._focusHack, 1); // [NumCast(doc.x), NumCast(doc.y)], 1); } else { if (DocListCast(this.dataDoc[this.props.fieldKey]).includes(doc)) { if (!doc.z) this.setPan(newPanX, newPanY, "Ease", true); // docs that are floating in their collection can't be panned to from their collection -- need to propagate the pan to a parent freeform somehow @@ -846,7 +849,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument, u } @computed get libraryPath() { return this.props.LibraryPath ? [...this.props.LibraryPath, this.props.Document] : []; } - @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); } + @computed get onChildClickHandler() { return this.props.childClickScript || ScriptCast(this.Document.onChildClick); } backgroundHalo = () => BoolCast(this.Document.useClusters); getChildDocumentViewProps(childLayout: Doc, childData?: Doc): DocumentViewProps { @@ -860,7 +863,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument, u LibraryPath: this.libraryPath, FreezeDimensions: this.props.freezeChildDimensions, layoutKey: undefined, - rootSelected: this.rootSelected, + rootSelected: childData ? this.rootSelected : returnFalse, dropAction: StrCast(this.props.Document.childDropAction) as dropActionType, //onClick: undefined, // this.props.onClick, // bcz: check this out -- I don't think we want to inherit click handlers, or we at least need a way to ignore them onClick: this.onChildClickHandler, @@ -905,7 +908,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument, u } onViewDefDivClick = (e: React.MouseEvent, payload: any) => { - (this.props.Document.onViewDefDivClick as ScriptField)?.script.run({ this: this.props.Document, payload }); + (this.props.viewDefDivClick || ScriptCast(this.props.Document.onViewDefDivClick))?.script.run({ this: this.props.Document, payload }); + e.stopPropagation(); } private viewDefToJSX(viewDef: ViewDefBounds): Opt<ViewDefResult> { const { x, y, z } = viewDef; @@ -990,6 +994,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument, u {...this.getChildDocumentViewProps(pair.layout, pair.data)} dataProvider={this.childDataProvider} LayoutDoc={this.childLayoutDocFunc} + pointerEvents={this.props.layoutEngine?.() !== undefined ? false : undefined} jitterRotation={NumCast(this.props.Document.jitterRotation)} fitToBox={this.props.fitToBox || BoolCast(this.props.freezeChildDimensions)} FreezeDimensions={BoolCast(this.props.freezeChildDimensions)} @@ -1043,16 +1048,17 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument, u onContextMenu = (e: React.MouseEvent) => { if (this.props.children && this.props.annotationsKey) return; - const layoutItems: ContextMenuProps[] = []; - - layoutItems.push({ description: "reset view", event: () => { this.props.Document._panX = this.props.Document._panY = 0; this.props.Document.scale = 1; }, icon: "compress-arrows-alt" }); - layoutItems.push({ description: `${this.Document._LODdisable ? "Enable LOD" : "Disable LOD"}`, event: () => this.Document._LODdisable = !this.Document._LODdisable, icon: "table" }); - layoutItems.push({ description: `${this.fitToContent ? "Unset" : "Set"} Fit To Container`, event: () => this.Document._fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" }); - layoutItems.push({ description: `${this.Document.useClusters ? "Uncluster" : "Use Clusters"}`, event: () => this.updateClusters(!this.Document.useClusters), icon: "braille" }); - layoutItems.push({ description: "Arrange contents in grid", event: this.layoutDocsInGrid, icon: "table" }); + const options = ContextMenu.Instance.findByDescription("Options..."); + const optionItems: ContextMenuProps[] = options && "subitems" in options ? options.subitems : []; + + optionItems.push({ description: "reset view", event: () => { this.props.Document._panX = this.props.Document._panY = 0; this.props.Document.scale = 1; }, icon: "compress-arrows-alt" }); + optionItems.push({ description: `${this.Document._LODdisable ? "Enable LOD" : "Disable LOD"}`, event: () => this.Document._LODdisable = !this.Document._LODdisable, icon: "table" }); + optionItems.push({ description: `${this.fitToContent ? "Unset" : "Set"} Fit To Container`, event: () => this.Document._fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" }); + optionItems.push({ description: `${this.Document.useClusters ? "Uncluster" : "Use Clusters"}`, event: () => this.updateClusters(!this.Document.useClusters), icon: "braille" }); + optionItems.push({ description: "Arrange contents in grid", event: this.layoutDocsInGrid, icon: "table" }); // layoutItems.push({ description: "Analyze Strokes", event: this.analyzeStrokes, icon: "paint-brush" }); - layoutItems.push({ description: "Jitter Rotation", event: action(() => this.props.Document.jitterRotation = 10), icon: "paint-brush" }); - layoutItems.push({ + optionItems.push({ description: "Jitter Rotation", event: action(() => this.props.Document.jitterRotation = (this.props.Document.jitterRotation ? 0 : 10)), icon: "paint-brush" }); + optionItems.push({ description: "Import document", icon: "upload", event: ({ x, y }) => { const input = document.createElement("input"); input.type = "file"; @@ -1080,7 +1086,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument, u } }); - ContextMenu.Instance.addItem({ description: "Freeform Options ...", subitems: layoutItems, icon: "eye" }); + ContextMenu.Instance.addItem({ description: "Options...", subitems: optionItems, icon: "eye" }); } private childViews = () => { @@ -1105,10 +1111,14 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument, u _nudgeTime = 0; nudge = action((x: number, y: number) => { - this.setPan(NumCast(this.layoutDoc._panX) + this.props.PanelWidth() / 2 * x / this.zoomScaling(), - NumCast(this.layoutDoc._panY) + this.props.PanelHeight() / 2 * (-y) / this.zoomScaling(), "Ease", true); - this._nudgeTime = Date.now(); - setTimeout(() => (Date.now() - this._nudgeTime >= 500) && (this.Document.panTransformType = undefined), 500); + if (this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Freeform) { // bcz: this isn't ideal, but want to try it out... + this.setPan(NumCast(this.layoutDoc._panX) + this.props.PanelWidth() / 2 * x / this.zoomScaling(), + NumCast(this.layoutDoc._panY) + this.props.PanelHeight() / 2 * (-y) / this.zoomScaling(), "Ease", true); + this._nudgeTime = Date.now(); + setTimeout(() => (Date.now() - this._nudgeTime >= 500) && (this.Document.panTransformType = undefined), 500); + return true; + } + return false; }); @computed get marqueeView() { return <MarqueeView {...this.props} nudge={this.nudge} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} addDocument={this.addDocument} diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss index 18d6da0da..1291e7dc1 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss @@ -6,7 +6,6 @@ width:100%; height:100%; overflow: hidden; - pointer-events: inherit; border-radius: inherit; } diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 5a6c8573b..454c3a5f2 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -31,7 +31,7 @@ interface MarqueeViewProps { addLiveTextDocument: (doc: Doc) => void; isSelected: () => boolean; isAnnotationOverlay?: boolean; - nudge:(x:number, y:number) => void; + nudge: (x: number, y: number) => boolean; setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void; } @@ -309,7 +309,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this.hideMarquee(); } - getCollection = (selected: Doc[], asTemplate: boolean, isBackground: boolean = false) => { + getCollection = (selected: Doc[], asTemplate: boolean, isBackground?: boolean) => { const bounds = this.Bounds; // const inkData = this.ink ? this.ink.inkData : undefined; const creator = asTemplate ? Docs.Create.StackingDocument : Docs.Create.FreeformDocument; diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index 7078cc01c..6ff6d1b42 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -20,6 +20,7 @@ import { DocumentView } from "./DocumentView"; import { Docs } from "../../documents/Documents"; import { ComputedField } from "../../../new_fields/ScriptField"; import { Networking } from "../../Network"; +import { Upload } from "../../../server/SharedMediaTypes"; // testing testing @@ -146,7 +147,9 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument AudioBox.ActiveRecordings.push(this.props.Document); this._recorder.ondataavailable = async (e: any) => { const [{ result }] = await Networking.UploadFilesToServer(e.data); - this.props.Document[this.props.fieldKey] = new AudioField(Utils.prepend(result.accessPaths.agnostic.client)); + if (!(result instanceof Error)) { + this.props.Document[this.props.fieldKey] = new AudioField(Utils.prepend(result.accessPaths.agnostic.client)); + } }; this._recordStart = new Date().getTime(); runInAction(() => this.audioState = "recording"); @@ -159,7 +162,7 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument const funcs: ContextMenuProps[] = []; funcs.push({ description: (this.layoutDoc.playOnSelect ? "Don't play" : "Play") + " when document selected", event: () => this.layoutDoc.playOnSelect = !this.layoutDoc.playOnSelect, icon: "expand-arrows-alt" }); - ContextMenu.Instance.addItem({ description: "Audio Funcs...", subitems: funcs, icon: "asterisk" }); + ContextMenu.Instance.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); } stopRecording = action(() => { diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 3503af442..4b282b0c9 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -1,4 +1,3 @@ -import anime from "animejs"; import { computed, IReactionDisposer, observable, reaction, trace } from "mobx"; import { observer } from "mobx-react"; import { Doc, HeightSym, WidthSym } from "../../../new_fields/Doc"; @@ -29,8 +28,14 @@ export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { @observer export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps, PositionDocument>(PositionDocument) { @observable _animPos: number[] | undefined = undefined; + random(min: number, max: number) { // min should not be equal to max + const mseed = Math.abs(this.X * this.Y); + const seed = (mseed * 9301 + 49297) % 233280; + const rnd = seed / 233280; + return min + rnd * (max - min); + } get displayName() { return "CollectionFreeFormDocumentView(" + this.props.Document.title + ")"; } // this makes mobx trace() statements more descriptive - get transform() { return `scale(${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) rotate(${anime.random(-1, 1) * this.props.jitterRotation}deg)`; } + get transform() { return `scale(${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) rotate(${this.random(-1, 1) * this.props.jitterRotation}deg)`; } get X() { return this.renderScriptDim ? this.renderScriptDim.x : this.props.x !== undefined ? this.props.x : this.dataProvider ? this.dataProvider.x : (this.Document.x || 0); } get Y() { return this.renderScriptDim ? this.renderScriptDim.y : this.props.y !== undefined ? this.props.y : this.dataProvider ? this.dataProvider.y : (this.Document.y || 0); } get ZInd() { return this.dataProvider ? this.dataProvider.zIndex : (this.Document.zIndex || 0); } @@ -60,8 +65,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF return undefined; } nudge = (x: number, y: number) => { - this.layoutDoc.x = NumCast(this.layoutDoc.x) + x; - this.layoutDoc.y = NumCast(this.layoutDoc.y) + y; + this.props.Document.x = NumCast(this.props.Document.x) + x; + this.props.Document.y = NumCast(this.props.Document.y) + y; } contentScaling = () => this.nativeWidth > 0 && !this.props.fitToBox && !this.freezeDimensions ? this.width / this.nativeWidth : 1; @@ -92,7 +97,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF height: this.height, zIndex: this.ZInd, display: this.ZInd === -99 ? "none" : undefined, - pointerEvents: this.props.Document.isBackground ? "none" : undefined + pointerEvents: this.props.Document.isBackground ? "none" : this.props.pointerEvents ? "all" : undefined }} > {!this.props.fitToBox ? diff --git a/src/client/views/nodes/DocumentBox.tsx b/src/client/views/nodes/DocumentBox.tsx index ac562f19a..7583aa070 100644 --- a/src/client/views/nodes/DocumentBox.tsx +++ b/src/client/views/nodes/DocumentBox.tsx @@ -45,7 +45,7 @@ export class DocHolderBox extends ViewBoxAnnotatableComponent<FieldViewProps, Do funcs.push({ description: (this.props.Document.excludeCollections ? "Include" : "Exclude") + " Collections", event: () => Doc.GetProto(this.props.Document).excludeCollections = !this.props.Document.excludeCollections, icon: "expand-arrows-alt" }); funcs.push({ description: `${this.props.Document.forceActive ? "Select" : "Force"} Contents Active`, event: () => this.props.Document.forceActive = !this.props.Document.forceActive, icon: "project-diagram" }); - ContextMenu.Instance.addItem({ description: "DocumentBox Funcs...", subitems: funcs, icon: "asterisk" }); + ContextMenu.Instance.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); } @computed get contentDoc() { return (this.props.Document.isTemplateDoc || this.props.Document.isTemplateForField ? this.props.Document : Doc.GetProto(this.props.Document)); diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 831bce22f..90d2d4936 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -1,8 +1,8 @@ import { computed } from "mobx"; import { observer } from "mobx-react"; -import { Doc, Opt } from "../../../new_fields/Doc"; -import { Cast, StrCast } from "../../../new_fields/Types"; -import { OmitKeys, Without } from "../../../Utils"; +import { Doc, Opt, Field } from "../../../new_fields/Doc"; +import { Cast, StrCast, NumCast } from "../../../new_fields/Types"; +import { OmitKeys, Without, emptyPath } from "../../../Utils"; import DirectoryImportBox from "../../util/Import & Export/DirectoryImportBox"; import { CollectionDockingView } from "../collections/CollectionDockingView"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; @@ -36,8 +36,10 @@ import { WebBox } from "./WebBox"; import { InkingStroke } from "../InkingStroke"; import React = require("react"); import { RecommendationsBox } from "../RecommendationsBox"; - import { TraceMobx } from "../../../new_fields/util"; +import { ScriptField } from "../../../new_fields/ScriptField"; +import XRegExp = require("xregexp"); + const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? type BindingProps = Without<FieldViewProps, 'fieldKey'>; @@ -54,6 +56,43 @@ class ObserverJsxParser1 extends JsxParser { const ObserverJsxParser: typeof JsxParser = ObserverJsxParser1 as any; + +interface HTMLtagProps { + Document: Doc; + htmltag: string; + onClick?: ScriptField; +} +//"<HTMLdiv borderRadius='100px' onClick={this.bannerColor=this.bannerColor==='red'?'green':'red'} width='100%' height='100%' transform='rotate({2*this.x+this.y}deg)'><ImageBox {...props} fieldKey={'data'}/><HTMLspan width='100%' marginTop='50%' height='10%' position='absolute' backgroundColor='{this.bannerColor===`green`?`dark`:`light`}grey'>{this.title}</HTMLspan></HTMLdiv>"@observer +@observer +export class HTMLtag extends React.Component<HTMLtagProps> { + click = (e: React.MouseEvent) => { + const clickScript = (this.props as any).onClick as Opt<ScriptField>; + clickScript?.script.run({ this: this.props.Document }); + } + render() { + const style: { [key: string]: any } = {}; + const divKeys = OmitKeys(this.props, ["children", "htmltag", "Document", "key", "onClick", "__proto__"]).omit; + Object.keys(divKeys).map((prop: string) => { + let p = (this.props as any)[prop] as string; + const replacer = (match: any, expr: string, offset: any, string: any) => { // bcz: extend this to support expression -- is this really a script? + return ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name })?.script.run({ this: this.props.Document }).result as string || ""; + }; + p = p?.replace(/{([^.'][^}']+)}/g, replacer); + + const replacer2 = (match: any, key: string, offset: any, string: any) => { // bcz: extend this to support expression -- is this really a script? + const n = Cast(this.props.Document[key], "number", null); + return n ? n.toString() : StrCast(this.props.Document[key], p); + }; + style[prop] = p?.replace(/@([a-zA-Z0-9-_]+)/g, replacer2); + + }); + const Tag = this.props.htmltag as keyof JSX.IntrinsicElements; + return <Tag style={style} onClick={this.click}> + {this.props.children} + </Tag>; + } +} + @observer export class DocumentContentsView extends React.Component<DocumentViewProps & { isSelected: (outsideReaction: boolean) => boolean, @@ -92,32 +131,67 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { return Doc.expandTemplateLayout(template, this.props.Document, params ? "(" + params + ")" : this.props.layoutKey); } - CreateBindings(): JsxBindings { + CreateBindings(onClick: Opt<ScriptField>): JsxBindings { const list = { ...OmitKeys(this.props, ['parentActive'], (obj: any) => obj.active = this.props.parentActive).omit, Document: this.layoutDoc, DataDoc: this.dataDoc, + onClick: onClick }; return { props: list }; } render() { TraceMobx(); - return (this.props.renderDepth > 12 || !this.layout || !this.layoutDoc) ? (null) : + let layoutFrame = this.layout; + + // replace code content with a script >{content}< as in <HTMLdiv>{this.title}</HTMLdiv> + const replacer = (match: any, prefix: string, expr: string, postfix: string, offset: any, string: any) => { + return prefix + (ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name })?.script.run({ this: this.props.Document }).result as string || "") + postfix; + }; + layoutFrame = layoutFrame.replace(/(>[^{]*)\{([^.'][^<}]+)\}([^}]*<)/g, replacer); + + // replace HTML<tag> with corresponding HTML tag as in: <HTMLdiv> becomes <HTMLtag Document={props.Document} htmltag='div'> + const replacer2 = (match: any, p1: string, offset: any, string: any) => { + return `<HTMLtag Document={props.Document} htmltag='${p1}'`; + }; + layoutFrame = layoutFrame.replace(/<HTML([a-zA-Z0-9_-]+)/g, replacer2); + + // replace /HTML<tag> with </HTMLdiv> as in: </HTMLdiv> becomes </HTMLtag> + const replacer3 = (match: any, p1: string, offset: any, string: any) => { + return `</HTMLtag`; + }; + layoutFrame = layoutFrame.replace(/<\/HTML([a-zA-Z0-9_-]+)/g, replacer3); + + // add onClick function to props + const splits = layoutFrame.split("onClick="); + let onClick: Opt<ScriptField>; + if (splits.length > 1) { + const code = XRegExp.matchRecursive(splits[1], "{", "}", "", { valueNames: ["between", "left", "match", "right", "between"] }); + layoutFrame = splits[0] + " onClick={props.onClick} " + splits[1].substring(code[1].end + 1); + onClick = ScriptField.MakeScript(code[1].value, { this: Doc.name, self: Doc.name }); + } + + const bindings = this.CreateBindings(onClick); + // layoutFrame = splits.length > 1 ? splits[0] + splits[1].replace(/{([^{}]|(?R))*}/, replacer4) : ""; // might have been more elegant if javascript supported recursive patterns + + return (this.props.renderDepth > 12 || !layoutFrame || !this.layoutDoc) ? (null) : this.props.forceLayout === "FormattedTextBox" && this.props.forceFieldKey ? - <FormattedTextBox {...this.CreateBindings().props} fieldKey={this.props.forceFieldKey} /> + <FormattedTextBox {...bindings.props} fieldKey={this.props.forceFieldKey} /> : <ObserverJsxParser + key={42} blacklistedAttrs={[]} + renderInWrapper={false} components={{ FormattedTextBox, ImageBox, DirectoryImportBox, FontIconBox, LabelBox, SliderBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, PresBox, YoutubeBox, PresElementBox, QueryBox, ColorBox, DashWebRTCVideo, LinkAnchorBox, InkingStroke, DocHolderBox, LinkBox, ScriptingBox, - RecommendationsBox, ScreenshotBox, ComparisonBox + RecommendationsBox, ScreenshotBox, HTMLtag, ComparisonBox }} - bindings={this.CreateBindings()} - jsx={this.layout} + bindings={bindings} + jsx={layoutFrame} showWarnings={true} onError={(test: any) => { console.log(test); }} diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index fc9ee1201..c9a745809 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -37,16 +37,10 @@ .documentView-linkAnchorBoxAnchor { display:flex; overflow: hidden; - } - .documentView-linkAnchorBoxWrapper { - pointer-events: none; - position: absolute; - transform-origin: top left; - width: 100%; - height: 100%; - top:0; - left:0; - z-index: 1; + + .documentView-node { + width:10px !important; + } } .documentView-lock { @@ -81,7 +75,6 @@ display: inline-block; width: 100%; height: 100%; - pointer-events: none; .documentView-styleContentWrapper { width: 100%; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 3784c5c9c..f05366a15 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,21 +1,24 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import * as fa from '@fortawesome/free-solid-svg-icons'; -import { action, computed, runInAction, trace, observable } from "mobx"; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; import * as rp from "request-promise"; -import { Doc, DocListCast, Opt, WidthSym, HeightSym } from "../../../new_fields/Doc"; +import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../new_fields/Doc"; import { Document, PositionDocument } from '../../../new_fields/documentSchemas'; import { Id } from '../../../new_fields/FieldSymbols'; import { InkTool } from '../../../new_fields/InkField'; import { RichTextField } from '../../../new_fields/RichTextField'; import { listSpec } from "../../../new_fields/Schema"; +import { SchemaHeaderField } from '../../../new_fields/SchemaHeaderField'; import { ScriptField } from '../../../new_fields/ScriptField'; import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types"; import { AudioField, ImageField, PdfField, VideoField } from '../../../new_fields/URLField'; import { TraceMobx } from '../../../new_fields/util'; import { GestureUtils } from '../../../pen-gestures/GestureUtils'; -import { emptyFunction, returnOne, returnTransparent, returnTrue, Utils, OmitKeys, returnZero } from "../../../Utils"; +import { emptyFunction, OmitKeys, returnOne, returnTransparent, Utils } from "../../../Utils"; import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; +import { ClientRecommender } from '../../ClientRecommender'; import { DocServer } from "../../DocServer"; import { Docs, DocumentOptions, DocUtils } from "../../documents/Documents"; import { DocumentType } from '../../documents/DocumentTypes'; @@ -24,6 +27,7 @@ import { DocumentManager } from "../../util/DocumentManager"; import { DragManager, dropActionType } from "../../util/DragManager"; import { InteractionUtils } from '../../util/InteractionUtils'; import { Scripting } from '../../util/Scripting'; +import { SearchUtil } from '../../util/SearchUtil'; import { SelectionManager } from "../../util/SelectionManager"; import SharingManager from '../../util/SharingManager'; import { Transform } from "../../util/Transform"; @@ -35,19 +39,11 @@ import { ContextMenuProps } from '../ContextMenuItem'; import { DocComponent } from "../DocComponent"; import { EditableView } from '../EditableView'; import { InkingControl } from '../InkingControl'; -import { OverlayView } from '../OverlayView'; -import { ScriptBox } from '../ScriptBox'; -import { ScriptingRepl } from '../ScriptingRepl'; +import { KeyphraseQueryView } from '../KeyphraseQueryView'; import { DocumentContentsView } from "./DocumentContentsView"; import "./DocumentView.scss"; -import React = require("react"); -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { SchemaHeaderField } from '../../../new_fields/SchemaHeaderField'; -import { ClientRecommender } from '../../ClientRecommender'; -import { SearchUtil } from '../../util/SearchUtil'; import { RadialMenu } from './RadialMenu'; -import { KeyphraseQueryView } from '../KeyphraseQueryView'; -import { undo } from 'prosemirror-history'; +import React = require("react"); library.add(fa.faEdit, fa.faTrash, fa.faShare, fa.faDownload, fa.faExpandArrowsAlt, fa.faCompressArrowsAlt, fa.faLayerGroup, fa.faExternalLinkAlt, fa.faAlignCenter, fa.faCaretSquareRight, fa.faSquare, fa.faConciergeBell, fa.faWindowRestore, fa.faFolder, fa.faMapPin, fa.faLink, fa.faFingerprint, fa.faCrosshairs, fa.faDesktop, fa.faUnlock, fa.faLock, fa.faLaptopCode, fa.faMale, @@ -65,6 +61,7 @@ export interface DocumentViewProps { LayoutDoc?: () => Opt<Doc>; LibraryPath: Doc[]; fitToBox?: boolean; + contextMenuItems?: () => { script: ScriptField, label: string }[]; rootSelected: (outsideReaction?: boolean) => boolean; // whether the root of a template has been selected onClick?: ScriptField; onPointerDown?: ScriptField; @@ -80,6 +77,7 @@ export interface DocumentViewProps { ContentScaling: () => number; PanelWidth: () => number; PanelHeight: () => number; + pointerEvents?: boolean; focus: (doc: Doc, willZoom: boolean, scale?: number, afterFocus?: DocFocusFunc) => void; parentActive: (outsideReaction: boolean) => boolean; whenActiveChanged: (isActive: boolean) => void; @@ -289,32 +287,40 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu let preventDefault = true; this.props.Document.isBackground === undefined && this.props.bringToFront(this.props.Document); if (this._doubleTap && this.props.renderDepth && !this.onClickHandler?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click - const fullScreenAlias = Doc.MakeAlias(this.props.Document); - if (StrCast(fullScreenAlias.layoutKey) !== "layout_fullScreen" && fullScreenAlias.layout_fullScreen) { - fullScreenAlias.layoutKey = "layout_fullScreen"; + if (!(e.nativeEvent as any).formattedHandled) { + const fullScreenAlias = Doc.MakeAlias(this.props.Document); + if (StrCast(fullScreenAlias.layoutKey) !== "layout_fullScreen" && fullScreenAlias.layout_fullScreen) { + fullScreenAlias.layoutKey = "layout_fullScreen"; + } + UndoManager.RunInBatch(() => this.props.addDocTab(fullScreenAlias, "inTab"), "double tap"); + SelectionManager.DeselectAll(); + Doc.UnBrushDoc(this.props.Document); } - UndoManager.RunInBatch(() => this.props.addDocTab(fullScreenAlias, "inTab"), "double tap"); - SelectionManager.DeselectAll(); - Doc.UnBrushDoc(this.props.Document); - } else if (this.onClickHandler?.script) { - SelectionManager.DeselectAll(); + } else if (this.onClickHandler?.script && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes("ScriptingBox")) { // bcz: hack? don't execute script if you're clicking on a scripting box itself + //SelectionManager.DeselectAll(); const func = () => this.onClickHandler.script.run({ this: this.layoutDoc, self: this.rootDoc, thisContainer: this.props.ContainingCollectionDoc, shiftKey: e.shiftKey }, console.log); - if (this.props.Document !== Doc.UserDoc().undoBtn && this.props.Document !== Doc.UserDoc().redoBtn) { + if (this.props.Document !== Doc.UserDoc()["dockedBtn-undo"] && this.props.Document !== Doc.UserDoc()["dockedBtn-redo"]) { UndoManager.RunInBatch(func, "on click"); } else func(); - } else if (this.Document["onClick-rawScript"]) { - UndoManager.RunInBatch(() => ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", e.clientX, e.clientY), "on button click"); + } else if (this.Document["onClick-rawScript"] && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes("ScriptingBox")) {// bcz: hack? don't edit a script if you're clicking on a scripting box itself + UndoManager.RunInBatch(() => DocumentView.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick"); + //ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", e.clientX, e.clientY), "on button click"); } else if (this.Document.isLinkButton) { DocListCast(this.props.Document.links).length && this.followLinkClick(e.altKey, e.ctrlKey, e.shiftKey); } else { - if ((this.props.Document.onDragStart || (this.props.Document.rootDocument && this.props.Document.isTemplateForField)) && !(e.ctrlKey || e.button > 0)) { // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTEmplaetForField implies we're clicking on part of a template instance and we want to select the whole template, not the part + if ((this.props.Document.onDragStart || (this.props.Document.rootDocument)) && !(e.ctrlKey || e.button > 0)) { // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTEmplaetForField implies we're clicking on part of a template instance and we want to select the whole template, not the part stopPropagate = false; // don't stop propagation for field templates -- want the selection to propagate up to the root document of the template } else { - this.props.focus(this.props.Document, false); + if (this.props.Document.type === DocumentType.RTF) { + DocumentView._focusHack = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY) || [0, 0]; + DocumentView._focusHack = [DocumentView._focusHack[0] + NumCast(this.props.Document.x), DocumentView._focusHack[1] + NumCast(this.props.Document.y)]; + + this.props.focus(this.props.Document, false); + } SelectionManager.SelectDoc(this, e.ctrlKey); } preventDefault = false; @@ -323,6 +329,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu preventDefault && e.preventDefault(); } } + static _focusHack: number[] = []; // bcz :this will get fixed... // follows a link - if the target is on screen, it highlights/pans to it. // if the target isn't onscreen, then it will open up the target in a tab, on the right, or in place @@ -472,15 +479,17 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } return; } - if (!e.nativeEvent.cancelBubble || this.onClickHandler || this.Document.onDragStart) { - this._downX = e.clientX; - this._downY = e.clientY; + this._downX = e.clientX; + this._downY = e.clientY; + if ((!e.nativeEvent.cancelBubble || this.onClickHandler || this.Document.onDragStart) && + // if this is part of a template, let the event go up to the tempalte root unless right/ctrl clicking + !((this.props.Document.rootDocument) && !(e.ctrlKey || e.button > 0))) { if ((this.active || this.Document.onDragStart || this.onClickHandler) && !e.ctrlKey && (e.button === 0 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) && - !this.Document.lockedPosition && !this.Document.inOverlay) { e.stopPropagation(); // events stop at the lowest document that is active. if right dragging, we let it go through though to allow for context menu clicks. PointerMove callbacks should remove themselves if the move event gets stopPropagated by a lower-level handler (e.g, marquee drag); + } document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); @@ -539,7 +548,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu deleteClicked = (): void => { SelectionManager.DeselectAll(); this.props.removeDocument?.(this.props.Document); } // applies a custom template to a document. the template is identified by it's short name (e.g, slideView not layout_slideView) - static makeCustomViewClicked = (doc: Doc, creator: (documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc, templateSignature: string = "custom", docLayoutTemplate?: Doc) => { + static makeCustomViewClicked = (doc: Doc, creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, templateSignature: string = "custom", docLayoutTemplate?: Doc) => { const batch = UndoManager.StartBatch("makeCustomViewClicked"); runInAction(() => { doc.layoutKey = "layout_" + templateSignature; @@ -551,17 +560,18 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } static findTemplate(templateName: string, type: string, signature: string) { let docLayoutTemplate: Opt<Doc>; - const iconViews = DocListCast(Cast(Doc.UserDoc().iconViews, Doc, null)?.data); - const templBtns = DocListCast(Cast(Doc.UserDoc().templateButtons, Doc, null)?.data); + 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().noteTypes, Doc, null)?.data); - const allTemplates = iconViews.concat(templBtns).concat(noteTypes).map(btnDoc => (btnDoc.dragFactory as Doc) || btnDoc).filter(doc => doc.isTemplateDoc); + const clickFuncs = DocListCast(Cast(Doc.UserDoc().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 (<typeName>_<templateName>). otherwise, fallback to a general match on <templateName> - !docLayoutTemplate && allTemplates.forEach(tempDoc => StrCast(tempDoc.title) === type + "_" + templateName && (docLayoutTemplate = tempDoc)); + !docLayoutTemplate && allTemplates.forEach(tempDoc => StrCast(tempDoc.title) === templateName + "_" + type && (docLayoutTemplate = tempDoc)); !docLayoutTemplate && allTemplates.forEach(tempDoc => StrCast(tempDoc.title) === templateName && (docLayoutTemplate = tempDoc)); return docLayoutTemplate; } - static createCustomView = (doc: Doc, creator: (documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc, templateSignature: string = "custom", docLayoutTemplate?: Doc) => { + static createCustomView = (doc: Doc, creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, templateSignature: string = "custom", docLayoutTemplate?: Doc) => { const templateName = templateSignature.replace(/\(.*\)/, ""); docLayoutTemplate = docLayoutTemplate || DocumentView.findTemplate(templateName, StrCast(doc.type), templateSignature); @@ -582,10 +592,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } else if (doc.data instanceof ImageField) { fieldTemplate = Docs.Create.ImageDocument("http://www.cs.brown.edu", options); } - const docTemplate = docLayoutTemplate || creator(fieldTemplate ? [fieldTemplate] : [], { title: customName + "(" + doc.title + ")", isTemplateDoc: true, _width: _width + 20, _height: Math.max(100, _height + 45) }); + const docTemplate = docLayoutTemplate || creator?.(fieldTemplate ? [fieldTemplate] : [], { title: customName + "(" + doc.title + ")", isTemplateDoc: true, _width: _width + 20, _height: Math.max(100, _height + 45) }); - fieldTemplate && Doc.MakeMetadataFieldTemplate(fieldTemplate, Doc.GetProto(docTemplate)); - Doc.ApplyTemplateTo(docTemplate, doc, customName, undefined); + fieldTemplate && Doc.MakeMetadataFieldTemplate(fieldTemplate, docTemplate ? Doc.GetProto(docTemplate) : docTemplate); + docTemplate && Doc.ApplyTemplateTo(docTemplate, doc, customName, undefined); } @undoBatch @@ -708,41 +718,36 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const cm = ContextMenu.Instance; const templateDoc = Cast(this.props.Document[StrCast(this.props.Document.layoutKey)], Doc, null); - const existing = cm.findByDescription("Layout..."); - const layoutItems: ContextMenuProps[] = existing && "subitems" in existing ? existing.subitems : []; - layoutItems.push({ description: this.Document.isBackground ? "As Foreground" : "As Background", event: (e) => this.toggleBackground(false), icon: this.Document.lockedPosition ? "unlock" : "lock" }); - layoutItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc), icon: "concierge-bell" }); + const customScripts = Cast(this.props.Document.contextMenuScripts, listSpec(ScriptField), []); + Cast(this.props.Document.contextMenuLabels, listSpec("string"), []).forEach((label, i) => + cm.addItem({ description: label, event: () => customScripts[i]?.script.run({ this: this.layoutDoc, self: this.rootDoc }), icon: "sticky-note" })); + this.props.contextMenuItems?.().forEach(item => + cm.addItem({ description: item.label, event: () => item.script.script.run({ this: this.layoutDoc, self: this.rootDoc }), icon: "sticky-note" })); + + const existing = cm.findByDescription("Options..."); + const layoutItems: ContextMenuProps[] = existing && "subitems" in existing ? existing.subitems : []; layoutItems.push({ description: `${this.Document._chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.Document._chromeStatus = (this.Document._chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" }); layoutItems.push({ description: `${this.Document._autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight, icon: "plus" }); - layoutItems.push({ description: !this.Document._nativeWidth || !this.Document._nativeHeight ? "Freeze" : "Unfreeze", event: this.toggleNativeDimensions, icon: "snowflake" }); layoutItems.push({ description: this.Document.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.Document.lockedPosition) ? "unlock" : "lock" }); layoutItems.push({ description: this.Document.lockedTransform ? "Unlock Transform" : "Lock Transform", event: this.toggleLockTransform, icon: BoolCast(this.Document.lockedTransform) ? "unlock" : "lock" }); - layoutItems.push({ description: "Center View", event: () => this.props.focus(this.props.Document, false), icon: "crosshairs" }); - layoutItems.push({ description: "Zoom to Document", event: () => this.props.focus(this.props.Document, true), icon: "search" }); - !existing && cm.addItem({ description: "Layout...", subitems: layoutItems, icon: "compass" }); + !existing && cm.addItem({ description: "Options...", subitems: layoutItems, icon: "compass" }); - const open = ContextMenu.Instance.findByDescription("Open..."); + const open = cm.findByDescription("Open New Perspective..."); const openItems: ContextMenuProps[] = open && "subitems" in open ? open.subitems : []; - openItems.push({ description: "Open Full Screen", event: () => CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(this, this.props.LibraryPath), icon: "desktop" }); - openItems.push({ description: "Open Tab ", event: () => this.props.addDocTab(this.props.Document, "inTab", this.props.LibraryPath), icon: "folder" }); - openItems.push({ description: "Open Right ", event: () => this.props.addDocTab(this.props.Document, "onRight", this.props.LibraryPath), icon: "caret-square-right" }); - openItems.push({ description: "Open Alias Tab ", event: () => this.props.addDocTab(Doc.MakeAlias(this.props.Document), "inTab"), icon: "folder" }); - openItems.push({ description: "Open Alias Right", event: () => this.props.addDocTab(Doc.MakeAlias(this.props.Document), "onRight"), icon: "caret-square-right" }); openItems.push({ description: "Open Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "onRight"), icon: "layer-group" }); templateDoc && openItems.push({ description: "Open Template ", event: () => this.props.addDocTab(templateDoc, "onRight"), icon: "eye" }); - openItems.push({ description: "Open Repl", icon: "laptop-code", event: () => OverlayView.Instance.addWindow(<ScriptingRepl />, { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" }) }); - !open && cm.addItem({ description: "Open...", subitems: openItems, icon: "external-link-alt" }); + !open && cm.addItem({ description: "Open New Perspective...", subitems: openItems, icon: "external-link-alt" }); const existingOnClick = cm.findByDescription("OnClick..."); const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; onClicks.push({ description: "Enter Portal", event: this.makeIntoPortal, icon: "window-restore" }); - onClicks.push({ description: "Toggle Detail", event: () => this.Document.onClick = ScriptField.MakeScript(`toggleDetail(this, "${this.props.Document.layoutKey}")`), icon: "window-restore" }); + onClicks.push({ description: "Toggle Detail", event: () => this.Document.onClick = ScriptField.MakeScript(`toggleDetail(self, "${this.props.Document.layoutKey}")`), icon: "window-restore" }); onClicks.push({ description: this.Document.ignoreClick ? "Select" : "Do Nothing", event: () => this.Document.ignoreClick = !this.Document.ignoreClick, icon: this.Document.ignoreClick ? "unlock" : "lock" }); onClicks.push({ description: this.Document.isLinkButton ? "Remove Follow Behavior" : "Follow Link in Place", event: this.toggleFollowInPlace, icon: "concierge-bell" }); onClicks.push({ description: this.Document.isLinkButton || this.Document.onClick ? "Remove Click Behavior" : "Follow Link", event: this.toggleLinkButtonBehavior, icon: "concierge-bell" }); - onClicks.push({ description: "Edit onClick Script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", obj.x, obj.y) }); + onClicks.push({ description: "Edit onClick Script", event: () => UndoManager.RunInBatch(() => DocumentView.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick"), icon: "edit" }); !existingOnClick && cm.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" }); const funcs: ContextMenuProps[] = []; @@ -755,6 +760,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const more = cm.findByDescription("More..."); const moreItems: ContextMenuProps[] = more && "subitems" in more ? more.subitems : []; + moreItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc), icon: "concierge-bell" }); + moreItems.push({ description: !this.Document._nativeWidth || !this.Document._nativeHeight ? "Freeze" : "Unfreeze", event: this.toggleNativeDimensions, icon: "snowflake" }); if (!ClientUtils.RELEASE) { // let copies: ContextMenuProps[] = []; @@ -807,11 +814,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu icon: "brain" }); - cm.addItem({ description: "Recommender System", subitems: recommender_subitems, icon: "brain" }); - - - moreItems.push({ description: "Publish", event: () => DocUtils.Publish(this.props.Document, this.Document.title || "", this.props.addDocument, this.props.removeDocument), icon: "file" }); moreItems.push({ description: "Delete", event: this.deleteClicked, icon: "trash" }); + moreItems.push({ description: "Recommender System", subitems: recommender_subitems, icon: "brain" }); + moreItems.push({ description: "Publish", event: () => DocUtils.Publish(this.props.Document, this.Document.title || "", this.props.addDocument, this.props.removeDocument), icon: "file" }); moreItems.push({ description: "Undo Debug Test", event: () => UndoManager.TraceOpenBatches(), icon: "exclamation" }); !more && cm.addItem({ description: "More...", subitems: moreItems, icon: "hand-point-right" }); runInAction(() => { @@ -854,7 +859,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu SelectionManager.SelectDoc(this, false); } }); - const path = this.props.LibraryPath.reduce((p: string, d: Doc) => p + "/" + (Doc.AreProtosEqual(d, (Doc.UserDoc().LibraryBtn as Doc).sourcePanel as Doc) ? "" : d.title), ""); + const path = this.props.LibraryPath.reduce((p: string, d: Doc) => p + "/" + (Doc.AreProtosEqual(d, (Doc.UserDoc()["tabs-button-library"] as Doc).sourcePanel as Doc) ? "" : d.title), ""); cm.addItem({ description: `path: ${path}`, event: () => { this.props.LibraryPath.map(lp => Doc.GetProto(lp).treeViewOpen = lp.treeViewOpen = true); @@ -978,7 +983,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu return typeof fallback === "string" ? fallback : "layout"; } rootSelected = (outsideReaction?: boolean) => { - return this.isSelected(outsideReaction) || (this.props.Document.forceActive && this.props.rootSelected?.(outsideReaction) ? true : false); + return this.isSelected(outsideReaction) || (this.props.Document.rootDocument && this.props.rootSelected?.(outsideReaction)) || false; } childScaling = () => (this.layoutDoc._fitWidth ? this.props.PanelWidth() / this.nativeWidth : this.props.ContentScaling()); panelWidth = () => this.props.PanelWidth(); @@ -986,38 +991,42 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu screenToLocalTransform = () => this.props.ScreenToLocalTransform(); @computed get contents() { TraceMobx(); - return (<DocumentContentsView ContainingCollectionView={this.props.ContainingCollectionView} - ContainingCollectionDoc={this.props.ContainingCollectionDoc} - NativeWidth={this.NativeWidth} - NativeHeight={this.NativeHeight} - Document={this.props.Document} - DataDoc={this.props.DataDoc} - LayoutDoc={this.props.LayoutDoc} - makeLink={this.makeLink} - rootSelected={this.rootSelected} - dontRegisterView={this.props.dontRegisterView} - fitToBox={this.props.fitToBox} - LibraryPath={this.props.LibraryPath} - addDocument={this.props.addDocument} - removeDocument={this.props.removeDocument} - moveDocument={this.props.moveDocument} - ScreenToLocalTransform={this.screenToLocalTransform} - renderDepth={this.props.renderDepth} - PanelWidth={this.panelWidth} - PanelHeight={this.panelHeight} - focus={this.props.focus} - parentActive={this.props.parentActive} - whenActiveChanged={this.props.whenActiveChanged} - bringToFront={this.props.bringToFront} - addDocTab={this.props.addDocTab} - pinToPres={this.props.pinToPres} - backgroundColor={this.props.backgroundColor} - ContentScaling={this.childScaling} - ChromeHeight={this.chromeHeight} - isSelected={this.isSelected} - select={this.select} - onClick={this.onClickHandler} - layoutKey={this.finalLayoutKey} />); + return (<> + <DocumentContentsView key={1} ContainingCollectionView={this.props.ContainingCollectionView} + ContainingCollectionDoc={this.props.ContainingCollectionDoc} + NativeWidth={this.NativeWidth} + NativeHeight={this.NativeHeight} + Document={this.props.Document} + DataDoc={this.props.DataDoc} + LayoutDoc={this.props.LayoutDoc} + makeLink={this.makeLink} + rootSelected={this.rootSelected} + dontRegisterView={this.props.dontRegisterView} + fitToBox={this.props.fitToBox} + LibraryPath={this.props.LibraryPath} + addDocument={this.props.addDocument} + removeDocument={this.props.removeDocument} + moveDocument={this.props.moveDocument} + ScreenToLocalTransform={this.screenToLocalTransform} + renderDepth={this.props.renderDepth} + PanelWidth={this.panelWidth} + PanelHeight={this.panelHeight} + focus={this.props.focus} + parentActive={this.props.parentActive} + whenActiveChanged={this.props.whenActiveChanged} + bringToFront={this.props.bringToFront} + addDocTab={this.props.addDocTab} + pinToPres={this.props.pinToPres} + backgroundColor={this.props.backgroundColor} + ContentScaling={this.childScaling} + ChromeHeight={this.chromeHeight} + isSelected={this.isSelected} + select={this.select} + onClick={this.onClickHandler} + layoutKey={this.finalLayoutKey} /> + {this.anchors} + </> + ); } linkEndpoint = (linkDoc: Doc) => Doc.LinkEndpoint(linkDoc, this.props.Document); @@ -1041,20 +1050,19 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @computed get anchors() { TraceMobx(); return this.layoutDoc.presBox ? (null) : DocListCast(this.Document.links).filter(d => !d.hidden && this.isNonTemporalLink).map((d, i) => - <div className="documentView-linkAnchorBoxWrapper" key={d[Id]}> - <DocumentView {...this.props} - Document={d} - ContainingCollectionView={this.props.ContainingCollectionView} - ContainingCollectionDoc={this.props.Document} // bcz: hack this.props.Document is not a collection Need a better prop for passing the containing document to the LinkAnchorBox - PanelWidth={this.anchorPanelWidth} - PanelHeight={this.anchorPanelHeight} - layoutKey={this.linkEndpoint(d)} - ContentScaling={returnOne} - backgroundColor={returnTransparent} - removeDocument={this.hideLinkAnchor} - LayoutDoc={undefined} - /> - </div>); + <DocumentView {...this.props} key={i + 1} + Document={d} + ContainingCollectionView={this.props.ContainingCollectionView} + ContainingCollectionDoc={this.props.Document} // bcz: hack this.props.Document is not a collection Need a better prop for passing the containing document to the LinkAnchorBox + PanelWidth={this.anchorPanelWidth} + PanelHeight={this.anchorPanelHeight} + layoutKey={this.linkEndpoint(d)} + ContentScaling={returnOne} + backgroundColor={returnTransparent} + removeDocument={this.hideLinkAnchor} + pointerEvents={false} + LayoutDoc={undefined} + />); } @computed get innards() { TraceMobx(); @@ -1084,7 +1092,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const titleView = (!showTitle ? (null) : <div className={`documentView-titleWrapper${showTitleHover ? "-hover" : ""}`} style={{ position: showTextTitle ? "relative" : "absolute", - pointerEvents: SelectionManager.GetIsDragging() || this.onClickHandler || this.Document.ignoreClick ? "none" : "all", + pointerEvents: SelectionManager.GetIsDragging() || this.onClickHandler || this.Document.ignoreClick ? "none" : undefined, }}> <EditableView ref={this._titleRef} contents={(this.props.DataDoc || this.props.Document)[showTitle]?.toString()} @@ -1093,22 +1101,18 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu SetValue={undoBatch((value: string) => (Doc.GetProto(this.props.DataDoc || this.props.Document)[showTitle] = value) ? true : true)} /> </div>); - return <> - {this.anchors} - {!showTitle && !showCaption ? - this.contents : - <div className="documentView-styleWrapper" > - <div className="documentView-styleContentWrapper" style={{ height: showTextTitle ? `calc(100% - ${this.chromeHeight()}px)` : "100%", top: showTextTitle ? this.chromeHeight() : undefined }}> - {this.contents} - </div> - {titleView} - {captionView} + return !showTitle && !showCaption ? + this.contents : + <div className="documentView-styleWrapper" > + <div className="documentView-styleContentWrapper" style={{ height: showTextTitle ? `calc(100% - ${this.chromeHeight()}px)` : "100%", top: showTextTitle ? this.chromeHeight() : undefined }}> + {this.contents} </div> - } - </>; + {titleView} + {captionView} + </div>; } @computed get ignorePointerEvents() { - return (this.Document.isBackground && !this.isSelected() && !SelectionManager.GetIsDragging()) || this.props.layoutKey?.includes("layout_key") || (this.Document.type === DocumentType.INK && InkingControl.Instance.selectedTool !== InkTool.None); + return this.props.pointerEvents === false || (this.Document.isBackground && !this.isSelected() && !SelectionManager.GetIsDragging()) || (this.Document.type === DocumentType.INK && InkingControl.Instance.selectedTool !== InkTool.None); } @observable _animate = 0; @@ -1146,7 +1150,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu transformOrigin: this._animate ? "center center" : undefined, transform: this._animate ? `scale(${this._animate})` : undefined, transition: !this._animate ? StrCast(this.Document.transition) : this._animate < 1 ? "transform 0.5s ease-in" : "transform 0.5s ease-out", - pointerEvents: this.ignorePointerEvents ? "none" : "all", + pointerEvents: this.ignorePointerEvents ? "none" : undefined, color: StrCast(this.layoutDoc.color, "inherit"), outline: highlighting && !borderRounding ? `${highlightColors[fullDegree]} ${highlightStyles[fullDegree]} ${localScale}px` : "solid 0px", border: highlighting && borderRounding ? `${highlightStyles[fullDegree]} ${highlightColors[fullDegree]} ${localScale}px` : undefined, @@ -1159,7 +1163,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu <div className="documentView-contentBlocker" /> </> : this.innards} - {this.Document.isBackground !== undefined || this.isSelected(false) ? <div className="documentView-lock" onClick={() => this.toggleBackground(true)}> <FontAwesomeIcon icon={this.Document.isBackground ? "unlock" : "lock"} size="lg" /> </div> : (null)} + {(this.Document.isBackground !== undefined || this.isSelected(false)) && this.props.renderDepth > 0 && this.props.PanelWidth() > 0 ? + <div className="documentView-lock" onClick={() => this.toggleBackground(true)}> <FontAwesomeIcon icon={this.Document.isBackground ? "unlock" : "lock"} size="lg" /> </div> + : (null)} </div>; { this._showKPQuery ? <KeyphraseQueryView keyphrases={this._queries}></KeyphraseQueryView> : undefined; } } diff --git a/src/client/views/nodes/FontIconBox.scss b/src/client/views/nodes/FontIconBox.scss index f0fe7a54e..68b00a5be 100644 --- a/src/client/views/nodes/FontIconBox.scss +++ b/src/client/views/nodes/FontIconBox.scss @@ -8,6 +8,18 @@ border-radius: 100%; transform-origin: top left; + .fontIconBox-label { + background: gray; + color:white; + margin-left: -10px; + border-radius: 8px; + width:100%; + position: absolute; + text-align: center; + font-size: 8px; + margin-top:4px; + } + svg { width: 95% !important; height: 95%; diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx index 9329cf210..c6ea6a882 100644 --- a/src/client/views/nodes/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox.tsx @@ -57,6 +57,7 @@ export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>( boxShadow: this.props.Document.ischecked ? `4px 4px 12px black` : undefined }}> <FontAwesomeIcon className="fontIconBox-icon" icon={this.dataDoc.icon as any} color={this._foregroundColor} size="sm" /> + {!this.rootDoc.label ? (null) : <div className="fontIconBox-label"> {StrCast(this.rootDoc.label).substring(0, 5)} </div>} </button>; } }
\ No newline at end of file diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss index 7d40b3149..7a05ec3a3 100644 --- a/src/client/views/nodes/FormattedTextBox.scss +++ b/src/client/views/nodes/FormattedTextBox.scss @@ -25,7 +25,6 @@ overflow-x: hidden; color: initial; height: 100%; - pointer-events: all; max-height: 100%; display: flex; flex-direction: row; diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index d641dc791..fd7462a10 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -13,7 +13,7 @@ import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from " import { ReplaceStep } from 'prosemirror-transform'; import { EditorView } from "prosemirror-view"; import { DateField } from '../../../new_fields/DateField'; -import { DataSym, Doc, DocListCastAsync, Field, HeightSym, Opt, WidthSym } from "../../../new_fields/Doc"; +import { DataSym, Doc, DocListCastAsync, Field, HeightSym, Opt, WidthSym, DocListCast } from "../../../new_fields/Doc"; import { documentSchema } from '../../../new_fields/documentSchemas'; import { Id } from '../../../new_fields/FieldSymbols'; import { InkTool } from '../../../new_fields/InkField'; @@ -48,6 +48,7 @@ import { FormattedTextBoxComment, formattedTextBoxCommentPlugin } from './Format import React = require("react"); import { PrefetchProxy } from '../../../new_fields/Proxy'; import { makeTemplate } from '../../util/DropConverter'; +import { DocumentView } from './DocumentView'; library.add(faEdit); library.add(faSmile, faTextHeight, faUpload); @@ -396,6 +397,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp return Cast(Doc.UserDoc().defaultTextLayout, Doc, null) || StrCast(Doc.UserDoc().defaultTextLayout, null); } specificContextMenu = (e: React.MouseEvent): void => { + const cm = ContextMenu.Instance; + const funcs: ContextMenuProps[] = []; this.props.Document.isTemplateDoc && funcs.push({ description: "Make Default Layout", event: async () => Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.props.Document), icon: "eye" }); funcs.push({ description: "Reset Default Layout", event: () => Doc.UserDoc().defaultTextLayout = undefined, icon: "eye" }); @@ -407,10 +410,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp }); funcs.push({ description: "Toggle Single Line", event: () => this.props.Document._singleLine = !this.props.Document._singleLine, icon: "expand-arrows-alt" }); funcs.push({ description: "Toggle Sidebar", event: () => this.props.Document._showSidebar = !this.props.Document._showSidebar, icon: "expand-arrows-alt" }); - funcs.push({ description: "Toggle Audio", event: () => this.props.Document._showAudio = !this.props.Document._showAudio, icon: "expand-arrows-alt" }); + funcs.push({ description: "Toggle Dictation Icon", event: () => this.props.Document._showAudio = !this.props.Document._showAudio, icon: "expand-arrows-alt" }); funcs.push({ description: "Toggle Menubar", event: () => this.toggleMenubar(), icon: "expand-arrows-alt" }); + + const highlighting: ContextMenuProps[] = []; ["My Text", "Text from Others", "Todo Items", "Important Items", "Ignore Items", "Disagree Items", "By Recent Minute", "By Recent Hour"].forEach(option => - funcs.push({ + highlighting.push({ description: (FormattedTextBox._highlights.indexOf(option) === -1 ? "Highlight " : "Unhighlight ") + option, event: () => { e.stopPropagation(); if (FormattedTextBox._highlights.indexOf(option) === -1) { @@ -421,8 +426,38 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this.updateHighlights(); }, icon: "expand-arrows-alt" })); + funcs.push({ description: "highlighting...", subitems: highlighting, icon: "hand-point-right" }); + + ContextMenu.Instance.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); + + const change = cm.findByDescription("Change Perspective..."); + const changeItems: ContextMenuProps[] = change && "subitems" in change ? change.subitems : []; + + const noteTypesDoc = Cast(Doc.UserDoc().noteTypes, Doc, null); + const noteTypes = DocListCast(noteTypesDoc?.data); + noteTypes.forEach(note => { + changeItems.push({ + description: StrCast(note.title), event: () => { + Doc.setNativeView(this.props.Document); + DocumentView.makeCustomViewClicked(this.rootDoc, Docs.Create.TreeDocument, StrCast(note.title), note); + }, icon: "eye" + }); + }); + changeItems.push({ description: "FreeForm", event: () => DocumentView.makeCustomViewClicked(this.rootDoc, Docs.Create.FreeformDocument, "freeform"), icon: "eye" }); + !change && cm.addItem({ description: "Change Perspective...", subitems: changeItems, icon: "external-link-alt" }); + + const open = cm.findByDescription("Open New Perspective..."); + const openItems: ContextMenuProps[] = open && "subitems" in open ? open.subitems : []; + + openItems.push({ + description: "FreeForm", event: () => { + const alias = Doc.MakeAlias(this.rootDoc); + DocumentView.makeCustomViewClicked(alias, Docs.Create.FreeformDocument, "freeform"); + this.props.addDocTab(alias, "onRight"); + }, icon: "eye" + }); + !open && cm.addItem({ description: "Open New Perspective...", subitems: openItems, icon: "external-link-alt" }); - ContextMenu.Instance.addItem({ description: "Text Funcs...", subitems: funcs, icon: "asterisk" }); } recordDictation = () => { @@ -596,7 +631,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this.setupEditor(this.config, this.props.fieldKey); - this._searchReactionDisposer = reaction(() => this.layoutDoc.searchMatch, + this._searchReactionDisposer = reaction(() => this.rootDoc.searchMatch, search => search ? this.highlightSearchTerms([Doc.SearchQuery()]) : this.unhighlightSearchTerms(), { fireImmediately: true }); @@ -821,13 +856,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this._editorView = new EditorView(this.ProseRef, { state: rtfField?.Data ? EditorState.fromJSON(config, JSON.parse(rtfField.Data)) : EditorState.create(config), handleScrollToSelection: (editorView) => { - const ref = editorView.domAtPos(editorView.state.selection.from); - let refNode = ref.node as any; - while (refNode && !("getBoundingClientRect" in refNode)) refNode = refNode.parentElement; - const r1 = refNode?.getBoundingClientRect(); - const r3 = self._ref.current!.getBoundingClientRect(); - if (r1.top < r3.top || r1.top > r3.bottom) { - r1 && (self._scrollRef.current!.scrollTop += (r1.top - r3.top) * self.props.ScreenToLocalTransform().Scale); + const docPos = editorView.coordsAtPos(editorView.state.selection.from); + const viewRect = self._ref.current!.getBoundingClientRect(); + if (docPos.top < viewRect.top || docPos.top > viewRect.bottom) { + docPos && (self._scrollRef.current!.scrollTop += (docPos.top - viewRect.top) * self.props.ScreenToLocalTransform().Scale); } return true; }, @@ -1185,7 +1217,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp background: this.props.hideOnLeave ? "rgba(0,0,0 ,0.4)" : StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"]), opacity: this.props.hideOnLeave ? (this._entered ? 1 : 0.1) : 1, color: this.props.hideOnLeave ? "white" : "inherit", - pointerEvents: interactive ? "none" : "all", + pointerEvents: interactive ? "none" : undefined, fontSize: NumCast(this.layoutDoc.fontSize, 13), fontFamily: StrCast(this.layoutDoc.fontFamily, "Crimson Text"), }} diff --git a/src/client/views/nodes/FormattedTextBoxComment.tsx b/src/client/views/nodes/FormattedTextBoxComment.tsx index 35304033f..41df5b3c1 100644 --- a/src/client/views/nodes/FormattedTextBoxComment.tsx +++ b/src/client/views/nodes/FormattedTextBoxComment.tsx @@ -16,6 +16,7 @@ import React = require("react"); import { Docs } from "../../documents/Documents"; import wiki from "wikijs"; import { DocumentType } from "../../documents/DocumentTypes"; +import { DocumentView } from "./DocumentView"; export let formattedTextBoxCommentPlugin = new Plugin({ view(editorView) { return new FormattedTextBoxComment(editorView); } @@ -85,8 +86,9 @@ export class FormattedTextBoxComment { const textBox = FormattedTextBoxComment.textBox; if (FormattedTextBoxComment.linkDoc && !keep && textBox) { if (FormattedTextBoxComment.linkDoc.type !== DocumentType.LINK) { - textBox.props.addDocTab(FormattedTextBoxComment.linkDoc, e.ctrlKey ? "inTab":"onRight"); + textBox.props.addDocTab(FormattedTextBoxComment.linkDoc, e.ctrlKey ? "inTab" : "onRight"); } else { + DocumentView._focusHack = []; DocumentManager.Instance.FollowLink(FormattedTextBoxComment.linkDoc, textBox.props.Document, (doc: Doc, followLinkLocation: string) => textBox.props.addDocTab(doc, e.ctrlKey ? "inTab" : followLinkLocation)); } diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index 7bbf4a368..49425c2c2 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -1,6 +1,5 @@ .imageBox, .imageBox-dragging { - pointer-events: all; border-radius: inherit; width: 100%; height: 100%; diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index de1640027..cf6a7ba5b 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -180,7 +180,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD //modes.push({ description: "Recommend", event: this.extractText, icon: "brain" }); !existingAnalyze && ContextMenu.Instance.addItem({ description: "Analyzers...", subitems: modes, icon: "hand-point-right" }); - ContextMenu.Instance.addItem({ description: "Image Funcs...", subitems: funcs, icon: "asterisk" }); + ContextMenu.Instance.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); } } @@ -250,25 +250,24 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD resize = (imgPath: string) => { const cachedNativeSize = { - width: NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"]), - height: NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"]) + width: imgPath === this.dataDoc[this.fieldKey + "-path"] ? NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"]) : 0, + height: imgPath === this.dataDoc[this.fieldKey + "-path"] ? NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"]) : 0, }; const docAspect = this.layoutDoc[HeightSym]() / this.layoutDoc[WidthSym](); const cachedAspect = cachedNativeSize.height / cachedNativeSize.width; if (!cachedNativeSize.width || !cachedNativeSize.height || Math.abs(NumCast(this.layoutDoc._width) / NumCast(this.layoutDoc._height) - cachedNativeSize.width / cachedNativeSize.height) > 0.05) { if (!this.layoutDoc.isTemplateDoc || this.dataDoc !== this.layoutDoc) { - requestImageSize(imgPath).then((inquiredSize: any) => { + requestImageSize(imgPath).then(action((inquiredSize: any) => { const rotation = NumCast(this.dataDoc[this.fieldKey + "-rotation"]) % 180; const rotatedNativeSize = rotation === 90 || rotation === 270 ? { height: inquiredSize.width, width: inquiredSize.height } : inquiredSize; const rotatedAspect = rotatedNativeSize.height / rotatedNativeSize.width; - setTimeout(action(() => { - if (this.layoutDoc[WidthSym]() && (!cachedNativeSize.width || !cachedNativeSize.height || Math.abs(1 - docAspect / rotatedAspect) > 0.1)) { - this.layoutDoc._height = this.layoutDoc[WidthSym]() * rotatedAspect; - this.dataDoc[this.fieldKey + "-nativeWidth"] = this.layoutDoc._nativeWidth = rotatedNativeSize.width; - this.dataDoc[this.fieldKey + "-nativeHeight"] = this.layoutDoc._nativeHeight = rotatedNativeSize.height; - } - }), 0); - }).catch((err: any) => console.log(err)); + if (this.layoutDoc[WidthSym]() && (!cachedNativeSize.width || !cachedNativeSize.height || Math.abs(1 - docAspect / rotatedAspect) > 0.1)) { + this.layoutDoc._height = this.layoutDoc[WidthSym]() * rotatedAspect; + this.dataDoc[this.fieldKey + "-nativeWidth"] = this.layoutDoc._nativeWidth = this.layoutDoc._width; + this.dataDoc[this.fieldKey + "-nativeHeight"] = this.layoutDoc._nativeHeight = this.layoutDoc._height; + this.dataDoc[this.fieldKey + "-path"] = imgPath; + } + })).catch(console.log); } else if (Math.abs(1 - docAspect / cachedAspect) > 0.1) { this.layoutDoc._width = this.layoutDoc[WidthSym]() || cachedNativeSize.width; this.layoutDoc._height = this.layoutDoc[WidthSym]() * cachedAspect; @@ -388,17 +387,14 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD const { nativeWidth, nativeHeight } = this.nativeSize; const rotation = NumCast(this.dataDoc[this.fieldKey + "-rotation"]); const aspect = (rotation % 180) ? nativeHeight / nativeWidth : 1; - const pwidth = this.props.PanelWidth(); - const pheight = this.props.PanelHeight(); - const shift = (rotation % 180) ? (pheight - pwidth) / aspect / 2 + (pheight - pwidth) / 2 : 0; - + const shift = (rotation % 180) ? (nativeHeight - nativeWidth) * (1 - 1 / aspect) : 0; this.resize(srcpath); return <div className="imageBox-cont" key={this.layoutDoc[Id]} ref={this.createDropTarget}> <div className="imageBox-fader" > <img key={this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys src={srcpath} - style={{ transform: `translate(0px, ${shift}px) rotate(${rotation}deg) scale(${aspect})` }} + style={{ transform: `scale(${aspect}) translate(0px, ${shift}px) rotate(${rotation}deg)` }} width={nativeWidth} ref={this._imgRef} onError={this.onError} /> @@ -427,17 +423,29 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD </div>; } + // adjust y position to center image in panel aspect is bigger than image aspect. + // bcz :note, this is broken for rotated images + get ycenter() { + const { nativeWidth, nativeHeight } = this.nativeSize; + const rotation = NumCast(this.dataDoc[this.fieldKey + "-rotation"]); + const aspect = (rotation % 180) ? nativeWidth / nativeHeight : nativeHeight / nativeWidth; + return this.props.PanelHeight() / this.props.PanelWidth() > aspect ? + (this.props.PanelHeight() - this.props.PanelWidth() * aspect) / 2 : 0; + } + + screenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(0, -this.ycenter / this.props.ContentScaling()); + contentFunc = () => [this.content]; render() { TraceMobx(); const dragging = !SelectionManager.GetIsDragging() ? "" : "-dragging"; return (<div className={`imageBox${dragging}`} onContextMenu={this.specificContextMenu} style={{ - transform: this.props.PanelWidth() ? undefined : `scale(${this.props.ContentScaling()})`, + transform: this.props.PanelWidth() ? `translate(0px, ${this.ycenter}px)` : `scale(${this.props.ContentScaling()})`, width: this.props.PanelWidth() ? undefined : `${100 / this.props.ContentScaling()}%`, height: this.props.PanelWidth() ? undefined : `${100 / this.props.ContentScaling()}%`, pointerEvents: this.layoutDoc.isBackground ? "none" : undefined, - borderRadius: `${Number(StrCast(this.layoutDoc.borderRounding).replace("px", "")) / this.props.ContentScaling()}px` + borderRadius: `${Number(StrCast(this.layoutDoc.borderRoundisng).replace("px", "")) / this.props.ContentScaling()}px` }} > <CollectionFreeFormView {...this.props} forceScaling={true} @@ -457,7 +465,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD moveDocument={this.moveDocument} addDocument={this.addDocument} CollectionView={undefined} - ScreenToLocalTransform={this.props.ScreenToLocalTransform} + ScreenToLocalTransform={this.screenToLocalTransform} renderDepth={this.props.renderDepth + 1} ContainingCollectionDoc={this.props.ContainingCollectionDoc}> {this.contentFunc} diff --git a/src/client/views/nodes/KeyValueBox.scss b/src/client/views/nodes/KeyValueBox.scss index a26880c9e..eb7c2f32b 100644 --- a/src/client/views/nodes/KeyValueBox.scss +++ b/src/client/views/nodes/KeyValueBox.scss @@ -8,7 +8,6 @@ border-radius: $border-radius; box-sizing: border-box; display: inline-block; - pointer-events: all; cursor: default; .imageBox-cont img { width: auto; diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index 7aad6f90e..2970674a2 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -16,6 +16,8 @@ import { FieldView, FieldViewProps } from './FieldView'; import "./KeyValueBox.scss"; import { KeyValuePair } from "./KeyValuePair"; import React = require("react"); +import { ContextMenu } from "../ContextMenu"; +import { ContextMenuProps } from "../ContextMenuItem"; export type KVPScript = { script: CompiledScript; @@ -34,11 +36,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { @observable private _keyInput: string = ""; @observable private _valueInput: string = ""; @computed get splitPercentage() { return NumCast(this.props.Document.schemaSplitPercentage, 50); } - get fieldDocToLayout() { return this.props.fieldKey ? FieldValue(Cast(this.props.Document[this.props.fieldKey], Doc)) : this.props.Document; } - - constructor(props: FieldViewProps) { - super(props); - } + get fieldDocToLayout() { return this.props.fieldKey ? Cast(this.props.Document[this.props.fieldKey], Doc, null) : this.props.Document; } @action onEnterKey = (e: React.KeyboardEvent): void => { @@ -234,13 +232,26 @@ export class KeyValueBox extends React.Component<FieldViewProps> { return new Doc; } + specificContextMenu = (e: React.MouseEvent): void => { + const cm = ContextMenu.Instance; + const open = cm.findByDescription("Change Perspective..."); + const openItems: ContextMenuProps[] = open && "subitems" in open ? open.subitems : []; + openItems.push({ + description: "Default Perspective", event: () => { + this.props.addDocTab(this.fieldDocToLayout, "inTab"); + this.props.addDocTab(this.props.Document, "close"); + }, icon: "image" + }); + !open && cm.addItem({ description: "Change Perspective...", subitems: openItems, icon: "external-link-alt" }); + } + render() { const dividerDragger = this.splitPercentage === 0 ? (null) : <div className="keyValueBox-dividerDragger" style={{ transform: `translate(calc(${100 - this.splitPercentage}% - 5px), 0px)` }}> <div className="keyValueBox-dividerDraggerThumb" onPointerDown={this.onDividerDown} /> </div>; - return (<div className="keyValueBox-cont" onWheel={this.onPointerWheel} ref={this._mainCont}> + return (<div className="keyValueBox-cont" onWheel={this.onPointerWheel} onContextMenu={this.specificContextMenu} ref={this._mainCont}> <table className="keyValueBox-table"> <tbody className="keyValueBox-tbody"> <tr className="keyValueBox-header"> diff --git a/src/client/views/nodes/LabelBox.scss b/src/client/views/nodes/LabelBox.scss index ab5b2c6b3..56dd86ff9 100644 --- a/src/client/views/nodes/LabelBox.scss +++ b/src/client/views/nodes/LabelBox.scss @@ -1,7 +1,6 @@ .labelBox-outerDiv { width: 100%; height: 100%; - pointer-events: all; border-radius: inherit; display: flex; flex-direction: column; diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx index 9b063e2aa..5c2fc3ffe 100644 --- a/src/client/views/nodes/LabelBox.tsx +++ b/src/client/views/nodes/LabelBox.tsx @@ -36,12 +36,13 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps, LabelDocument } } + get paramsDoc() { return Doc.AreProtosEqual(this.layoutDoc, this.dataDoc) ? this.dataDoc : this.layoutDoc; } specificContextMenu = (e: React.MouseEvent): void => { const funcs: ContextMenuProps[] = []; funcs.push({ description: "Clear Script Params", event: () => { - const params = Cast(this.dataDoc[this.fieldKey + "-paramFieldKeys"], listSpec("string"), []); - params?.map(p => this.dataDoc[p] = undefined); + const params = Cast(this.paramsDoc["onClick-paramFieldKeys"], listSpec("string"), []); + params?.map(p => this.paramsDoc[p] = undefined); }, icon: "trash" }); @@ -52,19 +53,19 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps, LabelDocument @action drop = (e: Event, de: DragManager.DropEvent) => { const docDragData = de.complete.docDragData; - const params = Cast(this.dataDoc[this.fieldKey + "-paramFieldKeys"], listSpec("string"), []); - const missingParams = params?.filter(p => this.dataDoc[p] === undefined); + const params = Cast(this.paramsDoc["onClick-paramFieldKeys"], listSpec("string"), []); + const missingParams = params?.filter(p => !this.paramsDoc[p]); if (docDragData && missingParams?.includes((e.target as any).textContent)) { - this.dataDoc[(e.target as any).textContent] = new List<Doc>(docDragData.droppedDocuments.map((d, i) => + this.paramsDoc[(e.target as any).textContent] = new List<Doc>(docDragData.droppedDocuments.map((d, i) => d.onDragStart ? docDragData.draggedDocuments[i] : d)); e.stopPropagation(); } } // (!missingParams || !missingParams.length ? "" : "(" + missingParams.map(m => m + ":").join(" ") + ")") render() { - const params = Cast(this.dataDoc[this.fieldKey + "-paramFieldKeys"], listSpec("string"), []); - const missingParams = params?.filter(p => this.dataDoc[p] === undefined); - params?.map(p => DocListCast(this.dataDoc[p])); // bcz: really hacky form of prefetching ... + const params = Cast(this.paramsDoc["onClick-paramFieldKeys"], listSpec("string"), []); + const missingParams = params?.filter(p => !this.paramsDoc[p]); + params?.map(p => DocListCast(this.paramsDoc[p])); // bcz: really hacky form of prefetching ... return ( <div className="labelBox-outerDiv" ref={this.createDropTarget} onContextMenu={this.specificContextMenu} style={{ boxShadow: this.layoutDoc.opacity ? StrCast(this.layoutDoc.boxShadow) : "" }}> @@ -76,7 +77,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps, LabelDocument textTransform: StrCast(this.layoutDoc.textTransform) as any }} > <div className="labelBox-mainButtonCenter"> - {StrCast(this.layoutDoc.text, StrCast(this.layoutDoc.title))} + {StrCast(this.rootDoc.text, StrCast(this.rootDoc.title))} </div> </div> <div className="labelBox-fieldKeyParams" > diff --git a/src/client/views/nodes/LinkAnchorBox.scss b/src/client/views/nodes/LinkAnchorBox.scss index 7b6093ebd..710f2178b 100644 --- a/src/client/views/nodes/LinkAnchorBox.scss +++ b/src/client/views/nodes/LinkAnchorBox.scss @@ -4,8 +4,8 @@ width: 15; height: 15; border-radius: 20px; - pointer-events: all; user-select: none; + pointer-events: all; .linkAnchorBox-linkCloser { position: absolute; diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx index 13ffc6956..eb647d0e4 100644 --- a/src/client/views/nodes/LinkAnchorBox.tsx +++ b/src/client/views/nodes/LinkAnchorBox.tsx @@ -17,6 +17,7 @@ import { LinkEditor } from "../linking/LinkEditor"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { SelectionManager } from "../../util/SelectionManager"; import { TraceMobx } from "../../../new_fields/util"; +import { DocumentView } from "./DocumentView"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -39,7 +40,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch @observable _forceOpen = false; onPointerDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, this.onPointerMove, () => { }, this.onClick); + setupMoveUpEvents(this, e, this.onPointerMove, () => { }, this.onClick, false); } onPointerMove = action((e: PointerEvent, down: number[], delta: number[]) => { const cdiv = this._ref && this._ref.current && this._ref.current.parentElement; @@ -74,6 +75,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch anchorContainerDoc && this.props.bringToFront(anchorContainerDoc, false); if (anchorContainerDoc && !this.layoutDoc.onClick && !this._isOpen) { this._timeout = setTimeout(action(() => { + DocumentView._focusHack = []; DocumentManager.Instance.FollowLink(this.rootDoc, anchorContainerDoc, document => this.props.addDocTab(document, StrCast(this.layoutDoc.linkOpenLocation, "inTab")), false); this._editing = false; }), 300 - (Date.now() - this._lastTap)); @@ -105,8 +107,9 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch funcs.push({ description: "Open Link Target on Right", event: () => this.openLinkTargetOnRight(e), icon: "eye" }); funcs.push({ description: "Open Link on Right", event: () => this.openLinkDocOnRight(e), icon: "eye" }); funcs.push({ description: "Open Link Editor", event: () => this.openLinkEditor(e), icon: "eye" }); + funcs.push({ description: "Toggle Always Show Link", event: () => this.props.Document.linkDisplay = !this.props.Document.linkDisplay, icon: "eye" }); - ContextMenu.Instance.addItem({ description: "Link Funcs...", subitems: funcs, icon: "asterisk" }); + ContextMenu.Instance.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); } render() { diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx index af4bf420f..740f2ef04 100644 --- a/src/client/views/nodes/LinkBox.tsx +++ b/src/client/views/nodes/LinkBox.tsx @@ -17,7 +17,6 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps, LinkDocument>( public static LayoutString(fieldKey: string) { return FieldView.LayoutString(LinkBox, fieldKey); } render() { return <div className={`linkBox-container${this.active() ? "-interactive" : ""}`} - onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()} style={{ background: this.props.backgroundColor?.(this.props.Document) }} > <CollectionTreeView {...this.props} diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss index 7a3d2e92b..bccf0f291 100644 --- a/src/client/views/nodes/PDFBox.scss +++ b/src/client/views/nodes/PDFBox.scss @@ -199,9 +199,6 @@ .pdfBox { pointer-events: none; - .collectionFreeFormView-none { - pointer-events: none; - } .pdfViewer-text { .textLayer { span { diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 6db36e43c..3712c648e 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -210,7 +210,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum pdfUrl && funcs.push({ description: "Copy path", event: () => Utils.CopyText(pdfUrl.url.pathname), icon: "expand-arrows-alt" }); funcs.push({ description: "Toggle Fit Width " + (this.Document._fitWidth ? "Off" : "On"), event: () => this.Document._fitWidth = !this.Document._fitWidth, icon: "expand-arrows-alt" }); - ContextMenu.Instance.addItem({ description: "Pdf Funcs...", subitems: funcs, icon: "asterisk" }); + ContextMenu.Instance.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); } @computed get contentScaling() { return this.props.ContentScaling(); } diff --git a/src/client/views/nodes/PresBox.scss b/src/client/views/nodes/PresBox.scss index ba8389fda..6676d8cd2 100644 --- a/src/client/views/nodes/PresBox.scss +++ b/src/client/views/nodes/PresBox.scss @@ -10,7 +10,6 @@ letter-spacing: 2px; overflow: hidden; transition: 0.7s opacity ease; - pointer-events: all; .presBox-buttons { padding: 10px; diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx index e428e16da..80d043db1 100644 --- a/src/client/views/nodes/PresBox.tsx +++ b/src/client/views/nodes/PresBox.tsx @@ -49,7 +49,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @computed get childDocs() { return DocListCast(this.dataDoc[this.fieldKey]); } @computed get currentIndex() { return NumCast(this.layoutDoc._itemIndex); } - updateCurrentPresentation = action(() => Doc.UserDoc().curPresentation = this.rootDoc); + updateCurrentPresentation = action(() => Doc.UserDoc().activePresentation = this.rootDoc); next = () => { this.updateCurrentPresentation(); @@ -253,14 +253,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> updateMinimize = undoBatch(action((e: React.ChangeEvent, mode: CollectionViewType) => { if (BoolCast(this.layoutDoc.inOverlay) !== (mode === CollectionViewType.Invalid)) { if (this.layoutDoc.inOverlay) { - Doc.RemoveDocFromList((Doc.UserDoc().overlays as Doc), undefined, this.rootDoc); + Doc.RemoveDocFromList((Doc.UserDoc().myOverlayDocuments as Doc), undefined, this.rootDoc); CollectionDockingView.AddRightSplit(this.rootDoc); this.layoutDoc.inOverlay = false; } else { this.layoutDoc.x = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0)[0];// 500;//e.clientX + 25; this.layoutDoc.y = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0)[1];////e.clientY - 25; this.props.addDocTab?.(this.rootDoc, "close"); - Doc.AddDocToList((Doc.UserDoc().overlays as Doc), undefined, this.rootDoc); + Doc.AddDocToList((Doc.UserDoc().myOverlayDocuments as Doc), undefined, this.rootDoc); } } })); @@ -292,7 +292,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> this.updateMinimize(e, StrCast(this.layoutDoc._viewType)); }); - childLayoutTemplate = () => this.layoutDoc._viewType === CollectionViewType.Stacking ? Cast(Doc.UserDoc().presentationTemplate, Doc, null) : undefined; + childLayoutTemplate = () => this.layoutDoc._viewType === CollectionViewType.Stacking ? Cast(Doc.UserDoc()["template-presentation"], Doc, null) : undefined; render() { const mode = StrCast(this.layoutDoc._viewType) as CollectionViewType; this.initializeViewAliases(this.childDocs, mode); diff --git a/src/client/views/nodes/QueryBox.scss b/src/client/views/nodes/QueryBox.scss index 82f64054c..b5f90aa1e 100644 --- a/src/client/views/nodes/QueryBox.scss +++ b/src/client/views/nodes/QueryBox.scss @@ -2,5 +2,4 @@ width: 100%; height: 100%; position: absolute; - pointer-events: all; }
\ No newline at end of file diff --git a/src/client/views/nodes/QueryBox.tsx b/src/client/views/nodes/QueryBox.tsx index 947167200..76885eada 100644 --- a/src/client/views/nodes/QueryBox.tsx +++ b/src/client/views/nodes/QueryBox.tsx @@ -3,13 +3,14 @@ import { IReactionDisposer } from "mobx"; import { observer } from "mobx-react"; import { documentSchema } from "../../../new_fields/documentSchemas"; import { Id } from '../../../new_fields/FieldSymbols'; -import { makeInterface } from "../../../new_fields/Schema"; -import { StrCast } from "../../../new_fields/Types"; +import { makeInterface, listSpec } from "../../../new_fields/Schema"; +import { StrCast, Cast } from "../../../new_fields/Types"; import { SelectionManager } from "../../util/SelectionManager"; import { ViewBoxAnnotatableComponent } from '../DocComponent'; import { SearchBox } from "../search/SearchBox"; import { FieldView, FieldViewProps } from './FieldView'; import "./QueryBox.scss"; +import { List } from "../../../new_fields/List"; type QueryDocument = makeInterface<[typeof documentSchema]>; const QueryDocument = makeInterface(documentSchema); @@ -28,7 +29,13 @@ export class QueryBox extends ViewBoxAnnotatableComponent<FieldViewProps, QueryD render() { const dragging = !SelectionManager.GetIsDragging() ? "" : "-dragging"; return <div className={`queryBox${dragging}`} onWheel={(e) => e.stopPropagation()} > - <SearchBox id={this.props.Document[Id]} searchQuery={StrCast(this.dataDoc.searchQuery)} filterQquery={StrCast(this.dataDoc.filterQuery)} /> + <SearchBox + id={this.props.Document[Id]} + setSearchQuery={q => this.dataDoc.searchQuery = q} + searchQuery={StrCast(this.dataDoc.searchQuery)} + setSearchFileTypes={q => this.dataDoc.searchFileTypes = new List<string>(q)} + searchFileTypes={Cast(this.dataDoc.searchFileTypes, listSpec("string"), [])} + filterQquery={StrCast(this.dataDoc.filterQuery)} /> </div >; } }
\ No newline at end of file diff --git a/src/client/views/nodes/ScreenshotBox.scss b/src/client/views/nodes/ScreenshotBox.scss index 6cc184948..141960f60 100644 --- a/src/client/views/nodes/ScreenshotBox.scss +++ b/src/client/views/nodes/ScreenshotBox.scss @@ -1,5 +1,4 @@ .screenshotBox { - pointer-events: all; transform-origin: top left; background: white; color: black; @@ -21,10 +20,6 @@ height: Auto; } -.screenshotBox-content-interactive, .screenshotBox-content-fullScreen { - pointer-events: all; -} - .screenshotBox-uiButtons { background:dimgray; border: orange solid 1px; diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx index 11b24b059..125690dc7 100644 --- a/src/client/views/nodes/ScreenshotBox.tsx +++ b/src/client/views/nodes/ScreenshotBox.tsx @@ -120,7 +120,7 @@ export class ScreenshotBox extends ViewBoxBaseComponent<FieldViewProps, Screensh this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true }); }), icon: "expand-arrows-alt" }); - ContextMenu.Instance.addItem({ description: "Screenshot Funcs...", subitems: subitems, icon: "video" }); + ContextMenu.Instance.addItem({ description: "Options...", subitems: subitems, icon: "video" }); } } diff --git a/src/client/views/nodes/ScriptingBox.scss b/src/client/views/nodes/ScriptingBox.scss index 678a1a22d..43695f00d 100644 --- a/src/client/views/nodes/ScriptingBox.scss +++ b/src/client/views/nodes/ScriptingBox.scss @@ -3,7 +3,6 @@ height: 100%; display: flex; flex-direction: column; - pointer-events: all; background-color: rgb(241, 239, 235); padding: 10px; .scriptingBox-inputDiv { diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx index 8a1d270b3..c607d6614 100644 --- a/src/client/views/nodes/ScriptingBox.tsx +++ b/src/client/views/nodes/ScriptingBox.tsx @@ -28,8 +28,8 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps, Sc @observable private _errorMessage: string = ""; - @computed get rawScript() { return StrCast(this.dataDoc[this.props.fieldKey + "-rawScript"]); } - @computed get compileParams() { return Cast(this.dataDoc[this.props.fieldKey + "-params"], listSpec("string"), []); } + @computed get rawScript() { return StrCast(this.dataDoc[this.props.fieldKey + "-rawScript"], StrCast(this.layoutDoc[this.props.fieldKey + "-rawScript"])); } + @computed get compileParams() { return Cast(this.dataDoc[this.props.fieldKey + "-params"], listSpec("string"), Cast(this.layoutDoc[this.props.fieldKey + "-params"], listSpec("string"), [])); } set rawScript(value) { this.dataDoc[this.props.fieldKey + "-rawScript"] = value; } set compileParams(value) { this.dataDoc[this.props.fieldKey + "-params"] = value; } @@ -75,9 +75,9 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps, Sc />; return ( <div className="scriptingBox-outerDiv" - onPointerDown={e => this.props.isSelected(true) && e.stopPropagation()} onWheel={e => this.props.isSelected(true) && e.stopPropagation()}> - <div className="scriptingBox-inputDiv" > + <div className="scriptingBox-inputDiv" + onPointerDown={e => this.props.isSelected(true) && e.stopPropagation()} > <textarea className="scriptingBox-textarea" placeholder="write your script here" onChange={e => this.rawScript = e.target.value} @@ -87,6 +87,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps, Sc <div className="scriptingBox-errorMessage" style={{ background: this._errorMessage ? "red" : "" }}>{this._errorMessage}</div> <div className="scriptingBox-params" >{params}</div> </div> + {this.rootDoc.layout === "layout" ? <div></div> : (null)} <div className="scriptingBox-toolbar"> <button className="scriptingBox-button" onPointerDown={e => { this.onCompile(); e.stopPropagation(); }}>Compile</button> <button className="scriptingBox-button" onPointerDown={e => { this.onRun(); e.stopPropagation(); }}>Run</button> diff --git a/src/client/views/nodes/SliderBox.scss b/src/client/views/nodes/SliderBox.scss index 4ef277d8c..78015bd70 100644 --- a/src/client/views/nodes/SliderBox.scss +++ b/src/client/views/nodes/SliderBox.scss @@ -1,7 +1,6 @@ .sliderBox-outerDiv { width: 100%; height: 100%; - pointer-events: all; border-radius: inherit; display: flex; flex-direction: column; diff --git a/src/client/views/nodes/SliderBox.tsx b/src/client/views/nodes/SliderBox.tsx index 746ea0b64..b2d451ea8 100644 --- a/src/client/views/nodes/SliderBox.tsx +++ b/src/client/views/nodes/SliderBox.tsx @@ -40,7 +40,7 @@ export class SliderBox extends ViewBoxBaseComponent<FieldViewProps, SliderDocume specificContextMenu = (e: React.MouseEvent): void => { const funcs: ContextMenuProps[] = []; funcs.push({ description: "Edit Thumb Change Script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Thumb Change ...", this.props.Document, "onThumbChange", obj.x, obj.y) }); - ContextMenu.Instance.addItem({ description: "Slider Funcs...", subitems: funcs, icon: "asterisk" }); + ContextMenu.Instance.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); } onChange = (values: readonly number[]) => runInAction(() => { this.dataDoc[this.minThumbKey] = values[0]; diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss index fabbf5196..0c0854ac2 100644 --- a/src/client/views/nodes/VideoBox.scss +++ b/src/client/views/nodes/VideoBox.scss @@ -1,5 +1,4 @@ .videoBox { - pointer-events: all; transform-origin: top left; .videoBox-viewer { opacity: 0.99; // hack! overcomes some kind of Chrome weirdness where buttons (e.g., snapshot) disappear at some point as the video is resized larger @@ -24,9 +23,9 @@ height: 100%; } -.videoBox-content-interactive, .videoBox-content-fullScreen, .videoBox-content-YouTube-fullScreen { - pointer-events: all; -} +// .videoBox-content-interactive, .videoBox-content-fullScreen, .videoBox-content-YouTube-fullScreen { +// pointer-events: all; +// } .videoBox-time{ color : white; diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 588068334..613929bca 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -209,7 +209,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true }); }), icon: "expand-arrows-alt" }); - ContextMenu.Instance.addItem({ description: "Video Funcs...", subitems: subitems, icon: "video" }); + ContextMenu.Instance.addItem({ description: "Options...", subitems: subitems, icon: "video" }); } } diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss index b41687c11..af84a7d95 100644 --- a/src/client/views/nodes/WebBox.scss +++ b/src/client/views/nodes/WebBox.scss @@ -3,9 +3,22 @@ .webBox-container, .webBox-container-dragging { transform-origin: top left; + .webBox-outerContent { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + overflow: auto; + .webBox-innerContent { + width:100%; + } + } + div.webBox-outerContent::-webkit-scrollbar-thumb { + display:none; + } } -.webBox-cont, -.webBox-cont-dragging { +.webBox-cont { padding: 0vw; position: absolute; top: 0; @@ -18,8 +31,6 @@ } .webBox-cont-interactive { - pointer-events: all; - span { user-select: text !important; } @@ -35,22 +46,26 @@ width: 100%; height: 100%; position: absolute; - pointer-events: all; } -.webBox-button { - padding: 0vw; - border: none; +.webBox-buttons { + margin-left: 44; + background:lightGray; width: 100%; - height: 100%; +} +.webBox-freeze { + display: flex; + align-items: center; + justify-content: center; + margin-right: 5px; + width: 30px; } -.webView-urlEditor { +.webBox-urlEditor { position: relative; opacity: 0.9; z-index: 9001; transition: top .5s; - background: lightgrey; padding: 10px; @@ -101,7 +116,6 @@ width: 100%; height: 100%; position: absolute; - pointer-events: all; .indicator { position: absolute; diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 55ad7eb0f..66ddf64c9 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -1,24 +1,22 @@ import { library } from "@fortawesome/fontawesome-svg-core"; -import { faStickyNote } from '@fortawesome/free-solid-svg-icons'; -import { action, computed, observable } from "mobx"; +import { faStickyNote, faPen, faMousePointer } from '@fortawesome/free-solid-svg-icons'; +import { action, computed, observable, trace, IReactionDisposer, reaction } from "mobx"; import { observer } from "mobx-react"; import { Doc, FieldResult } from "../../../new_fields/Doc"; import { documentSchema } from "../../../new_fields/documentSchemas"; import { HtmlField } from "../../../new_fields/HtmlField"; import { InkTool } from "../../../new_fields/InkField"; import { makeInterface } from "../../../new_fields/Schema"; -import { Cast, NumCast } from "../../../new_fields/Types"; +import { Cast, NumCast, BoolCast, StrCast } from "../../../new_fields/Types"; import { WebField } from "../../../new_fields/URLField"; import { Utils, returnOne, emptyFunction, returnZero } from "../../../Utils"; import { Docs } from "../../documents/Documents"; import { DragManager } from "../../util/DragManager"; import { ImageUtils } from "../../util/Import & Export/ImageUtils"; -import { SelectionManager } from "../../util/SelectionManager"; import { ViewBoxAnnotatableComponent } from "../DocComponent"; import { DocumentDecorations } from "../DocumentDecorations"; import { InkingControl } from "../InkingControl"; import { FieldView, FieldViewProps } from './FieldView'; -import { KeyValueBox } from "./KeyValueBox"; import "./WebBox.scss"; import React = require("react"); import * as WebRequest from 'web-request'; @@ -26,7 +24,6 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; const htmlToText = require("html-to-text"); - library.add(faStickyNote); type WebDocument = makeInterface<[typeof documentSchema]>; @@ -36,23 +33,53 @@ const WebDocument = makeInterface(documentSchema); export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocument>(WebDocument) { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(WebBox, fieldKey); } - @observable private collapsed: boolean = true; - @observable private url: string = "hello"; + get _collapsed() { return StrCast(this.layoutDoc._chromeStatus) === "disabled"; } + set _collapsed(value) { this.layoutDoc._chromeStatus = !value ? "enabled" : "disabled"; } + @observable private _url: string = "hello"; + @observable private _pressX: number = 0; + @observable private _pressY: number = 0; private _longPressSecondsHack?: NodeJS.Timeout; + private _outerRef = React.createRef<HTMLDivElement>(); private _iframeRef = React.createRef<HTMLIFrameElement>(); private _iframeIndicatorRef = React.createRef<HTMLDivElement>(); private _iframeDragRef = React.createRef<HTMLDivElement>(); - @observable private _pressX: number = 0; - @observable private _pressY: number = 0; - + private _reactionDisposer?: IReactionDisposer; + private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean) => void); + + iframeLoaded = action((e: any) => { + this._iframeRef.current!.contentDocument?.addEventListener('pointerdown', this.iframedown, false); + this._iframeRef.current!.contentDocument?.addEventListener('scroll', this.iframeScrolled, false); + this.layoutDoc.scrollHeight = this._iframeRef.current!.contentDocument?.children?.[0].scrollHeight || 1000; + this._iframeRef.current!.contentDocument!.children[0].scrollTop = NumCast(this.layoutDoc.scrollTop); + this._reactionDisposer?.(); + this._reactionDisposer = reaction(() => this.layoutDoc.scrollY, + (scrollY) => { + if (scrollY !== undefined) { + this._outerRef.current!.scrollTop = scrollY; + this.layoutDoc.scrollY = undefined; + } + }, + { fireImmediately: true } + ); + }); + setPreviewCursor = (func?: (x: number, y: number, drag: boolean) => void) => this._setPreviewCursor = func; + iframedown = (e: PointerEvent) => { + this._setPreviewCursor?.(e.screenX, e.screenY, false); + } + iframeScrolled = (e: any) => { + const scroll = e.target?.children?.[0].scrollTop; + this.layoutDoc.scrollTop = this._outerRef.current!.scrollTop = scroll; + } async componentDidMount() { this.setURL(); + this._iframeRef.current!.setAttribute("enable-annotation", "true"); + document.addEventListener("pointerup", this.onLongPressUp); document.addEventListener("pointermove", this.onLongPressMove); - const field = Cast(this.props.Document[this.props.fieldKey], WebField); + const field = Cast(this.rootDoc[this.props.fieldKey], WebField); if (field?.url.href.indexOf("youtube") !== -1) { const youtubeaspect = 400 / 315; const nativeWidth = NumCast(this.layoutDoc._nativeWidth); @@ -66,29 +93,31 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum const result = await WebRequest.get(Utils.CorsProxy(field.url.href)); this.dataDoc.text = htmlToText.fromString(result.content); } - } componentWillUnmount() { + this._reactionDisposer?.(); document.removeEventListener("pointerup", this.onLongPressUp); document.removeEventListener("pointermove", this.onLongPressMove); + this._iframeRef.current!.contentDocument?.removeEventListener('pointerdown', this.iframedown); + this._iframeRef.current!.contentDocument?.removeEventListener('scroll', this.iframeScrolled); } @action onURLChange = (e: React.ChangeEvent<HTMLInputElement>) => { - this.url = e.target.value; + this._url = e.target.value; } @action submitURL = () => { - this.dataDoc[this.props.fieldKey] = new WebField(new URL(this.url)); + this.dataDoc[this.props.fieldKey] = new WebField(new URL(this._url)); } @action setURL() { const urlField: FieldResult<WebField> = Cast(this.dataDoc[this.props.fieldKey], WebField); - if (urlField) this.url = urlField.url.toString(); - else this.url = ""; + if (urlField) this._url = urlField.url.toString(); + else this._url = ""; } onValueKeyDown = async (e: React.KeyboardEvent) => { @@ -98,47 +127,45 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum } } - - switchToText = () => { - let url: string = ""; - const field = Cast(this.props.Document[this.props.fieldKey], WebField); - if (field) url = field.url.href; - - const newBox = Docs.Create.TextDocument(url, { - x: NumCast(this.props.Document.x), - y: NumCast(this.props.Document.y), - title: url, - _width: 200, - _height: 70, - }); - - SelectionManager.SelectedDocuments().map(dv => { - dv.props.addDocument && dv.props.addDocument(newBox); - dv.props.removeDocument && dv.props.removeDocument(dv.props.Document); - }); - - Doc.BrushDoc(newBox); + toggleNativeDimensions = () => { + if (!this.layoutDoc.isAnnotating) { + //DocumentView.unfreezeNativeDimensions(this.layoutDoc); + this.layoutDoc.lockedTransform = false; + this.layoutDoc.isAnnotating = true; + } + else { + //Doc.freezeNativeDimensions(this.layoutDoc, this.props.PanelWidth(), this.props.PanelHeight()); + this.layoutDoc.lockedTransform = true; + this.layoutDoc.isAnnotating = false; + } } urlEditor() { + const frozen = this.layoutDoc._nativeWidth && this.layoutDoc.isAnnotating; return ( - <div className="webView-urlEditor" style={{ top: this.collapsed ? -70 : 0 }}> + <div className="webBox-urlEditor" style={{ top: this._collapsed ? -70 : 0 }}> <div className="urlEditor"> <div className="editorBase"> <button className="editor-collapse" style={{ - top: this.collapsed ? 70 : 10, - transform: `rotate(${this.collapsed ? 180 : 0}deg) scale(${this.collapsed ? 0.5 : 1}) translate(${this.collapsed ? "-100%, -100%" : "0, 0"})`, - opacity: (this.collapsed && !this.props.isSelected()) ? 0 : 0.9, - left: (this.collapsed ? 0 : "unset"), + top: this._collapsed ? 70 : 10, + transform: `rotate(${this._collapsed ? 180 : 0}deg) scale(${this._collapsed ? 0.5 : 1}) translate(${this._collapsed ? "-100%, -100%" : "0, 0"})`, + opacity: (this._collapsed && !this.props.isSelected()) ? 0 : 0.9, + left: (this._collapsed ? 0 : "unset"), }} title="Collapse Url Editor" onClick={this.toggleCollapse}> <FontAwesomeIcon icon="caret-up" size="2x" /> </button> - <div style={{ marginLeft: 54, width: "100%", display: this.collapsed ? "none" : "flex" }}> + <div className="webBox-buttons" style={{ display: this._collapsed ? "none" : "flex" }}> + <div className="webBox-freeze" title={"Annotate"} style={{ background: frozen ? "lightBlue" : "gray" }} onClick={this.toggleNativeDimensions} > + <FontAwesomeIcon icon={faPen} size={"2x"} /> + </div> + <div className="webBox-freeze" title={"Select"} style={{ background: !frozen ? "lightBlue" : "gray" }} onClick={this.toggleNativeDimensions} > + <FontAwesomeIcon icon={faMousePointer} size={"2x"} /> + </div> <input className="webpage-urlInput" placeholder="ENTER URL" - value={this.url} + value={this._url} onChange={this.onURLChange} onKeyDown={this.onValueKeyDown} /> @@ -151,9 +178,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum <button className="submitUrl" onClick={this.submitURL}> SUBMIT </button> - <div className="switchToText" title="Convert web to text doc" onClick={this.switchToText} style={{ display: "flex", alignItems: "center", justifyContent: "center" }} > - <FontAwesomeIcon icon={faStickyNote} size={"lg"} /> - </div> </div> </div> </div> @@ -164,7 +188,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum @action toggleCollapse = () => { - this.collapsed = !this.collapsed; + this._collapsed = !this._collapsed; } _ignore = 0; @@ -293,7 +317,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum if (field instanceof HtmlField) { view = <span id="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: field.html }} />; } else if (field instanceof WebField) { - view = <iframe ref={this._iframeRef} src={Utils.CorsProxy(field.url.href)} style={{ position: "absolute", width: "100%", height: "100%", top: 0 }} />; + view = <iframe ref={this._iframeRef} onLoad={this.iframeLoaded} src={Utils.CorsProxy(field.url.href)} style={{ position: "absolute", width: "100%", height: "100%", top: 0 }} />; } else { view = <iframe ref={this._iframeRef} src={"https://crossorigin.me/https://cs.brown.edu"} style={{ position: "absolute", width: "100%", height: "100%", top: 0 }} />; } @@ -303,56 +327,68 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum {view} </div>; - const decInteracting = DocumentDecorations.Instance && DocumentDecorations.Instance.Interacting; + const decInteracting = DocumentDecorations.Instance?.Interacting; const frozen = !this.props.isSelected() || decInteracting; - const classname = "webBox-cont" + (this.props.isSelected() && InkingControl.Instance.selectedTool === InkTool.None && !decInteracting ? "-interactive" : ""); - return ( - <> - <div className={classname} > - {content} - </div> - {!frozen ? (null) : - <div className="webBox-overlay" onWheel={this.onPreWheel} onPointerDown={this.onPrePointer} onPointerMove={this.onPrePointer} onPointerUp={this.onPrePointer}> - <div className="touch-iframe-overlay" onPointerDown={this.onLongPressDown} > - <div className="indicator" ref={this._iframeIndicatorRef}></div> - <div className="dragger" ref={this._iframeDragRef}></div> - </div> - </div>} - </>); + return (<> + <div className={"webBox-cont" + (this.props.isSelected() && InkingControl.Instance.selectedTool === InkTool.None && !decInteracting ? "-interactive" : "")} > + {content} + </div> + {!frozen ? (null) : + <div className="webBox-overlay" style={{ pointerEvents: this.layoutDoc.isBackground ? undefined : "all" }} + onWheel={this.onPreWheel} onPointerDown={this.onPrePointer} onPointerMove={this.onPrePointer} onPointerUp={this.onPrePointer}> + <div className="touch-iframe-overlay" onPointerDown={this.onLongPressDown} > + <div className="indicator" ref={this._iframeIndicatorRef}></div> + <div className="dragger" ref={this._iframeDragRef}></div> + </div> + </div>} + </>); } + scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.props.Document.scrollTop)); render() { - const dragging = "";//</div>!SelectionManager.GetIsDragging() ? "" : "-dragging"; - return (<div className={`webBox-container${dragging}`} + return (<div className={`webBox-container`} style={{ transform: `scale(${this.props.ContentScaling()})`, width: `${100 / this.props.ContentScaling()}%`, height: `${100 / this.props.ContentScaling()}%`, - pointerEvents: this.props.Document.isBackground ? "none" : undefined + pointerEvents: this.layoutDoc.isBackground ? "none" : undefined }} > - <CollectionFreeFormView {...this.props} - PanelHeight={this.props.PanelHeight} - PanelWidth={this.props.PanelWidth} - annotationsKey={this.annotationKey} - NativeHeight={returnZero} - NativeWidth={returnZero} - focus={this.props.focus} - isSelected={this.props.isSelected} - isAnnotationOverlay={true} - select={emptyFunction} - active={this.active} - ContentScaling={returnOne} - whenActiveChanged={this.whenActiveChanged} - removeDocument={this.removeDocument} - moveDocument={this.moveDocument} - addDocument={this.addDocument} - CollectionView={undefined} - ScreenToLocalTransform={this.props.ScreenToLocalTransform} - renderDepth={this.props.renderDepth + 1} - ContainingCollectionDoc={this.props.ContainingCollectionDoc}> - {() => [this.content]} - </CollectionFreeFormView> + {this.content} + <div className={"webBox-outerContent"} ref={this._outerRef} + style={{ pointerEvents: this.layoutDoc.isAnnotating && !this.layoutDoc.isBackground ? "all" : "none" }} + onWheel={e => e.stopPropagation()} + onScroll={e => { + if (this._iframeRef.current!.contentDocument!.children[0].scrollTop !== this._outerRef.current!.scrollTop) { + this._iframeRef.current!.contentDocument!.children[0].scrollTop = this._outerRef.current!.scrollTop; + } + //this._outerRef.current!.scrollTop !== this._scrollTop && (this._outerRef.current!.scrollTop = this._scrollTop) + }}> + <div className={"webBox-innerContent"} style={{ height: NumCast(this.layoutDoc.scrollHeight) }}> + <CollectionFreeFormView {...this.props} + PanelHeight={this.props.PanelHeight} + PanelWidth={this.props.PanelWidth} + annotationsKey={this.annotationKey} + NativeHeight={returnZero} + NativeWidth={returnZero} + focus={this.props.focus} + setPreviewCursor={this.setPreviewCursor} + isSelected={this.props.isSelected} + isAnnotationOverlay={true} + select={emptyFunction} + active={this.active} + ContentScaling={returnOne} + whenActiveChanged={this.whenActiveChanged} + removeDocument={this.removeDocument} + moveDocument={this.moveDocument} + addDocument={this.addDocument} + CollectionView={undefined} + ScreenToLocalTransform={this.scrollXf} + renderDepth={this.props.renderDepth + 1} + ContainingCollectionDoc={this.props.ContainingCollectionDoc}> + </CollectionFreeFormView> + </div> + </div> </div >); } }
\ No newline at end of file diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx index 71b19f3a6..eaf80d252 100644 --- a/src/client/views/pdf/Annotation.tsx +++ b/src/client/views/pdf/Annotation.tsx @@ -8,6 +8,7 @@ import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types"; import { DocumentManager } from "../../util/DocumentManager"; import PDFMenu from "./PDFMenu"; import "./Annotation.scss"; +import { DocumentView } from "../nodes/DocumentView"; interface IAnnotationProps { anno: Doc; @@ -97,7 +98,8 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> { else if (e.button === 0) { const annoGroup = await Cast(this.props.document.group, Doc); if (annoGroup) { - DocumentManager.Instance.FollowLink(undefined, annoGroup, (doc, followLinkLocation) => this.props.addDocTab(doc, e.ctrlKey ? "inTab" : followLinkLocation), false, undefined); + DocumentView._focusHack = []; + DocumentManager.Instance.FollowLink(undefined, annoGroup, (doc, followLinkLocation) => this.props.addDocTab(doc, e.ctrlKey ? "inTab" : followLinkLocation), false, undefined); e.stopPropagation(); } } diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss index 5cd2c4fe4..760f64a72 100644 --- a/src/client/views/pdf/PDFViewer.scss +++ b/src/client/views/pdf/PDFViewer.scss @@ -30,10 +30,6 @@ .page { position: relative; } - .collectionfreeformview-container { - pointer-events: none; - } - .pdfViewer-text-selected { .textLayer{ pointer-events: all; @@ -61,12 +57,11 @@ display: inline-block; width:100%; pointer-events: none; - } - .pdfViewer-overlay-inking { - .collectionfreeformview-container { + .collectionFreeFormDocumentView-container { pointer-events: all; } } + .pdfViewer-annotationLayer { position: absolute; transform-origin: left top; diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index c49e6512a..46b60b554 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -9,7 +9,7 @@ import { List } from "../../../new_fields/List"; import { makeInterface, createSchema } from "../../../new_fields/Schema"; import { ScriptField, ComputedField } from "../../../new_fields/ScriptField"; import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; -import { smoothScroll, Utils, emptyFunction, returnOne, intersectRect, addStyleSheet, addStyleSheetRule, clearStyleSheetRules, returnZero } from "../../../Utils"; +import { smoothScroll, Utils, emptyFunction, returnOne, intersectRect, addStyleSheet, addStyleSheetRule, clearStyleSheetRules, returnZero, emptyPath } from "../../../Utils"; import { Docs, DocUtils } from "../../documents/Documents"; import { DragManager } from "../../util/DragManager"; import { CompiledScript, CompileScript } from "../../util/Scripting"; @@ -282,7 +282,6 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu if (anno.style.height) annoDoc._height = parseInt(anno.style.height); if (anno.style.width) annoDoc._width = parseInt(anno.style.width); annoDoc.group = mainAnnoDoc; - annoDoc.isLinkButton = true; annoDocs.push(annoDoc); anno.remove(); mainAnnoDoc = annoDoc; @@ -419,7 +418,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu addStyleSheetRule(PDFViewer._annotationStyle, "pdfAnnotation", { "pointer-events": "none" }); if ((this.Document.scale || 1) !== 1) return; if ((e.button !== 0 || e.altKey) && this.active(true)) { - this._setPreviewCursor && this._setPreviewCursor(e.clientX, e.clientY, true); + this._setPreviewCursor?.(e.clientX, e.clientY, true); //e.stopPropagation(); } this._marqueeing = false; @@ -554,7 +553,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu highlight = (color: string) => { // creates annotation documents for current highlights const annotationDoc = this.makeAnnotationDocument(color); - annotationDoc && this.props.addDocument && this.props.addDocument(annotationDoc); + annotationDoc && this.props.addDocument?.(annotationDoc); return annotationDoc; } @@ -574,7 +573,9 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu clipDoc._scrollTop = this.marqueeY(); const targetDoc = Docs.Create.TextDocument("", { _width: 200, _height: 200, title: "Note linked to " + this.props.Document.title }); Doc.GetProto(targetDoc).data = new List<Doc>([clipDoc]); + clipDoc.rootDocument = targetDoc; DocumentView.makeCustomViewClicked(targetDoc, Docs.Create.StackingDocument, "slideView", undefined); + targetDoc.layoutKey = "layout"; // const targetDoc = Docs.Create.TextDocument("", { _width: 200, _height: 200, title: "Note linked to " + this.props.Document.title }); // Doc.GetProto(targetDoc).snipped = this.dataDoc[this.props.fieldKey][Copy](); // const snipLayout = Docs.Create.PdfDocument("http://www.msn.com", { title: "snippetView", isTemplateDoc: true, isTemplateForField: "snipped", _fitWidth: true, _width: this.marqueeWidth(), _height: this.marqueeHeight(), _scrollTop: this.marqueeY() }); @@ -585,6 +586,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu dragComplete: e => { if (!e.aborted && e.annoDragData && !e.annoDragData.linkedToDoc) { const link = DocUtils.MakeLink({ doc: annotationDoc }, { doc: e.annoDragData.dropDocument }, "Annotation"); + annotationDoc.isLinkButton = true; if (link) link.followLinkLocation = "onRight"; } } @@ -607,7 +609,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu getCoverImage = () => { - if (!this.props.Document[HeightSym]() || !this.props.Document.nativeHeight) { + if (!this.props.Document[HeightSym]() || !this.props.Document._nativeHeight) { setTimeout((() => { this.Document._height = this.Document[WidthSym]() * this._coverPath.height / this._coverPath.width; this.Document._nativeHeight = (this.Document._nativeWidth || 0) * this._coverPath.height / this._coverPath.width; @@ -632,7 +634,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu @computed get annotationLayer() { TraceMobx(); - return <div className="pdfViewer-annotationLayer" style={{ height: NumCast(this.Document.nativeHeight), transform: `scale(${this._zoomed})` }} ref={this._annotationLayer}> + return <div className="pdfViewer-annotationLayer" style={{ height: NumCast(this.Document._nativeHeight), transform: `scale(${this._zoomed})` }} ref={this._annotationLayer}> {this.nonDocAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map((anno, index) => <Annotation {...this.props} focus={this.props.focus} dataDoc={this.dataDoc} fieldKey={this.props.fieldKey} anno={anno} key={`${anno[Id]}-annotation`} />)} </div>; @@ -641,9 +643,10 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu panelWidth = () => (this.Document.scrollHeight || this.Document._nativeHeight || 0); panelHeight = () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : (this.Document._nativeWidth || 0); @computed get overlayLayer() { - return <div className={`pdfViewer-overlay${InkingControl.Instance.selectedTool !== InkTool.None ? "-inking" : ""}`} id="overlay" style={{ transform: `scale(${this._zoomed})` }}> + return <div className={`pdfViewer-overlay${InkingControl.Instance.selectedTool !== InkTool.None ? "-inking" : ""}`} id="overlay" + style={{ transform: `scale(${this._zoomed})` }}> <CollectionFreeFormView {...this.props} - LibraryPath={this.props.ContainingCollectionView?.props.LibraryPath ?? []} + LibraryPath={this.props.ContainingCollectionView?.props.LibraryPath ?? emptyPath} annotationsKey={this.annotationKey} setPreviewCursor={this.setPreviewCursor} PanelHeight={this.panelWidth} diff --git a/src/client/views/search/IconBar.tsx b/src/client/views/search/IconBar.tsx index ec942bf7c..9b7cf2fc6 100644 --- a/src/client/views/search/IconBar.tsx +++ b/src/client/views/search/IconBar.tsx @@ -24,8 +24,13 @@ library.add(faChartBar); library.add(faGlobeAsia); library.add(faBan); +export interface IconBarProps { + setIcons: (icons: string[]) => void; +} + + @observer -export class IconBar extends React.Component { +export class IconBar extends React.Component<IconBarProps> { public _allIcons: string[] = [DocumentType.AUDIO, DocumentType.COL, DocumentType.IMG, DocumentType.LINK, DocumentType.PDF, DocumentType.RTF, DocumentType.VID, DocumentType.WEB]; @observable private _icons: string[] = this._allIcons; @@ -38,7 +43,10 @@ export class IconBar extends React.Component { @observable public _select: number = 0; @action.bound - updateIcon(newArray: string[]) { this._icons = newArray; } + updateIcon(newArray: string[]) { + this._icons = newArray; + this.props.setIcons?.(this._icons); + } @action.bound getIcons(): string[] { return this._icons; } diff --git a/src/client/views/search/SearchBox.scss b/src/client/views/search/SearchBox.scss index f0223ca76..bb62113a1 100644 --- a/src/client/views/search/SearchBox.scss +++ b/src/client/views/search/SearchBox.scss @@ -9,7 +9,8 @@ position: absolute; font-size: 10px; line-height: 1; - overflow: auto; + overflow-y: auto; + overflow-x: visible; background: lightgrey, } diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index 19a4d558e..e41b725b1 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -19,13 +19,17 @@ import { FieldView } from '../nodes/FieldView'; import { DocumentType } from "../../documents/DocumentTypes"; import { DocumentView } from '../nodes/DocumentView'; import { SelectionManager } from '../../util/SelectionManager'; +import { listSpec } from '../../../new_fields/Schema'; library.add(faTimes); export interface SearchProps { id: string; - searchQuery?: string; + searchQuery: string; filterQquery?: string; + setSearchQuery: (q: string) => {}; + searchFileTypes: string[]; + setSearchFileTypes: (types: string[]) => {}; } export enum Keys { @@ -37,7 +41,8 @@ export enum Keys { @observer export class SearchBox extends React.Component<SearchProps> { - @observable private _searchString: string = ""; + private get _searchString() { return this.props.searchQuery; } + private set _searchString(value) { this.props.setSearchQuery(value); } @observable private _resultsOpen: boolean = false; @observable private _searchbarOpen: boolean = false; @observable private _results: [Doc, string[], string[]][] = []; @@ -72,20 +77,16 @@ export class SearchBox extends React.Component<SearchProps> { this.resultsScrolled = this.resultsScrolled.bind(this); } - componentDidMount = () => { + componentDidMount = action(() => { if (this.inputRef.current) { this.inputRef.current.focus(); - runInAction(() => this._searchbarOpen = true); + this._searchbarOpen = true; } - if (this.props.searchQuery && this.props.filterQquery) { - console.log(this.props.searchQuery); - const sq = this.props.searchQuery; - runInAction(() => { - this._searchString = sq; - this.submitSearch(); - }); + if (this.props.searchQuery) { // bcz: why was this here? } && this.props.filterQquery) { + this._searchString = this.props.searchQuery; + this.submitSearch(); } - } + }); @action @@ -133,7 +134,10 @@ export class SearchBox extends React.Component<SearchProps> { //this also serves as an indicator if the word status filter is applied @observable private _filterOpen: boolean = false; //if icons = all icons, then no icon filter is applied - @observable private _icons: string[] = this._allIcons; + get _icons() { return this.props.searchFileTypes; } + set _icons(value) { + this.props.setSearchFileTypes(value); + } //if all of these are true, no key filter is applied @observable private _titleFieldStatus: boolean = true; @observable private _authorFieldStatus: boolean = true; @@ -167,15 +171,7 @@ export class SearchBox extends React.Component<SearchProps> { } basicRequireWords(query: string): string { - const oldWords = query.split(" "); - const newWords: string[] = []; - oldWords.forEach(word => { - const newWrd = "+" + word; - newWords.push(newWrd); - }); - query = newWords.join(" "); - - return query; + return query.split(" ").join(" + ").replace(/ + /, ""); } @action @@ -214,12 +210,6 @@ export class SearchBox extends React.Component<SearchProps> { return this._icons.length === this._allIcons.length ? undefined : this._icons; } - @action.bound - updateIcon(newArray: string[]) { this._icons = newArray; } - - @action.bound - getIcons(): string[] { return this._icons; } - //TODO: basically all of this //gets all of the collections of all the docviews that are selected //if a collection is the only thing selected, search only in that collection (not its container) @@ -316,9 +306,13 @@ export class SearchBox extends React.Component<SearchProps> { private get filterQuery() { const types = this.filterTypes; - const includeDeleted = this.getDataStatus() ? "" : " AND NOT deleted_b:true"; - const includeIcons = this.getDataStatus() ? "" : " AND NOT type_t:fonticonbox"; - return "NOT baseProto_b:true" + includeDeleted + includeIcons + (types ? ` AND (${types.map(type => `({!join from=id to=proto_i}type_t:"${type}" AND NOT type_t:*) OR type_t:"${type}"`).join(" ")})` : ""); + const baseExpr = "NOT baseProto_b:true"; + const includeDeleted = this.getDataStatus() ? "" : " NOT deleted_b:true"; + const includeIcons = this.getDataStatus() ? "" : " NOT type_t:fonticonbox"; + const typeExpr = !types ? "" : ` (${types.map(type => `({!join from=id to=proto_i}type_t:"${type}" AND NOT type_t:*) OR type_t:"${type}"`).join(" ")})`; + // fq: type_t:collection OR {!join from=id to=proto_i}type_t:collection q:text_t:hello + const query = [baseExpr, includeDeleted, includeIcons, typeExpr].join(" AND ").replace(/AND $/, ""); + return query; } getDataStatus() { return this._deletedDocsStatus; } @@ -652,7 +646,9 @@ export class SearchBox extends React.Component<SearchProps> { <button className="filter-item" style={this._nodeStatus ? { background: "#aaaaa3" } : {}} onClick={this.handleNodeChange}>Nodes</button> </div> <div id={`node${this.props.id}`} className="filter-body" style={this._nodeStatus ? { borderTop: "grey 1px solid" } : { borderTop: "0px" }}> - <IconBar /> + <IconBar setIcons={(icons: string[]) => { + this._icons = icons; + }} /> </div> <div className="filter-key" id={`key${this.props.id}`} style={this._keyStatus ? { borderTop: "grey 1px solid" } : { borderTop: "0px" }}> <div className="filter-keybar"> diff --git a/src/client/views/webcam/DashWebRTCVideo.tsx b/src/client/views/webcam/DashWebRTCVideo.tsx index 1d52ba38f..2ea011316 100644 --- a/src/client/views/webcam/DashWebRTCVideo.tsx +++ b/src/client/views/webcam/DashWebRTCVideo.tsx @@ -3,7 +3,7 @@ import React = require("react"); import { CollectionFreeFormDocumentViewProps } from "../nodes/CollectionFreeFormDocumentView"; import { FieldViewProps, FieldView } from "../nodes/FieldView"; import { observable, action } from "mobx"; -import { DocumentDecorations, CloseCall } from "../DocumentDecorations"; +import { DocumentDecorations } from "../DocumentDecorations"; import { InkingControl } from "../InkingControl"; import "../../views/nodes/WebBox.scss"; import "./DashWebRTCVideo.scss"; diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx index 5903a2ce9..295e82142 100644 --- a/src/mobile/ImageUpload.tsx +++ b/src/mobile/ImageUpload.tsx @@ -67,7 +67,7 @@ class Uploader extends React.Component { const field = await DocServer.GetRefField(res); let pending: Opt<Doc>; if (field instanceof Doc) { - pending = await Cast(field.optionalRightCollection, Doc); + pending = await Cast(field.rightSidebarCollection, Doc); } if (pending) { this.status = "has pending docs"; diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx index c87ac719c..73ebbb303 100644 --- a/src/mobile/MobileInterface.tsx +++ b/src/mobile/MobileInterface.tsx @@ -1,51 +1,38 @@ import React = require('react'); +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faEraser, faHighlighter, faLongArrowAltLeft, faMousePointer, faPenNib } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; -import { computed, action, observable } from 'mobx'; -import { CurrentUserUtils } from '../server/authentication/models/current_user_utils'; -import { FieldValue, Cast, StrCast } from '../new_fields/Types'; -import { Doc, DocListCast } from '../new_fields/Doc'; +import { DocServer } from '../client/DocServer'; import { Docs } from '../client/documents/Documents'; -import { CollectionView } from '../client/views/collections/CollectionView'; -import { DocumentView } from '../client/views/nodes/DocumentView'; -import { emptyPath, emptyFunction, returnFalse, returnOne, returnEmptyString, returnTrue, returnZero } from '../Utils'; -import { Transform } from '../client/util/Transform'; -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faPenNib, faHighlighter, faEraser, faMousePointer, faBreadSlice, faTrash, faCheck, faLongArrowAltLeft } from '@fortawesome/free-solid-svg-icons'; +import { DocumentManager } from '../client/util/DocumentManager'; +import RichTextMenu from '../client/util/RichTextMenu'; import { Scripting } from '../client/util/Scripting'; -import { CollectionFreeFormView } from '../client/views/collections/collectionFreeForm/CollectionFreeFormView'; +import { Transform } from '../client/util/Transform'; +import { CollectionView } from '../client/views/collections/CollectionView'; +import { DocumentDecorations } from '../client/views/DocumentDecorations'; import GestureOverlay from '../client/views/GestureOverlay'; import { InkingControl } from '../client/views/InkingControl'; -import { InkTool } from '../new_fields/InkField'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import "./MobileInterface.scss"; -import { SelectionManager } from '../client/util/SelectionManager'; -import { DateField } from '../new_fields/DateField'; -import { GestureUtils } from '../pen-gestures/GestureUtils'; -import { DocServer } from '../client/DocServer'; -import { DocumentDecorations } from '../client/views/DocumentDecorations'; -import { OverlayView } from '../client/views/OverlayView'; -import { DictationOverlay } from '../client/views/DictationOverlay'; -import SharingManager from '../client/util/SharingManager'; -import { PreviewCursor } from '../client/views/PreviewCursor'; -import { ContextMenu } from '../client/views/ContextMenu'; +import { DocumentView } from '../client/views/nodes/DocumentView'; import { RadialMenu } from '../client/views/nodes/RadialMenu'; -import PDFMenu from '../client/views/pdf/PDFMenu'; -import MarqueeOptionsMenu from '../client/views/collections/collectionFreeForm/MarqueeOptionsMenu'; -import GoogleAuthenticationManager from '../client/apis/GoogleAuthenticationManager'; -import { listSpec } from '../new_fields/Schema'; +import { PreviewCursor } from '../client/views/PreviewCursor'; +import { Doc, DocListCast, FieldResult } from '../new_fields/Doc'; import { Id } from '../new_fields/FieldSymbols'; -import { DocumentManager } from '../client/util/DocumentManager'; -import RichTextMenu from '../client/util/RichTextMenu'; +import { InkTool } from '../new_fields/InkField'; +import { listSpec } from '../new_fields/Schema'; +import { Cast, FieldValue } from '../new_fields/Types'; import { WebField } from "../new_fields/URLField"; -import { FieldResult } from "../new_fields/Doc"; -import { List } from '../new_fields/List'; +import { CurrentUserUtils } from '../server/authentication/models/current_user_utils'; +import { emptyFunction, emptyPath, returnEmptyString, returnFalse, returnOne, returnTrue, returnZero } from '../Utils'; +import "./MobileInterface.scss"; library.add(faLongArrowAltLeft); @observer export default class MobileInterface extends React.Component { @observable static Instance: MobileInterface; - @computed private get userDoc() { return CurrentUserUtils.UserDocument; } + @computed private get userDoc() { return Doc.UserDoc(); } @computed private get mainContainer() { return this.userDoc ? FieldValue(Cast(this.userDoc.activeMobile, Doc)) : CurrentUserUtils.GuestMobile; } // @observable private currentView: "main" | "ink" | "upload" = "main"; private mainDoc: any = CurrentUserUtils.setupMobileDoc(this.userDoc); diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index 92abb7a71..5f790f886 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -565,8 +565,9 @@ export namespace Doc { } else if (cfield instanceof ComputedField) { copy[key] = ComputedField.MakeFunction(cfield.script.originalScript); } else if (field instanceof ObjectField) { - copy[key] = key.includes("layout[") && doc[key] instanceof Doc ? Doc.MakeCopy(doc[key] as Doc, false) : - doc[key] instanceof Doc ? doc[key] : ObjectField.MakeCopy(field); + copy[key] = doc[key] instanceof Doc ? + key.includes("layout[") ? Doc.MakeCopy(doc[key] as Doc, false) : doc[key] : // reference documents except copy documents that are expanded teplate fields + ObjectField.MakeCopy(field); } else if (field instanceof Promise) { debugger; //This shouldn't happend... } else { @@ -578,6 +579,44 @@ export namespace Doc { return copy; } + export function MakeClone(doc: Doc, cloneProto: boolean = true): Doc { + const copy = new Doc(undefined, true); + const exclude = Cast(doc.excludeFields, listSpec("string"), []); + Object.keys(doc).forEach(key => { + if (exclude.includes(key)) return; + const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); + const field = ProxyField.WithoutProxy(() => doc[key]); + if (key === "proto" && cloneProto) { + if (doc[key] instanceof Doc) { + copy[key] = Doc.MakeClone(doc[key]!, false); + } + } else { + if (field instanceof RefField) { + copy[key] = field; + } else if (cfield instanceof ComputedField) { + copy[key] = ComputedField.MakeFunction(cfield.script.originalScript); + } else if (field instanceof ObjectField) { + const list = Cast(doc[key], listSpec(Doc)); + if (list !== undefined && !(list instanceof Promise)) { + copy[key] = new List<Doc>(list.filter(d => d instanceof Doc).map(d => Doc.MakeCopy(d as Doc, false))); + } else { + copy[key] = doc[key] instanceof Doc ? + key.includes("layout[") ? + Doc.MakeCopy(doc[key] as Doc, false) : doc[key] : // reference documents except copy documents that are expanded teplate fields + ObjectField.MakeCopy(field); + } + } else if (field instanceof Promise) { + debugger; //This shouldn't happend... + } else { + copy[key] = field; + } + } + }); + Doc.SetInPlace(copy, "title", "CLONE: " + doc.title, true); + copy.cloneOf = doc; + return copy; + } + export function MakeDelegate(doc: Doc, id?: string, title?: string): Doc; export function MakeDelegate(doc: Opt<Doc>, id?: string, title?: string): Opt<Doc>; export function MakeDelegate(doc: Opt<Doc>, id?: string, title?: string): Opt<Doc> { @@ -603,14 +642,6 @@ export namespace Doc { return undefined; } export function ApplyTemplateTo(templateDoc: Doc, target: Doc, targetKey: string, titleTarget: string | undefined) { - if (!templateDoc) { - target.layout = undefined; - target._nativeWidth = undefined; - target._nativeHeight = undefined; - target.type = undefined; - return; - } - if (!Doc.AreProtosEqual(target[targetKey] as Doc, templateDoc)) { if (target.resolvedDataDoc) { target[targetKey] = new PrefetchProxy(templateDoc); @@ -873,6 +904,16 @@ export namespace Doc { return id; } + export function isDocPinned(doc: Doc) { + //add this new doc to props.Document + const curPres = Cast(Doc.UserDoc().activePresentation, Doc) as Doc; + if (curPres) { + return DocListCast(curPres.data).findIndex((val) => Doc.AreProtosEqual(val, doc)) !== -1; + } + return false; + } + + export async function addFieldEnumerations(doc: Opt<Doc>, enumeratedFieldKey: string, enumerations: { title: string, _backgroundColor?: string, color?: string }[]) { let optionsCollection = await DocServer.GetRefField(enumeratedFieldKey); if (!(optionsCollection instanceof Doc)) { @@ -914,13 +955,13 @@ Scripting.addGlobal(function redo() { return UndoManager.Redo(); }); Scripting.addGlobal(function DOC(id: string) { console.log("Can't parse a document id in a script"); return "invalid"; }); Scripting.addGlobal(function assignDoc(doc: Doc, field: string, id: string) { return Doc.assignDocToField(doc, field, id); }); Scripting.addGlobal(function docCast(doc: FieldResult): any { return DocCastAsync(doc); }); -Scripting.addGlobal(function curPresentationItem() { - const curPres = Doc.UserDoc().curPresentation as Doc; +Scripting.addGlobal(function activePresentationItem() { + const curPres = Doc.UserDoc().activePresentation as Doc; return curPres && DocListCast(curPres[Doc.LayoutFieldKey(curPres)])[NumCast(curPres._itemIndex)]; }); -Scripting.addGlobal(function selectDoc(doc: any) { Doc.UserDoc().SelectedDocs = new List([doc]); }); +Scripting.addGlobal(function selectDoc(doc: any) { Doc.UserDoc().activeSelection = new List([doc]); }); Scripting.addGlobal(function selectedDocs(container: Doc, excludeCollections: boolean, prevValue: any) { - const docs = DocListCast(Doc.UserDoc().SelectedDocs). + const docs = DocListCast(Doc.UserDoc().activeSelection). filter(d => !Doc.AreProtosEqual(d, container) && !d.annotationOn && d.type !== DocumentType.DOCHOLDER && d.type !== DocumentType.KVP && (!excludeCollections || d.type !== DocumentType.COL || !Cast(d.data, listSpec(Doc), null))); return docs.length ? new List(docs) : prevValue; diff --git a/src/new_fields/documentSchemas.ts b/src/new_fields/documentSchemas.ts index b11941f40..61b185e5c 100644 --- a/src/new_fields/documentSchemas.ts +++ b/src/new_fields/documentSchemas.ts @@ -57,6 +57,8 @@ export const documentSchema = createSchema({ isLinkButton: "boolean", // whether document functions as a link follow button to follow the first link on the document when clicked ignoreClick: "boolean", // whether documents ignores input clicks (but does not ignore manipulation and other events) scrollToLinkID: "string", // id of link being traversed. allows this doc to scroll/highlight/etc its link anchor. scrollToLinkID should be set to undefined by this doc after it sets up its scroll,etc. + scrollY: "number", // "command" to scroll a document to a position on load (the value will be reset to 0 after that ) + scrollTop: "number", // scroll position of a scrollable document (pdf, text, web) strokeWidth: "number", fontSize: "string", fitToBox: "boolean", // whether freeform view contents should be zoomed/panned to fill the area of the document view @@ -74,7 +76,7 @@ export const positionSchema = createSchema({ export const collectionSchema = createSchema({ childLayout: Doc, // layout template for children of a collecion - childDetailed: Doc, // layout template to apply to a child when its clicked on in a collection and opened (requires onChildClick or other script to use this field) + childDetailView: Doc, // layout template to apply to a child when its clicked on in a collection and opened (requires onChildClick or other script to use this field) onChildClick: ScriptField, // script to run for each child when its clicked onCheckedClick: ScriptField, // script to run when a checkbox is clicked next to a child in a tree view }); diff --git a/src/server/ApiManagers/SearchManager.ts b/src/server/ApiManagers/SearchManager.ts index 5f7d1cf6d..753c31fcf 100644 --- a/src/server/ApiManagers/SearchManager.ts +++ b/src/server/ApiManagers/SearchManager.ts @@ -1,18 +1,14 @@ -import ApiManager, { Registration } from "./ApiManager"; -import { Method } from "../RouteManager"; -import { Search } from "../Search"; -const findInFiles = require('find-in-files'); +import { exec } from "child_process"; +import { cyan, green, red, yellow } from "colors"; import * as path from 'path'; -import { pathToDirectory, Directory } from "./UploadManager"; -import { red, cyan, yellow, green } from "colors"; -import RouteSubscriber from "../RouteSubscriber"; -import { exec, execSync } from "child_process"; -import { onWindows } from ".."; -import { get } from "request-promise"; import { log_execution } from "../ActionUtilities"; import { Database } from "../database"; -import rimraf = require("rimraf"); -import { mkdirSync, chmod, chmodSync } from "fs"; +import { Method } from "../RouteManager"; +import RouteSubscriber from "../RouteSubscriber"; +import { Search } from "../Search"; +import ApiManager, { Registration } from "./ApiManager"; +import { Directory, pathToDirectory } from "./UploadManager"; +const findInFiles = require('find-in-files'); export class SearchManager extends ApiManager { @@ -48,8 +44,10 @@ export class SearchManager extends ApiManager { res.send([]); return; } - const results = await findInFiles.find({ 'term': q, 'flags': 'ig' }, pathToDirectory(Directory.text), ".txt$"); const resObj: { ids: string[], numFound: number, lines: string[] } = { ids: [], numFound: 0, lines: [] }; + let results: any; + const dir = pathToDirectory(Directory.text); + results = await findInFiles.find({ 'term': q, 'flags': 'ig' }, dir, ".txt$"); for (const result in results) { resObj.ids.push(path.basename(result, ".txt").replace(/upload_/, "")); resObj.lines.push(results[result].line); diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts index 9b518749c..3f903a861 100644 --- a/src/server/DashUploadUtils.ts +++ b/src/server/DashUploadUtils.ts @@ -100,7 +100,7 @@ export namespace DashUploadUtils { const writeStream = createWriteStream(serverPathToFile(Directory.text, textFilename)); writeStream.write(result.text, error => error ? reject(error) : resolve()); }); - return MoveParsedFile(file, Directory.pdfs); + return MoveParsedFile(file, Directory.pdfs, undefined, result.text); } const manualSuffixes = [".webm"]; @@ -237,7 +237,7 @@ export namespace DashUploadUtils { * @param suffix If the file doesn't have a suffix and you want to provide it one * to appear in the new location */ - export async function MoveParsedFile(file: File, destination: Directory, suffix: string | undefined = undefined): Promise<Upload.FileResponse> { + export async function MoveParsedFile(file: File, destination: Directory, suffix: string | undefined = undefined, text?: string): Promise<Upload.FileResponse> { const { path: sourcePath } = file; let name = path.basename(sourcePath); suffix && (name += suffix); @@ -249,7 +249,8 @@ export namespace DashUploadUtils { result: error ? error : { accessPaths: { agnostic: getAccessPaths(destination, name) - } + }, + rawText: text } }); }); diff --git a/src/server/SharedMediaTypes.ts b/src/server/SharedMediaTypes.ts index 2495123b7..0f788f6c5 100644 --- a/src/server/SharedMediaTypes.ts +++ b/src/server/SharedMediaTypes.ts @@ -21,6 +21,7 @@ export namespace Upload { export interface FileInformation { accessPaths: AccessPathInfo; + rawText?: string; } export type FileResponse<T extends FileInformation = FileInformation> = { source: File, result: T | Error }; diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index dccbecdca..4cddbb347 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -3,7 +3,7 @@ import * as rp from 'request-promise'; import { DocServer } from "../../../client/DocServer"; import { Docs, DocumentOptions } from "../../../client/documents/Documents"; import { UndoManager } from "../../../client/util/UndoManager"; -import { Doc, DocListCast } from "../../../new_fields/Doc"; +import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc"; import { List } from "../../../new_fields/List"; import { listSpec } from "../../../new_fields/Schema"; import { ScriptField, ComputedField } from "../../../new_fields/ScriptField"; @@ -18,6 +18,8 @@ import { makeTemplate } from "../../../client/util/DropConverter"; import { RichTextField } from "../../../new_fields/RichTextField"; import { PrefetchProxy } from "../../../new_fields/Proxy"; import { FormattedTextBox } from "../../../client/views/nodes/FormattedTextBox"; +import { MainView } from "../../../client/views/MainView"; +import { DocumentType } from "../../../client/documents/DocumentTypes"; export class CurrentUserUtils { private static curr_id: string; @@ -28,112 +30,237 @@ export class CurrentUserUtils { public static get MainDocId() { return this.mainDocId; } public static set MainDocId(id: string | undefined) { this.mainDocId = id; } @computed public static get UserDocument() { return Doc.UserDoc(); } - @computed public static get ActivePen() { return Doc.UserDoc().activePen instanceof Doc && (Doc.UserDoc().activePen as Doc).pen as Doc; } + @computed public static get ActivePen() { return Doc.UserDoc().activePen instanceof Doc && (Doc.UserDoc().activePen as Doc).inkPen as Doc; } @observable public static GuestTarget: Doc | undefined; @observable public static GuestWorkspace: Doc | undefined; @observable public static GuestMobile: Doc | undefined; - static setupDefaultDocTemplates(doc: Doc, buttons?: string[]) { - const taskStatusValues = [{ title: "todo", _backgroundColor: "blue", color: "white" }, - { title: "in progress", _backgroundColor: "yellow", color: "black" }, - { title: "completed", _backgroundColor: "green", color: "white" } - ]; - const noteTemplates = [ - Docs.Create.TextDocument("", { title: "text", style: "Note", isTemplateDoc: true, backgroundColor: "yellow" }), - Docs.Create.TextDocument("", { title: "text", style: "Idea", isTemplateDoc: true, backgroundColor: "pink" }), - Docs.Create.TextDocument("", { title: "text", style: "Topic", isTemplateDoc: true, backgroundColor: "lightBlue" }), - Docs.Create.TextDocument("", { title: "text", style: "Person", isTemplateDoc: true, backgroundColor: "lightGreen" }), - Docs.Create.TextDocument("", { - title: "text", style: "Todo", isTemplateDoc: true, backgroundColor: "orange", _autoHeight: false, - layout: FormattedTextBox.LayoutString("Todo"), _height: 100, _showCaption: "caption", caption: RichTextField.DashField("taskStatus") - }) - ]; - doc.fieldTypes = Docs.Create.TreeDocument([], { title: "field enumerations" }); - Doc.addFieldEnumerations(Doc.GetProto(noteTemplates[4]), "taskStatus", taskStatusValues); - doc.noteTypes = new PrefetchProxy(Docs.Create.TreeDocument(noteTemplates.map(nt => makeTemplate(nt, true, StrCast(nt.style)) ? nt : nt), { title: "Note Layouts", _height: 75 })); + // sets up the default User Templates - slideView, queryView, descriptionView + static setupUserTemplateButtons(doc: Doc) { + if (doc["template-button-query"] === undefined) { + const queryTemplate = Docs.Create.MulticolumnDocument( + [ + Docs.Create.QueryDocument({ title: "query", _height: 200 }), + Docs.Create.FreeformDocument([], { title: "data", _height: 100, _LODdisable: true }) + ], + { _width: 400, _height: 300, title: "queryView", _chromeStatus: "disabled", _xMargin: 3, _yMargin: 3, hideFilterView: true } + ); + queryTemplate.isTemplateDoc = makeTemplate(queryTemplate); + doc["template-button-query"] = CurrentUserUtils.ficon({ + onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'), + dragFactory: new PrefetchProxy(queryTemplate) as any as Doc, + removeDropProperties: new List<string>(["dropAction"]), title: "query view", icon: "question-circle" + }); + } + + if (doc["template-button-slides"] === undefined) { + const slideTemplate = Docs.Create.MultirowDocument( + [ + Docs.Create.MulticolumnDocument([], { title: "data", _height: 200 }), + Docs.Create.TextDocument("", { title: "text", _height: 100 }) + ], + { _width: 400, _height: 300, title: "slideView", _chromeStatus: "disabled", _xMargin: 3, _yMargin: 3, hideFilterView: true } + ); + slideTemplate.isTemplateDoc = makeTemplate(slideTemplate); + doc["template-button-slides"] = CurrentUserUtils.ficon({ + onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'), + dragFactory: new PrefetchProxy(slideTemplate) as any as Doc, + removeDropProperties: new List<string>(["dropAction"]), title: "presentation slide", icon: "address-card" + }); + } + + if (doc["template-button-description"] === undefined) { + const descriptionTemplate = Docs.Create.TextDocument("", { title: "text", _height: 100, _showTitle: "title" }); + Doc.GetProto(descriptionTemplate).layout = FormattedTextBox.LayoutString("description"); + descriptionTemplate.isTemplateDoc = makeTemplate(descriptionTemplate, true, "descriptionView"); + + doc["template-button-description"] = CurrentUserUtils.ficon({ + onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'), + dragFactory: new PrefetchProxy(descriptionTemplate) as any as Doc, + removeDropProperties: new List<string>(["dropAction"]), title: "description view", icon: "window-maximize" + }); + } + + if (doc["template-buttons"] === undefined) { + doc["template-buttons"] = new PrefetchProxy(Docs.Create.MasonryDocument([doc["template-button-slides"] as Doc, doc["template-button-description"] as Doc, doc["template-button-query"] as Doc], { + title: "Compound Item Creators", _xMargin: 0, _showTitle: "title", + _autoHeight: true, _width: 500, columnWidth: 35, ignoreClick: true, lockedPosition: true, _chromeStatus: "disabled", + dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), + })); + } else { + DocListCast(Cast(doc["template-buttons"], Doc, null)?.data); // prefetch templates + } + return doc["template-buttons"] as Doc; } - static setupDefaultIconTypes(doc: Doc, buttons?: string[]) { - doc.iconView = new PrefetchProxy(Docs.Create.TextDocument("", { title: "icon", _width: 150, _height: 30, isTemplateDoc: true, onClick: ScriptField.MakeScript("deiconifyView(this)") })); - Doc.GetProto(doc.iconView as any as Doc).icon = new RichTextField('{"doc":{"type":"doc","content":[{"type":"paragraph","attrs":{"align":null,"color":null,"id":null,"indent":null,"inset":null,"lineSpacing":null,"paddingBottom":null,"paddingTop":null},"content":[{"type":"dashField","attrs":{"fieldKey":"title","docid":""}}]}]},"selection":{"type":"text","anchor":2,"head":2},"storedMarks":[]}', ""); - doc.isTemplateDoc = makeTemplate(doc.iconView as any as Doc); - doc.iconImageView = new PrefetchProxy(Docs.Create.ImageDocument("http://www.cs.brown.edu/~bcz/face.gif", { title: "data", _width: 50, isTemplateDoc: true, onClick: ScriptField.MakeScript("deiconifyView(self)") })); - doc.isTemplateDoc = makeTemplate(doc.iconImageView as any as Doc, true, "image_icon"); - doc.iconColView = new PrefetchProxy(Docs.Create.TreeDocument([], { title: "data", _width: 180, _height: 80, isTemplateDoc: true, onClick: ScriptField.MakeScript("deiconifyView(self)") })); - doc.isTemplateDoc = makeTemplate(doc.iconColView as any as Doc, true, "collection_icon"); - doc.iconViews = Docs.Create.TreeDocument([doc.iconView as any as Doc, doc.iconImageView as any as Doc, doc.iconColView as any as Doc], { title: "icon types", _height: 75 }); + + // setup the different note type skins + static setupNoteTemplates(doc: Doc) { + if (doc.noteTypes === undefined) { + const taskStatusValues = [ + { title: "todo", _backgroundColor: "blue", color: "white" }, + { title: "in progress", _backgroundColor: "yellow", color: "black" }, + { title: "completed", _backgroundColor: "green", color: "white" } + ]; + const noteTemplates = [ + Docs.Create.TextDocument("", { title: "text", style: "Note", isTemplateDoc: true, backgroundColor: "yellow" }), + Docs.Create.TextDocument("", { title: "text", style: "Idea", isTemplateDoc: true, backgroundColor: "pink" }), + Docs.Create.TextDocument("", { title: "text", style: "Topic", isTemplateDoc: true, backgroundColor: "lightBlue" }), + Docs.Create.TextDocument("", { title: "text", style: "Person", isTemplateDoc: true, backgroundColor: "lightGreen" }), + Docs.Create.TextDocument("", { + title: "text", style: "Todo", isTemplateDoc: true, backgroundColor: "orange", _autoHeight: false, _height: 100, _showCaption: "caption", + layout: FormattedTextBox.LayoutString("Todo"), caption: RichTextField.DashField("taskStatus") + }) + ]; + doc.fieldTypes = Docs.Create.TreeDocument([], { title: "field enumerations" }); + Doc.addFieldEnumerations(Doc.GetProto(noteTemplates[4]), "taskStatus", taskStatusValues); + doc.noteTypes = new PrefetchProxy(Docs.Create.TreeDocument(noteTemplates.map(nt => makeTemplate(nt, true, StrCast(nt.style)) ? nt : nt), + { title: "Note Layouts", _height: 75 })); + } else { + DocListCast(Cast(doc.noteTypes, Doc, null)?.data); // prefetch templates + } + + return doc.noteTypes as Doc; } - // setup the "creator" buttons for the sidebar-- eg. the default set of draggable document creation tools - static setupCreatorButtons(doc: Doc, alreadyCreatedButtons?: string[]) { - const emptyPresentation = Docs.Create.PresDocument(new List<Doc>(), { title: "Presentation", _viewType: CollectionViewType.Stacking, _LODdisable: true, _chromeStatus: "replaced", _showTitle: "title", boxShadow: "0 0" }); - const emptyCollection = Docs.Create.FreeformDocument([], { _nativeWidth: undefined, _nativeHeight: undefined, _LODdisable: true, _width: 150, _height: 100, title: "freeform" }); - doc.activePen = doc; - const docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, click?: string, ischecked?: string, activePen?: Doc, backgroundColor?: string, dragFactory?: Doc }[] = [ - { title: "comparison box", icon: "cat", ignoreClick: true, drag: 'Docs.Create.ComparisonDocument()' }, - { title: "collection", icon: "folder", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: emptyCollection }, - { title: "preview", icon: "expand", ignoreClick: true, drag: 'Docs.Create.DocumentDocument(ComputedField.MakeFunction("selectedDocs(this,this.excludeCollections,[_last_])?.[0]"), { _width: 250, _height: 250, title: "container" })' }, - { title: "web page", icon: "globe-asia", ignoreClick: true, drag: 'Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", {_width: 300, _height: 300, title: "New Webpage" })' }, - { title: "cat image", icon: "cat", ignoreClick: true, drag: 'Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { _width: 200, title: "an image of a cat" })' }, - { title: "screenshot", icon: "photo-video", ignoreClick: true, drag: 'Docs.Create.ScreenshotDocument("", { _width: 400, _height: 200, title: "screen snapshot" })' }, - { title: "webcam", icon: "video", ignoreClick: true, drag: 'Docs.Create.WebCamDocument("", { _width: 400, _height: 400, title: "a test cam" })' }, - { title: "record", icon: "microphone", ignoreClick: true, drag: `Docs.Create.AudioDocument("${nullAudio}", { _width: 200, title: "ready to record audio" })` }, - { title: "clickable button", icon: "bolt", ignoreClick: true, drag: 'Docs.Create.ButtonDocument({ _width: 150, _height: 50, title: "Button" })' }, - { title: "presentation", icon: "tv", click: 'openOnRight(Doc.UserDoc().curPresentation = getCopy(this.dragFactory, true))', drag: `Doc.UserDoc().curPresentation = getCopy(this.dragFactory,true)`, dragFactory: emptyPresentation }, - { title: "script", icon: "terminal", ignoreClick: true, drag: 'Docs.Create.ScriptingDocument(undefined, { _width: 200, _height: 250 title: "untitled script" })' }, - { title: "import folder", icon: "cloud-upload-alt", ignoreClick: true, drag: 'Docs.Create.DirectoryImportDocument({ title: "Directory Import", _width: 400, _height: 400 })' }, - { title: "mobile view", icon: "phone", ignoreClick: true, drag: 'Doc.UserDoc().activeMobile' }, - { title: "use pen", icon: "pen-nib", click: 'activatePen(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,2, this.backgroundColor)', backgroundColor: "blue", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc }, - { title: "use highlighter", icon: "highlighter", click: 'activateBrush(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,20,this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc }, - { title: "use stamp", icon: "stamp", click: 'activateStamp(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this)', backgroundColor: "orange", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc }, - { title: "use eraser", icon: "eraser", click: 'activateEraser(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this);', ischecked: `sameDocs(this.activePen.pen, this)`, backgroundColor: "pink", activePen: doc }, - { title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activePen.pen = this;', ischecked: `sameDocs(this.activePen.pen, this)`, backgroundColor: "white", activePen: doc }, - { title: "query", icon: "bolt", ignoreClick: true, drag: 'Docs.Create.QueryDocument({ _width: 200, title: "an image of a cat" })' }, - // { title: "buxton", icon: "cloud-upload-alt", ignoreClick: true, drag: "Docs.Create.Buxton()" }, + // creates Note templates, and initial "user" templates + static setupDocTemplates(doc: Doc) { + const noteTemplates = CurrentUserUtils.setupNoteTemplates(doc); + const userTemplateBtns = CurrentUserUtils.setupUserTemplateButtons(doc); + const clickTemplates = CurrentUserUtils.setupClickEditorTemplates(doc); + if (doc.templateDocs === undefined) { + doc.templateDocs = new PrefetchProxy(Docs.Create.TreeDocument([noteTemplates, userTemplateBtns, clickTemplates], { + title: "template layouts", _xPadding: 0, + dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }) + })); + } + } + + // setup templates for different document types when they are iconified from Document Decorations + static setupDefaultIconTemplates(doc: Doc) { + if (doc["template-icon-view"] === undefined) { + const iconView = Docs.Create.TextDocument("", { + title: "icon", _width: 150, _height: 30, isTemplateDoc: true, + onClick: ScriptField.MakeScript("deiconifyView(self)") + }); + Doc.GetProto(iconView).icon = new RichTextField('{"doc":{"type":"doc","content":[{"type":"paragraph","attrs":{"align":null,"color":null,"id":null,"indent":null,"inset":null,"lineSpacing":null,"paddingBottom":null,"paddingTop":null},"content":[{"type":"dashField","attrs":{"fieldKey":"title","docid":""}}]}]},"selection":{"type":"text","anchor":2,"head":2},"storedMarks":[]}', ""); + iconView.isTemplateDoc = makeTemplate(iconView); + doc["template-icon-view"] = new PrefetchProxy(iconView); + } + if (doc["template-icon-view-rtf"] === undefined) { + const iconRtfView = Docs.Create.LabelDocument({ title: "icon_" + DocumentType.RTF, textTransform: "unset", letterSpacing: "unset", _width: 150, _height: 30, isTemplateDoc: true, onClick: ScriptField.MakeScript("deiconifyView(self)") }); + iconRtfView.isTemplateDoc = makeTemplate(iconRtfView, true, "icon_" + DocumentType.RTF); + doc["template-icon-view-rtf"] = new PrefetchProxy(iconRtfView); + } + if (doc["template-icon-view-img"] === undefined) { + const iconImageView = Docs.Create.ImageDocument("http://www.cs.brown.edu/~bcz/face.gif", { + title: "data", _width: 50, isTemplateDoc: true, + onClick: ScriptField.MakeScript("deiconifyView(self)") + }); + iconImageView.isTemplateDoc = makeTemplate(iconImageView, true, "icon_" + DocumentType.IMG); + doc["template-icon-view-img"] = new PrefetchProxy(iconImageView); + } + if (doc["template-icon-view-col"] === undefined) { + const iconColView = Docs.Create.TreeDocument([], { title: "data", _width: 180, _height: 80, onClick: ScriptField.MakeScript("deiconifyView(self)") }); + iconColView.isTemplateDoc = makeTemplate(iconColView, true, "icon_" + DocumentType.COL); + doc["template-icon-view-col"] = new PrefetchProxy(iconColView); + } + if (doc["template-icons"] === undefined) { + doc["template-icons"] = new PrefetchProxy(Docs.Create.TreeDocument([doc["template-icon-view"] as Doc, doc["template-icon-view-img"] as Doc, + doc["template-icon-view-col"] as Doc, doc["template-icon-view-rtf"] as Doc], { title: "icon templates", _height: 75 })); + } else { + const templateIconsDoc = Cast(doc["template-icons"], Doc, null); + DocListCastAsync(templateIconsDoc).then(list => templateIconsDoc.data = new List<Doc>([doc["template-icon-view"] as Doc, doc["template-icon-view-img"] as Doc, + doc["template-icon-view-col"] as Doc, doc["template-icon-view-rtf"] as Doc])); + } + return doc["template-icons"] as Doc; + } + + static creatorBtnDescriptors(doc: Doc): { + title: string, label: string, icon: string, drag?: string, ignoreClick?: boolean, + click?: string, ischecked?: string, activePen?: Doc, backgroundColor?: string, dragFactory?: Doc + }[] { + if (doc.emptyPresentation === undefined) { + doc.emptyPresentation = Docs.Create.PresDocument(new List<Doc>(), + { title: "Presentation", _viewType: CollectionViewType.Stacking, _LODdisable: true, _chromeStatus: "replaced", _showTitle: "title", boxShadow: "0 0" }); + } + if (doc.emptyCollection === undefined) { + doc.emptyCollection = Docs.Create.FreeformDocument([], + { _nativeWidth: undefined, _nativeHeight: undefined, _LODdisable: true, _width: 150, _height: 100, title: "freeform" }); + } + return [ + { title: "Drag a comparison box", label: "Comparison", icon: "cat", ignoreClick: true, drag: 'Docs.Create.ComparisonDocument()' }, + { title: "Drag a collection", label: "Col", icon: "folder", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyCollection as Doc }, + { title: "Drag a web page", label: "Web", icon: "globe-asia", ignoreClick: true, drag: 'Docs.Create.WebDocument("", { title: "New Webpage" })' }, + { title: "Drag a cat image", label: "Img", icon: "cat", ignoreClick: true, drag: 'Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { _width: 250, _nativeWidth:250, title: "an image of a cat" })' }, + { title: "Drag a screenshot", label: "Grab", icon: "photo-video", ignoreClick: true, drag: 'Docs.Create.ScreenshotDocument("", { _width: 400, _height: 200, title: "screen snapshot" })' }, + { title: "Drag a webcam", label: "Cam", icon: "video", ignoreClick: true, drag: 'Docs.Create.WebCamDocument("", { _width: 400, _height: 400, title: "a test cam" })' }, + { title: "Drag a audio recorder", label: "Audio", icon: "microphone", ignoreClick: true, drag: `Docs.Create.AudioDocument("${nullAudio}", { _width: 200, title: "ready to record audio" })` }, + { title: "Drag a clickable button", label: "Btn", icon: "bolt", ignoreClick: true, drag: 'Docs.Create.ButtonDocument({ _width: 150, _height: 50, title: "Button" })' }, + { title: "Drag a presentation view", label: "Prezi", icon: "tv", click: 'openOnRight(Doc.UserDoc().activePresentation = getCopy(this.dragFactory, true))', drag: `Doc.UserDoc().activePresentation = getCopy(this.dragFactory,true)`, dragFactory: doc.emptyPresentation as Doc }, + { title: "Drag a search box", label: "Query", icon: "search", ignoreClick: true, drag: 'Docs.Create.QueryDocument({ _width: 200, title: "an image of a cat" })' }, + { title: "Drag a scripting box", label: "Script", icon: "terminal", ignoreClick: true, drag: 'Docs.Create.ScriptingDocument(undefined, { _width: 200, _height: 250 title: "untitled script" })' }, + { title: "Drag an import folder", label: "Load", icon: "cloud-upload-alt", ignoreClick: true, drag: 'Docs.Create.DirectoryImportDocument({ title: "Directory Import", _width: 400, _height: 400 })' }, + { title: "Drag a mobile view", label: "Phone", icon: "phone", ignoreClick: true, drag: 'Doc.UserDoc().activeMobile' }, + { title: "Drag an instance of the device collection", label: "Buxton", icon: "globe-asia", ignoreClick: true, drag: 'Docs.Create.Buxton()' }, + // { title: "use pen", icon: "pen-nib", click: 'activatePen(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this,2, this.backgroundColor)', backgroundColor: "blue", ischecked: `sameDocs(this.activePen.inkPen, this)`, activePen: doc }, + // { title: "use highlighter", icon: "highlighter", click: 'activateBrush(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this,20,this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activePen.inkPen, this)`, activePen: doc }, + // { title: "use stamp", icon: "stamp", click: 'activateStamp(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this)', backgroundColor: "orange", ischecked: `sameDocs(this.activePen.inkPen, this)`, activePen: doc }, + // { title: "use eraser", icon: "eraser", click: 'activateEraser(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this);', ischecked: `sameDocs(this.activePen.inkPen, this)`, backgroundColor: "pink", activePen: doc }, + // { title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activePen.inkPen = this;', ischecked: `sameDocs(this.activePen.inkPen, this)`, backgroundColor: "white", activePen: doc }, + { title: "Drag a document previewer", label: "Prev", icon: "expand", ignoreClick: true, drag: 'Docs.Create.DocumentDocument(ComputedField.MakeFunction("selectedDocs(this,this.excludeCollections,[_last_])?.[0]"), { _width: 250, _height: 250, title: "container" })' }, + { title: "Drag a Calculator REPL", label: "repl", icon: "calculator", click: 'addOverlayWindow("ScriptingRepl", { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" })' }, ]; - return docProtoData.filter(d => !alreadyCreatedButtons?.includes(d.title)).map(data => Docs.Create.FontIconDocument({ - _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, - icon: data.icon, - title: data.title, - ignoreClick: data.ignoreClick, - dropAction: data.click ? "copy" : undefined, - onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined, - onClick: data.click ? ScriptField.MakeScript(data.click) : undefined, - ischecked: data.ischecked ? ComputedField.MakeFunction(data.ischecked) : undefined, - activePen: data.activePen, - backgroundColor: data.backgroundColor, removeDropProperties: new List<string>(["dropAction"]), - dragFactory: data.dragFactory, - })); + } - static async updateCreatorButtons(doc: Doc) { - const toolsBtn = await Cast(doc.ToolsBtn, Doc); - if (toolsBtn) { - const stackingDoc = await Cast(toolsBtn.sourcePanel, Doc); - if (stackingDoc) { - const stackdocs = await Cast(stackingDoc.data, listSpec(Doc)); - if (stackdocs) { - const dragset = await Cast(stackdocs[0], Doc); - if (dragset) { - const dragdocs = await Cast(dragset.data, listSpec(Doc)); - if (dragdocs) { - const dragDocs = await Promise.all(dragdocs); - this.setupCreatorButtons(doc, dragDocs.map(d => StrCast(d.title))).map(nb => Doc.AddDocToList(dragset, "data", nb)); - } - } - } + // setup the "creator" buttons for the sidebar-- eg. the default set of draggable document creation tools + static async setupCreatorButtons(doc: Doc) { + let alreadyCreatedButtons: string[] = []; + const dragCreatorSet = await Cast(doc.myItemCreators, Doc, null); + if (dragCreatorSet) { + const dragCreators = await Cast(dragCreatorSet.data, listSpec(Doc)); + if (dragCreators) { + const dragDocs = await Promise.all(dragCreators); + alreadyCreatedButtons = dragDocs.map(d => StrCast(d.title)); } } + const creatorBtns = CurrentUserUtils.creatorBtnDescriptors(doc).filter(d => !alreadyCreatedButtons?.includes(d.title)). + map(data => Docs.Create.FontIconDocument({ + _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, + icon: data.icon, + title: data.title, + label: data.label, + ignoreClick: data.ignoreClick, + dropAction: data.click ? "copy" : undefined, + onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined, + onClick: data.click ? ScriptField.MakeScript(data.click) : undefined, + ischecked: data.ischecked ? ComputedField.MakeFunction(data.ischecked) : undefined, + activePen: data.activePen, + backgroundColor: data.backgroundColor, removeDropProperties: new List<string>(["dropAction"]), + dragFactory: data.dragFactory, + })); + + if (dragCreatorSet === undefined) { + doc.myItemCreators = new PrefetchProxy(Docs.Create.MasonryDocument(creatorBtns, { + title: "Basic Item Creators", _showTitle: "title", _xMargin: 0, + _autoHeight: true, _width: 500, columnWidth: 35, ignoreClick: true, lockedPosition: true, _chromeStatus: "disabled", + dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), + })); + } else { + creatorBtns.forEach(nb => Doc.AddDocToList(doc.myItemCreators as Doc, "data", nb)); + } + return doc.myItemCreators as Doc; } static setupMobileButtons(doc: Doc, buttons?: string[]) { const docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, click?: string, ischecked?: string, activePen?: Doc, backgroundColor?: string, dragFactory?: Doc }[] = [ { title: "record", icon: "microphone", ignoreClick: true, click: "FILL" }, - { title: "use pen", icon: "pen-nib", click: 'activatePen(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,2, this.backgroundColor)', backgroundColor: "blue", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc }, - { title: "use highlighter", icon: "highlighter", click: 'activateBrush(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,20,this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc }, - { title: "use eraser", icon: "eraser", click: 'activateEraser(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this);', ischecked: `sameDocs(this.activePen.pen, this)`, backgroundColor: "pink", activePen: doc }, - { title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activePen.pen = this;', ischecked: `sameDocs(this.activePen.pen, this)`, backgroundColor: "white", activePen: doc }, - // { title: "draw", icon: "pen-nib", click: 'switchMobileView(setupMobileInkingDoc, renderMobileInking, onSwitchMobileInking);', ischecked: `sameDocs(this.activePen.pen, this)`, backgroundColor: "red", activePen: doc }, + { title: "use pen", icon: "pen-nib", click: 'activatePen(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this,2, this.backgroundColor)', backgroundColor: "blue", ischecked: `sameDocs(this.activePen.inkPen, this)`, activePen: doc }, + { title: "use highlighter", icon: "highlighter", click: 'activateBrush(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this,20,this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activePen.inkPen, this)`, activePen: doc }, + { title: "use eraser", icon: "eraser", click: 'activateEraser(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this);', ischecked: `sameDocs(this.activePen.inkPen, this)`, backgroundColor: "pink", activePen: doc }, + { title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activePen.inkPen = this;', ischecked: `sameDocs(this.activePen.inkPen, this)`, backgroundColor: "white", activePen: doc }, + // { title: "draw", icon: "pen-nib", click: 'switchMobileView(setupMobileInkingDoc, renderMobileInking, onSwitchMobileInking);', ischecked: `sameDocs(this.activePen.inkPen, this)`, backgroundColor: "red", activePen: doc }, { title: "upload", icon: "upload", click: 'switchMobileView(setupMobileUploadDoc, renderMobileUpload, onSwitchMobileUpload);', backgroundColor: "orange" }, // { title: "upload", icon: "upload", click: 'uploadImageMobile();', backgroundColor: "cyan" }, ]; @@ -147,11 +274,11 @@ export class CurrentUserUtils { static setupThumbButtons(doc: Doc) { const docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, pointerDown?: string, pointerUp?: string, ischecked?: string, clipboard?: Doc, activePen?: Doc, backgroundColor?: string, dragFactory?: Doc }[] = [ - { title: "use pen", icon: "pen-nib", pointerUp: "resetPen()", pointerDown: 'setPen(2, this.backgroundColor)', backgroundColor: "blue", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc }, - { title: "use highlighter", icon: "highlighter", pointerUp: "resetPen()", pointerDown: 'setPen(20, this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc }, - { title: "notepad", icon: "clipboard", pointerUp: "GestureOverlay.Instance.closeFloatingDoc()", pointerDown: 'GestureOverlay.Instance.openFloatingDoc(this.clipboard)', clipboard: Docs.Create.FreeformDocument([], { _width: 300, _height: 300 }), backgroundColor: "orange", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc }, - { title: "interpret text", icon: "font", pointerUp: "setToolglass('none')", pointerDown: "setToolglass('inktotext')", backgroundColor: "orange", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc }, - { title: "ignore gestures", icon: "signature", pointerUp: "setToolglass('none')", pointerDown: "setToolglass('ignoregesture')", backgroundColor: "green", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc }, + { title: "use pen", icon: "pen-nib", pointerUp: "resetPen()", pointerDown: 'setPen(2, this.backgroundColor)', backgroundColor: "blue", ischecked: `sameDocs(this.activePen.inkPen, this)`, activePen: doc }, + { title: "use highlighter", icon: "highlighter", pointerUp: "resetPen()", pointerDown: 'setPen(20, this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activePen.inkPen, this)`, activePen: doc }, + { title: "notepad", icon: "clipboard", pointerUp: "GestureOverlay.Instance.closeFloatingDoc()", pointerDown: 'GestureOverlay.Instance.openFloatingDoc(this.clipboard)', clipboard: Docs.Create.FreeformDocument([], { _width: 300, _height: 300 }), backgroundColor: "orange", ischecked: `sameDocs(this.activePen.inkPen, this)`, activePen: doc }, + { title: "interpret text", icon: "font", pointerUp: "setToolglass('none')", pointerDown: "setToolglass('inktotext')", backgroundColor: "orange", ischecked: `sameDocs(this.activePen.inkPen, this)`, activePen: doc }, + { title: "ignore gestures", icon: "signature", pointerUp: "setToolglass('none')", pointerDown: "setToolglass('ignoregesture')", backgroundColor: "green", ischecked: `sameDocs(this.activePen.inkPen, this)`, activePen: doc }, ]; return docProtoData.map(data => Docs.Create.FontIconDocument({ _nativeWidth: 10, _nativeHeight: 10, _width: 10, _height: 10, title: data.title, icon: data.icon, @@ -201,200 +328,244 @@ export class CurrentUserUtils { }); } - // setup the Creator button which will display the creator panel. This panel will include the drag creators and the color picker. when clicked, this panel will be displayed in the target container (ie, sidebarContainer) - static setupToolsPanel(sidebarContainer: Doc, doc: Doc) { + // setup the Creator button which will display the creator panel. This panel will include the drag creators and the color picker. + // when clicked, this panel will be displayed in the target container (ie, sidebarContainer) + static async setupToolsBtnPanel(doc: Doc, sidebarContainer: Doc) { // setup a masonry view of all he creators - const dragCreators = Docs.Create.MasonryDocument(CurrentUserUtils.setupCreatorButtons(doc), { - _width: 500, _autoHeight: true, columnWidth: 35, ignoreClick: true, lockedPosition: true, _chromeStatus: "disabled", title: "buttons", - dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), _yMargin: 5 - }); + const creatorBtns = await CurrentUserUtils.setupCreatorButtons(doc); + const templateBtns = CurrentUserUtils.setupUserTemplateButtons(doc); + + if (doc.myCreators === undefined) { + doc.myCreators = new PrefetchProxy(Docs.Create.StackingDocument([creatorBtns, templateBtns], { + title: "all Creators", _yMargin: 0, _autoHeight: true, _xMargin: 0, + _width: 500, ignoreClick: true, lockedPosition: true, _chromeStatus: "disabled", + })); + } // setup a color picker - const color = Docs.Create.ColorDocument({ - title: "color picker", _width: 300, dropAction: "alias", forceActive: true, removeDropProperties: new List<string>(["dropAction", "forceActive"]) - }); + if (doc.myColorPicker === undefined) { + const color = Docs.Create.ColorDocument({ + title: "color picker", _width: 300, dropAction: "alias", forceActive: true, removeDropProperties: new List<string>(["dropAction", "forceActive"]) + }); + doc.myColorPicker = new PrefetchProxy(color); + } - return Docs.Create.ButtonDocument({ - _width: 35, _height: 25, title: "Tools", fontSize: 10, targetContainer: sidebarContainer, - letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)", - sourcePanel: Docs.Create.StackingDocument([dragCreators, color], { - _width: 500, lockedPosition: true, _chromeStatus: "disabled", title: "tools stack", forceActive: true - }), - onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel"), - }); + if (doc["tabs-button-tools"] === undefined) { + doc["tabs-button-tools"] = new PrefetchProxy(Docs.Create.ButtonDocument({ + _width: 35, _height: 25, title: "Tools", fontSize: 10, + letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)", + sourcePanel: new PrefetchProxy(Docs.Create.StackingDocument([doc.myCreators as Doc, doc.myColorPicker as Doc], { + _width: 500, lockedPosition: true, _chromeStatus: "disabled", title: "tools stack", forceActive: true + })) as any as Doc, + targetContainer: new PrefetchProxy(sidebarContainer) as any as Doc, + onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel"), + })); + } + (doc["tabs-button-tools"] as Doc).sourcePanel; // prefetch sourcePanel + return doc["tabs-button-tools"] as Doc; } - // setup the Library button which will display the library panel. This panel includes a collection of workspaces, documents, and recently closed views - static setupLibraryPanel(sidebarContainer: Doc, doc: Doc) { + static setupWorkspaces(doc: Doc) { // setup workspaces library item - doc.workspaces = Docs.Create.TreeDocument([], { - title: "WORKSPACES", _height: 100, forceActive: true, boxShadow: "0 0", lockedPosition: true, - }); - - doc.documents = Docs.Create.TreeDocument([], { - title: "DOCUMENTS", _height: 42, forceActive: true, boxShadow: "0 0", treeViewPreventOpen: true, lockedPosition: true, - }); + if (doc.myWorkspaces === undefined) { + doc.myWorkspaces = new PrefetchProxy(Docs.Create.TreeDocument([], { + title: "WORKSPACES", _height: 100, forceActive: true, boxShadow: "0 0", lockedPosition: true, + })); + } + const newWorkspace = ScriptField.MakeScript(`createNewWorkspace()`); + (doc.myWorkspaces as Doc).contextMenuScripts = new List<ScriptField>([newWorkspace!]); + (doc.myWorkspaces as Doc).contextMenuLabels = new List<string>(["Create New Workspace"]); + return doc.myWorkspaces as Doc; + } + static setupDocumentCollection(doc: Doc) { + if (doc.myDocuments === undefined) { + doc.myDocuments = new PrefetchProxy(Docs.Create.TreeDocument([], { + title: "DOCUMENTS", _height: 42, forceActive: true, boxShadow: "0 0", treeViewPreventOpen: true, lockedPosition: true, + })); + } + return doc.myDocuments as Doc; + } + static setupRecentlyClosed(doc: Doc) { // setup Recently Closed library item - doc.recentlyClosed = Docs.Create.TreeDocument([], { - title: "RECENTLY CLOSED", _height: 75, forceActive: true, boxShadow: "0 0", treeViewPreventOpen: true, lockedPosition: true, - }); + if (doc.myRecentlyClosed === undefined) { + doc.myRecentlyClosed = new PrefetchProxy(Docs.Create.TreeDocument([], { + title: "RECENTLY CLOSED", _height: 75, forceActive: true, boxShadow: "0 0", treeViewPreventOpen: true, lockedPosition: true, + })); + } + // this is equivalent to using PrefetchProxies to make sure the recentlyClosed doc is ready + PromiseValue(Cast(doc.myRecentlyClosed, Doc)).then(recent => recent && PromiseValue(recent.data).then(DocListCast)); + const clearAll = ScriptField.MakeScript(`self.data = new List([])`); + (doc.myRecentlyClosed as Doc).contextMenuScripts = new List<ScriptField>([clearAll!]); + (doc.myRecentlyClosed as Doc).contextMenuLabels = new List<string>(["Clear All"]); - return Docs.Create.ButtonDocument({ - _width: 50, _height: 25, title: "Library", fontSize: 10, - letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)", - sourcePanel: Docs.Create.TreeDocument([doc.workspaces as Doc, doc.documents as Doc, Docs.Prototypes.MainLinkDocument(), doc, doc.recentlyClosed as Doc], { - title: "Library", _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "place", lockedPosition: true, boxShadow: "0 0", dontRegisterChildren: true - }), - targetContainer: sidebarContainer, - onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel;") - }); + return doc.myRecentlyClosed as Doc; + } + // setup the Library button which will display the library panel. This panel includes a collection of workspaces, documents, and recently closed views + static setupLibraryPanel(doc: Doc, sidebarContainer: Doc) { + const workspaces = CurrentUserUtils.setupWorkspaces(doc); + const documents = CurrentUserUtils.setupDocumentCollection(doc); + const recentlyClosed = CurrentUserUtils.setupRecentlyClosed(doc); + + if (doc["tabs-button-library"] === undefined) { + doc["tabs-button-library"] = new PrefetchProxy(Docs.Create.ButtonDocument({ + _width: 50, _height: 25, title: "Library", fontSize: 10, + letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)", + sourcePanel: new PrefetchProxy(Docs.Create.TreeDocument([workspaces, documents, recentlyClosed, doc], { + title: "Library", _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "move", lockedPosition: true, boxShadow: "0 0", dontRegisterChildren: true + })) as any as Doc, + targetContainer: new PrefetchProxy(sidebarContainer) as any as Doc, + onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel;") + })); + } + return doc["tabs-button-library"] as Doc; } // setup the Search button which will display the search panel. - static setupSearchPanel(sidebarContainer: Doc) { - return Docs.Create.ButtonDocument({ - _width: 50, _height: 25, title: "Search", fontSize: 10, - letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)", - sourcePanel: Docs.Create.QueryDocument({ title: "search stack", }), - targetContainer: sidebarContainer, - lockedPosition: true, - onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel") - }); + static setupSearchBtnPanel(doc: Doc, sidebarContainer: Doc) { + if (doc["tabs-button-search"] === undefined) { + doc["tabs-button-search"] = new PrefetchProxy(Docs.Create.ButtonDocument({ + _width: 50, _height: 25, title: "Search", fontSize: 10, + letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)", + sourcePanel: new PrefetchProxy(Docs.Create.QueryDocument({ title: "search stack", })) as any as Doc, + searchFileTypes: new List<string>([DocumentType.RTF, DocumentType.IMG, DocumentType.PDF, DocumentType.VID, DocumentType.WEB, DocumentType.SCRIPTING]), + targetContainer: new PrefetchProxy(sidebarContainer) as any as Doc, + lockedPosition: true, + onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel") + })); + } + return doc["tabs-button-search"] as Doc; } - // setup the list of sidebar mode buttons which determine what is displayed in the sidebar - static setupSidebarButtons(doc: Doc) { - const sidebarContainer = new Doc(); - doc.sidebarContainer = new PrefetchProxy(sidebarContainer); - sidebarContainer._chromeStatus = "disabled"; - sidebarContainer.onClick = ScriptField.MakeScript("freezeSidebar()"); + static setupSidebarContainer(doc: Doc) { + if (doc["tabs-panelContainer"] === undefined) { + const sidebarContainer = new Doc(); + sidebarContainer._chromeStatus = "disabled"; + sidebarContainer.onClick = ScriptField.MakeScript("freezeSidebar()"); + doc["tabs-panelContainer"] = new PrefetchProxy(sidebarContainer); + } + return doc["tabs-panelContainer"] as Doc; + } - doc.ToolsBtn = new PrefetchProxy(this.setupToolsPanel(sidebarContainer, doc)); - doc.LibraryBtn = new PrefetchProxy(this.setupLibraryPanel(sidebarContainer, doc)); - doc.SearchBtn = new PrefetchProxy(this.setupSearchPanel(sidebarContainer)); + // setup the list of sidebar mode buttons which determine what is displayed in the sidebar + static async setupSidebarButtons(doc: Doc) { + const sidebarContainer = CurrentUserUtils.setupSidebarContainer(doc); + const toolsBtn = await CurrentUserUtils.setupToolsBtnPanel(doc, sidebarContainer); + const libraryBtn = CurrentUserUtils.setupLibraryPanel(doc, sidebarContainer); + const searchBtn = CurrentUserUtils.setupSearchBtnPanel(doc, sidebarContainer); // Finally, setup the list of buttons to display in the sidebar - doc.sidebarButtons = new PrefetchProxy(Docs.Create.StackingDocument([doc.SearchBtn as any as Doc, doc.LibraryBtn as any as Doc, doc.ToolsBtn as any as Doc], { - _width: 500, _height: 80, boxShadow: "0 0", _pivotField: "title", hideHeadings: true, ignoreClick: true, _chromeStatus: "view-mode", - title: "sidebar btn row stack", backgroundColor: "dimGray", - })); + if (doc["tabs-buttons"] === undefined) { + doc["tabs-buttons"] = new PrefetchProxy(Docs.Create.StackingDocument([searchBtn, libraryBtn, toolsBtn], { + _width: 500, _height: 80, boxShadow: "0 0", _pivotField: "title", hideHeadings: true, ignoreClick: true, _chromeStatus: "view-mode", + title: "sidebar btn row stack", backgroundColor: "dimGray", + })); + (toolsBtn.onClick as ScriptField).script.run({ this: toolsBtn }); + } } + static blist = (opts: DocumentOptions, docs: Doc[]) => new PrefetchProxy(Docs.Create.LinearDocument(docs, { + ...opts, + _gridGap: 5, _xMargin: 5, _yMargin: 5, _height: 42, _width: 100, boxShadow: "0 0", forceActive: true, + dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), + backgroundColor: "black", treeViewPreventOpen: true, lockedPosition: true, _chromeStatus: "disabled", linearViewIsExpanded: true + })) as any as Doc + + static ficon = (opts: DocumentOptions) => new PrefetchProxy(Docs.Create.FontIconDocument({ + ...opts, + dropAction: "alias", removeDropProperties: new List<string>(["dropAction"]), _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100 + })) as any as Doc + /// sets up the default list of buttons to be shown in the expanding button menu at the bottom of the Dash window - static setupExpandingButtons(doc: Doc) { - const queryTemplate = Docs.Create.MulticolumnDocument( - [ - Docs.Create.QueryDocument({ title: "query", _height: 200, forceActive: true }), - Docs.Create.FreeformDocument([], { title: "data", _height: 100, _LODdisable: true, forceActive: true }) - ], - { _width: 400, _height: 300, title: "queryView", _chromeStatus: "disabled", _xMargin: 3, _yMargin: 3, _autoHeight: false, forceActive: true, hideFilterView: true }); - queryTemplate.isTemplateDoc = makeTemplate(queryTemplate); - const slideTemplate = Docs.Create.MultirowDocument( - [ - Docs.Create.MulticolumnDocument([], { title: "data", _height: 200, forceActive: true }), - Docs.Create.TextDocument("", { title: "text", _height: 100, forceActive: true }) - ], - { _width: 400, _height: 300, title: "slideView", _chromeStatus: "disabled", _xMargin: 3, _yMargin: 3, _autoHeight: false, forceActive: true, hideFilterView: true }); - slideTemplate.isTemplateDoc = makeTemplate(slideTemplate); - const descriptionTemplate = Docs.Create.TextDocument("", { title: "text", _height: 100, _showTitle: "title" }); - Doc.GetProto(descriptionTemplate).layout = FormattedTextBox.LayoutString("description"); - descriptionTemplate.isTemplateDoc = makeTemplate(descriptionTemplate, true, "descriptionView"); - - const ficon = (opts: DocumentOptions) => new PrefetchProxy(Docs.Create.FontIconDocument({ - ...opts, - dropAction: "alias", removeDropProperties: new List<string>(["dropAction"]), _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100 - })) as any as Doc; - const blist = (opts: DocumentOptions, docs: Doc[]) => new PrefetchProxy(Docs.Create.LinearDocument(docs, { - ...opts, - _gridGap: 5, _xMargin: 5, _yMargin: 5, _height: 42, _width: 100, boxShadow: "0 0", forceActive: true, - dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), - backgroundColor: "black", treeViewPreventOpen: true, lockedPosition: true, _chromeStatus: "disabled", linearViewIsExpanded: true - })) as any as Doc; - - doc.undoBtn = ficon({ onClick: ScriptField.MakeScript("undo()"), title: "undo button", icon: "undo-alt" }); - doc.redoBtn = ficon({ onClick: ScriptField.MakeScript("redo()"), title: "redo button", icon: "redo-alt" }); - doc.slidesBtn = ficon({ onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'), dragFactory: slideTemplate, removeDropProperties: new List<string>(["dropAction"]), title: "presentation slide", icon: "sticky-note" }); - doc.descriptionBtn = ficon({ onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'), dragFactory: descriptionTemplate, removeDropProperties: new List<string>(["dropAction"]), title: "description view", icon: "sticky-note" }); - doc.queryBtn = ficon({ onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'), dragFactory: queryTemplate, removeDropProperties: new List<string>(["dropAction"]), title: "query view", icon: "sticky-note" }); - doc.templateButtons = blist({ title: "template buttons", ignoreClick: true }, [doc.slidesBtn as Doc, doc.descriptionBtn as Doc, doc.queryBtn as Doc]); - doc.expandingButtons = blist({ title: "expanding buttons", ignoreClick: true }, [doc.undoBtn as Doc, doc.redoBtn as Doc, doc.templateButtons as Doc]); - doc.templateDocs = new PrefetchProxy(Docs.Create.TreeDocument([doc.noteTypes as Doc, doc.templateButtons as Doc, doc.clickFuncs as Doc], { - title: "template layouts", _xPadding: 0, - dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }) - })); + static setupDockedButtons(doc: Doc) { + if (doc["dockedBtn-pen"] === undefined) { + doc["dockedBtn-pen"] = CurrentUserUtils.ficon({ + onClick: ScriptField.MakeScript("activatePen(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this,2, this.backgroundColor)"), + author: "systemTemplates", title: "ink mode", icon: "pen-nib", ischecked: ComputedField.MakeFunction(`sameDocs(this.activePen.inkPen, this)`), activePen: doc + }); + } + if (doc["dockedBtn-undo"] === undefined) { + doc["dockedBtn-undo"] = CurrentUserUtils.ficon({ onClick: ScriptField.MakeScript("undo()"), title: "undo button", icon: "undo-alt" }); + } + if (doc["dockedBtn-redo"] === undefined) { + doc["dockedBtn-redo"] = CurrentUserUtils.ficon({ onClick: ScriptField.MakeScript("redo()"), title: "redo button", icon: "redo-alt" }); + } + if (doc.dockedBtns === undefined) { + doc.dockedBtns = CurrentUserUtils.blist({ title: "docked buttons", ignoreClick: true }, [doc["dockedBtn-undo"] as Doc, doc["dockedBtn-redo"] as Doc, doc["dockedBtn-pen"] as Doc]); + } } - // sets up the default set of documents to be shown in the Overlay layer static setupOverlays(doc: Doc) { - doc.overlays = new PrefetchProxy(Docs.Create.FreeformDocument([], { title: "Overlays", backgroundColor: "#aca3a6" })); + if (doc.myOverlayDocuments === undefined) { + doc.myOverlayDocuments = new PrefetchProxy(Docs.Create.FreeformDocument([], { title: "overlay documents", backgroundColor: "#aca3a6" })); + } } // the initial presentation Doc to use static setupDefaultPresentation(doc: Doc) { - doc.presentationTemplate = new PrefetchProxy(Docs.Create.PresElementBoxDocument({ title: "pres element template", backgroundColor: "transparent", _xMargin: 5, _height: 46, isTemplateDoc: true, isTemplateForField: "data" })); - doc.curPresentation = Docs.Create.PresDocument(new List<Doc>(), { title: "Presentation", _viewType: CollectionViewType.Stacking, _LODdisable: true, _chromeStatus: "replaced", _showTitle: "title", boxShadow: "0 0" }); + if (doc["template-presentation"] === undefined) { + doc["template-presentation"] = new PrefetchProxy(Docs.Create.PresElementBoxDocument({ + title: "pres element template", backgroundColor: "transparent", _xMargin: 5, _height: 46, isTemplateDoc: true, isTemplateForField: "data" + })); + } + if (doc.activePresentation === undefined) { + doc.activePresentation = Docs.Create.PresDocument(new List<Doc>(), { + title: "Presentation", _viewType: CollectionViewType.Stacking, + _LODdisable: true, _chromeStatus: "replaced", _showTitle: "title", boxShadow: "0 0" + }); + } } - static setupMobileUploads(doc: Doc) { - doc.optionalRightCollection = new PrefetchProxy(Docs.Create.StackingDocument([], { title: "New mobile uploads" })); + static setupRightSidebar(doc: Doc) { + if (doc.rightSidebarCollection === undefined) { + doc.rightSidebarCollection = new PrefetchProxy(Docs.Create.StackingDocument([], { title: "Right Sidebar" })); + } } - static setupChildClicks(doc: Doc) { - const openInTarget = Docs.Create.ScriptingDocument(ScriptField.MakeScript( - "docCast(thisContainer.target).then((target) => { target && docCast(this.source).then((source) => { target.proto.data = new List([source || this]); } ); } )", - { target: Doc.name }), { title: "On Child Clicked (open in target)", _width: 300, _height: 200 }); - const onClick = Docs.Create.ScriptingDocument(ScriptField.MakeScript("console.log('click')"), { title: "onClick", isTemplateDoc: true, isTemplateForField: "onClick", _width: 300, _height: 200 }, "onClick"); - doc.childClickFuncs = Docs.Create.TreeDocument([openInTarget], { title: "on Child Click function templates" }); - doc.clickFuncs = Docs.Create.TreeDocument([onClick], { title: "onClick funcs" }); - } - - static updateUserDocument(doc: Doc) { - doc.title = Doc.CurrentUserEmail; - new InkingControl(); - (doc.iconTypes === undefined) && CurrentUserUtils.setupDefaultIconTypes(doc); - (doc.noteTypes === undefined) && CurrentUserUtils.setupDefaultDocTemplates(doc); - (doc.childClickFuncs === undefined) && CurrentUserUtils.setupChildClicks(doc); - (doc.optionalRightCollection === undefined) && CurrentUserUtils.setupMobileUploads(doc); - (doc.overlays === undefined) && CurrentUserUtils.setupOverlays(doc); - (doc.expandingButtons === undefined) && CurrentUserUtils.setupExpandingButtons(doc); - (doc.curPresentation === undefined) && CurrentUserUtils.setupDefaultPresentation(doc); - (doc.sidebarButtons === undefined) && CurrentUserUtils.setupSidebarButtons(doc); + static setupClickEditorTemplates(doc: Doc) { + if (doc.childClickFuncs === undefined) { + const openInTarget = Docs.Create.ScriptingDocument(ScriptField.MakeScript( + "docCast(thisContainer.target).then((target) => { target && docCast(this.source).then((source) => { target.proto.data = new List([source || this]); } ); } )", + { target: Doc.name }), { title: "On Child Clicked (open in target)", _width: 300, _height: 200 }); + doc.childClickFuncs = Docs.Create.TreeDocument([openInTarget], { title: "on Child Click function templates" }); + } // this is equivalent to using PrefetchProxies to make sure all the childClickFuncs have been retrieved. PromiseValue(Cast(doc.childClickFuncs, Doc)).then(func => func && PromiseValue(func.data).then(DocListCast)); - // this is equivalent to using PrefetchProxies to make sure the recentlyClosed doc is ready - PromiseValue(Cast(doc.recentlyClosed, Doc)).then(recent => recent && PromiseValue(recent.data).then(DocListCast)); - // this is equivalent to using PrefetchProxies to make sure all the sidebarButtons and noteType internal Doc's have been retrieved. - PromiseValue(Cast(doc.noteTypes, Doc)).then(noteTypes => noteTypes && PromiseValue(noteTypes.data).then(DocListCast)); + + if (doc.clickFuncs === undefined) { + const onClick = Docs.Create.ScriptingDocument(undefined, { + title: "onClick", "onClick-rawScript": "console.log('click')", + isTemplateDoc: true, isTemplateForField: "onClick", _width: 300, _height: 200 + }, "onClick"); + const onCheckedClick = Docs.Create.ScriptingDocument(undefined, { + title: "onCheckedClick", "onCheckedClick-rawScript": "console.log(heading + checked + containingTreeView)", "onCheckedClick-params": new List<string>(["heading", "checked", "containingTreeView"]), isTemplateDoc: true, isTemplateForField: "onCheckedClick", _width: 300, _height: 200 + }, "onCheckedClick"); + doc.clickFuncs = Docs.Create.TreeDocument([onClick, onCheckedClick], { title: "onClick funcs" }); + } PromiseValue(Cast(doc.clickFuncs, Doc)).then(func => func && PromiseValue(func.data).then(DocListCast)); - PromiseValue(Cast(doc.childClickFuncs, Doc)).then(func => func && PromiseValue(func.data).then(DocListCast)); - PromiseValue(Cast(doc.sidebarButtons, Doc)).then(stackingDoc => { - stackingDoc && PromiseValue(Cast(stackingDoc.data, listSpec(Doc))).then(sidebarButtons => { - sidebarButtons && sidebarButtons.map((sidebarBtn, i) => { - sidebarBtn && PromiseValue(Cast(sidebarBtn, Doc)).then(async btn => { - btn && btn.sourcePanel && btn.targetContainer && i === 1 && (btn.onClick as ScriptField).script.run({ this: btn }); - }); - }); - }); - }); + + return doc.clickFuncs as Doc; + } + + static async updateUserDocument(doc: Doc) { + new InkingControl(); + doc.title = Doc.CurrentUserEmail; + doc.activePen = doc; + this.setupDefaultIconTemplates(doc); // creates a set of icon templates triggered by the document deoration icon + this.setupDocTemplates(doc); // sets up the template menu of templates + this.setupRightSidebar(doc); // sets up the right sidebar collection for mobile upload documents and sharing + this.setupOverlays(doc); // documents in overlay layer + this.setupDockedButtons(doc); // the bottom bar of font icons + this.setupDefaultPresentation(doc); // presentation that's initially triggered + await this.setupSidebarButtons(doc); // the pop-out left sidebar of tools/panels + doc.globalLinkDatabase = Docs.Prototypes.MainLinkDocument(); // setup reactions to change the highlights on the undo/redo buttons -- would be better to encode this in the undo/redo buttons, but the undo/redo stacks are not wired up that way yet - doc.undoBtn && reaction(() => UndoManager.undoStack.slice(), () => Doc.GetProto(doc.undoBtn as Doc).opacity = UndoManager.CanUndo() ? 1 : 0.4, { fireImmediately: true }); - doc.redoBtn && reaction(() => UndoManager.redoStack.slice(), () => Doc.GetProto(doc.redoBtn as Doc).opacity = UndoManager.CanRedo() ? 1 : 0.4, { fireImmediately: true }); + doc["dockedBtn-undo"] && reaction(() => UndoManager.undoStack.slice(), () => Doc.GetProto(doc["dockedBtn-undo"] as Doc).opacity = UndoManager.CanUndo() ? 1 : 0.4, { fireImmediately: true }); + doc["dockedBtn-redo"] && reaction(() => UndoManager.redoStack.slice(), () => Doc.GetProto(doc["dockedBtn-redo"] as Doc).opacity = UndoManager.CanRedo() ? 1 : 0.4, { fireImmediately: true }); - this.updateCreatorButtons(doc); return doc; } - - public static IsDocPinned(doc: Doc) { - //add this new doc to props.Document - const curPres = Cast(CurrentUserUtils.UserDocument.curPresentation, Doc) as Doc; - if (curPres) { - return DocListCast(curPres.data).findIndex((val) => Doc.AreProtosEqual(val, doc)) !== -1; - } - return false; - } - public static async loadCurrentUser() { return rp.get(Utils.prepend("/getCurrentUser")).then(response => { if (response) { @@ -422,3 +593,4 @@ export class CurrentUserUtils { Scripting.addGlobal(function setupMobileInkingDoc(userDoc: Doc) { return CurrentUserUtils.setupMobileInkingDoc(userDoc); }); Scripting.addGlobal(function setupMobileUploadDoc(userDoc: Doc) { return CurrentUserUtils.setupMobileUploadDoc(userDoc); }); +Scripting.addGlobal(function createNewWorkspace() { return MainView.Instance.createNewWorkspace(); });
\ No newline at end of file diff --git a/src/server/database.ts b/src/server/database.ts index a46531641..ad285765b 100644 --- a/src/server/database.ts +++ b/src/server/database.ts @@ -14,7 +14,7 @@ export namespace Database { export let disconnect: Function; const schema = 'Dash'; const port = 27017; - export const url = `mongodb://localhost:${port}/${schema}`; + export const url = `mongodb://localhost:${port}/`; enum ConnectionStates { disconnected = 0, @@ -35,7 +35,7 @@ export namespace Database { console.log(`mongoose established default connection at ${url}`); resolve(); }); - mongoose.connect(url, { useNewUrlParser: true }); + mongoose.connect(url, { useNewUrlParser: true, useUnifiedTopology: true, dbName: schema }); }); } } catch (e) { @@ -46,17 +46,20 @@ export namespace Database { } } - class Database implements IDatabase { + export class Database implements IDatabase { public static DocumentsCollection = 'documents'; private MongoClient = mongodb.MongoClient; private currentWrites: { [id: string]: Promise<void> } = {}; private db?: mongodb.Db; private onConnect: (() => void)[] = []; - constructor() { - this.MongoClient.connect(url, { connectTimeoutMS: 30000, socketTimeoutMS: 30000 }, (_err, client) => { + doConnect() { + console.error(`\nConnecting to Mongo with URL : ${url}\n`); + this.MongoClient.connect(url, { connectTimeoutMS: 30000, socketTimeoutMS: 30000, useUnifiedTopology: true }, (_err, client) => { + console.error("mongo connect response\n"); if (!client) { - console.error("\nPlease start MongoDB by running 'mongod' in a terminal before continuing...\n"); + console.error("\nMongo connect failed with the error:\n"); + console.log(_err); process.exit(0); } this.db = client.db(); @@ -65,6 +68,7 @@ export namespace Database { } public async update(id: string, value: any, callback: (err: mongodb.MongoError, res: mongodb.UpdateWriteOpResult) => void, upsert = true, collectionName = Database.DocumentsCollection) { + if (this.db) { const collection = this.db.collection(collectionName); const prom = this.currentWrites[id]; diff --git a/src/server/index.ts b/src/server/index.ts index 8b040c926..f26c8a6ab 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -106,12 +106,17 @@ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }: method: Method.GET, subscription: ["/home", new RouteSubscriber("doc").add("docId")], secureHandler: serve, - publicHandler: ({ req, ...remaining }) => { + publicHandler: ({ req, res, ...remaining }) => { const { originalUrl: target } = req; const sharing = qs.parse(qs.extract(req.originalUrl), { sort: false }).sharing === "true"; const docAccess = target.startsWith("/doc/"); + // since this is the public handler, there's no meaning of '/home' to speak of + // since there's no user logged in, so the only viable operation + // for a guest is to look at a shared document if (sharing && docAccess) { - serve({ req, ...remaining }); + serve({ req, res, ...remaining }); + } else { + res.redirect("/login"); } } }); @@ -148,5 +153,6 @@ export async function launchServer() { if (process.env.RELEASE) { (sessionAgent = new DashSessionAgent()).launch(); } else { + (Database.Instance as Database.Database).doConnect(); launchServer(); } |