diff options
23 files changed, 839 insertions, 349 deletions
diff --git a/src/.DS_Store b/src/.DS_Store Binary files differindex bdc161da2..04487bc70 100644 --- a/src/.DS_Store +++ b/src/.DS_Store diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index df573a377..12b57351d 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -235,7 +235,7 @@ export class DocumentOptions { docColorBtn?: string; userColorBtn?: string; canClick?: string; - script?: string; + script?: ScriptField; numBtnType?: string; numBtnMax?: number; numBtnMin?: number; @@ -361,31 +361,34 @@ export namespace Docs { const TemplateMap: TemplateMap = new Map([ [DocumentType.RTF, { layout: { view: FormattedTextBox, dataField: "text" }, - options: { _height: 150, _xMargin: 10, _yMargin: 10, nativeDimModifiable: true, nativeHeightUnfrozen: true, links: ComputedField.MakeFunction("links(self)") as any } + options: { + _height: 150, _xMargin: 10, _yMargin: 10, nativeDimModifiable: true, nativeHeightUnfrozen: true, + links: "@links(self)" + } }], [DocumentType.SEARCH, { layout: { view: SearchBox, dataField: defaultDataKey }, - options: { _width: 400, links: ComputedField.MakeFunction("links(self)") as any } + options: { _width: 400, links: "@links(self)" } }], [DocumentType.FILTER, { layout: { view: FilterBox, dataField: defaultDataKey }, - options: { _width: 400, links: ComputedField.MakeFunction("links(self)") as any } + options: { _width: 400, links: "@links(self)" } }], [DocumentType.COLOR, { layout: { view: ColorBox, dataField: defaultDataKey }, - options: { _nativeWidth: 220, _nativeHeight: 300, links: ComputedField.MakeFunction("links(self)") as any } + options: { _nativeWidth: 220, _nativeHeight: 300, links: "@links(self)" } }], [DocumentType.IMG, { layout: { view: ImageBox, dataField: defaultDataKey }, - options: { links: ComputedField.MakeFunction("links(self)") as any } + options: { links: "@links(self)" } }], [DocumentType.WEB, { layout: { view: WebBox, dataField: defaultDataKey }, - options: { _height: 300, _fitWidth: true, nativeDimModifiable: true, nativeHeightUnfrozen: true, links: ComputedField.MakeFunction("links(self)") as any } + options: { _height: 300, _fitWidth: true, nativeDimModifiable: true, nativeHeightUnfrozen: true, links: "@links(self)" } }], [DocumentType.COL, { layout: { view: CollectionView, dataField: defaultDataKey }, - options: { _fitWidth: true, _panX: 0, _panY: 0, _viewScale: 1, links: ComputedField.MakeFunction("links(self)") as any } + options: { _fitWidth: true, _panX: 0, _panY: 0, _viewScale: 1, links: "@links(self)" } }], [DocumentType.KVP, { layout: { view: KeyValueBox, dataField: defaultDataKey }, @@ -393,19 +396,19 @@ export namespace Docs { }], [DocumentType.VID, { layout: { view: VideoBox, dataField: defaultDataKey }, - options: { _currentTimecode: 0, links: ComputedField.MakeFunction("links(self)") as any }, + options: { _currentTimecode: 0, links: "@links(self)" }, }], [DocumentType.AUDIO, { layout: { view: AudioBox, dataField: defaultDataKey }, - options: { _height: 100, backgroundColor: "lightGray", links: ComputedField.MakeFunction("links(self)") as any } + options: { _height: 100, backgroundColor: "lightGray", links: "@links(self)" } }], [DocumentType.PDF, { layout: { view: PDFBox, dataField: defaultDataKey }, - options: { _curPage: 1, _fitWidth: true, nativeDimModifiable: true, nativeHeightUnfrozen: true, links: ComputedField.MakeFunction("links(self)") as any } + options: { _curPage: 1, _fitWidth: true, nativeDimModifiable: true, nativeHeightUnfrozen: true, links: "@links(self)" } }], [DocumentType.MAP, { layout: { view: MapBox, dataField: defaultDataKey }, - options: { _height: 600, _width: 800, links: ComputedField.MakeFunction("links(self)") as any } + options: { _height: 600, _width: 800, links: "@links(self)" } }], [DocumentType.IMPORT, { layout: { view: DirectoryImportBox, dataField: defaultDataKey }, @@ -416,7 +419,7 @@ export namespace Docs { options: { childDontRegisterViews: true, _isLinkButton: true, _height: 150, description: "", showCaption: "description", backgroundColor: "lightblue", // lightblue is default color for linking dot and link documents text comment area - links: ComputedField.MakeFunction("links(self)") as any, + links: "@links(self)", _removeDropProperties: new List(["_layerTags", "isLinkButton"]), } }], @@ -432,61 +435,61 @@ export namespace Docs { }], [DocumentType.SCRIPTING, { layout: { view: ScriptingBox, dataField: defaultDataKey }, - options: { links: ComputedField.MakeFunction("links(self)") as any } + options: { links: "@links(self)" } }], [DocumentType.YOUTUBE, { layout: { view: YoutubeBox, dataField: defaultDataKey } }], [DocumentType.LABEL, { layout: { view: LabelBox, dataField: defaultDataKey }, - options: { links: ComputedField.MakeFunction("links(self)") as any } + options: { links: "@links(self)" } }], [DocumentType.EQUATION, { layout: { view: EquationBox, dataField: defaultDataKey }, - options: { links: ComputedField.MakeFunction("links(self)") as any, hideResizeHandles: true, hideDecorationTitle: true } + options: { links: "@links(self)", hideResizeHandles: true, hideDecorationTitle: true } }], [DocumentType.FUNCPLOT, { layout: { view: FunctionPlotBox, dataField: defaultDataKey }, - options: { links: ComputedField.MakeFunction("links(self)") as any } + options: { links: "@links(self)" } }], [DocumentType.BUTTON, { layout: { view: LabelBox, dataField: "onClick" }, - options: { links: ComputedField.MakeFunction("links(self)") as any } + options: { links: "@links(self)" } }], [DocumentType.SLIDER, { layout: { view: SliderBox, dataField: defaultDataKey }, - options: { links: ComputedField.MakeFunction("links(self)") as any } + options: { links: "@links(self)" } }], [DocumentType.PRES, { layout: { view: PresBox, dataField: defaultDataKey }, - options: { links: ComputedField.MakeFunction("links(self)") as any } + options: { links: "@links(self)" } }], [DocumentType.FONTICON, { layout: { view: FontIconBox, dataField: defaultDataKey }, - options: { hideLinkButton: true, _width: 40, _height: 40, borderRounding: "100%", links: ComputedField.MakeFunction("links(self)") as any }, + options: { hideLinkButton: true, _width: 40, _height: 40, borderRounding: "100%", links: "@links(self)" }, }], [DocumentType.WEBCAM, { layout: { view: DashWebRTCVideo, dataField: defaultDataKey }, - options: { links: ComputedField.MakeFunction("links(self)") as any } + options: { links: "@links(self)" } }], [DocumentType.PRESELEMENT, { layout: { view: PresElementBox, dataField: defaultDataKey } }], [DocumentType.MARKER, { layout: { view: CollectionView, dataField: defaultDataKey }, - options: { links: ComputedField.MakeFunction("links(self)") as any, hideLinkButton: true } + options: { links: "@links(self)", hideLinkButton: true } }], [DocumentType.INK, { // NOTE: this is unused!! ink fields are filled in directly within the InkDocument() method layout: { view: InkingStroke, dataField: defaultDataKey }, - options: { links: ComputedField.MakeFunction("links(self)") as any } + options: { links: "@links(self)" } }], [DocumentType.SCREENSHOT, { layout: { view: ScreenshotBox, dataField: defaultDataKey }, - options: { links: ComputedField.MakeFunction("links(self)") as any } + options: { links: "@links(self)" } }], [DocumentType.COMPARISON, { layout: { view: ComparisonBox, dataField: defaultDataKey }, - options: { clipWidth: 50, backgroundColor: "gray", targetDropAction: "alias", links: ComputedField.MakeFunction("links(self)") as any } + options: { clipWidth: 50, backgroundColor: "gray", targetDropAction: "alias", links: "@links(self)" } }], [DocumentType.GROUPDB, { data: new List<Doc>(), @@ -495,7 +498,7 @@ export namespace Docs { }], [DocumentType.GROUP, { layout: { view: EmptyBox, dataField: defaultDataKey }, - options: { links: ComputedField.MakeFunction("links(self)") as any } + options: { links: "@links(self)" } }], ]); @@ -592,6 +595,11 @@ export namespace Docs { system: true, _layoutKey: "layout", title, type, baseProto: true, x: 0, y: 0, _width: 300, ...(template.options || {}), layout: layout.view?.LayoutString(layout.dataField), data: template.data, layout_keyValue: KeyValueBox.LayoutString("") }; + Object.entries(options).map(pair => { + if (typeof pair[1] === "string" && pair[1].startsWith("@")) { + (options as any)[pair[0]] = ComputedField.MakeFunction(pair[1].substring(1)); + } + }); return Doc.assign(new Doc(prototypeId, true), options as any, undefined, true); } } @@ -758,7 +766,7 @@ export namespace Docs { I.data = new InkField(points); I["acl-Public"] = Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment; I["acl-Override"] = "None"; - I.links = ComputedField.MakeFunction("links(self)") as any; + I.links = "@links(self)"; I[Initializing] = false; return I; } diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index a8b0da369..fd4b01753 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -51,7 +51,6 @@ interface Button { numBtnMax?: number; switchToggle?: boolean; script?: string; - checkResult?: string; width?: number; list?: string[]; ignoreClick?: boolean; @@ -162,37 +161,37 @@ export class CurrentUserUtils { }); } - if (doc["template-button-switch"] === undefined) { - const { FreeformDocument, MulticolumnDocument, TextDocument } = Docs.Create; - - const yes = FreeformDocument([], { title: "yes", _height: 35, _width: 50, _dimUnit: DimUnit.Pixel, _dimMagnitude: 40, system: true }); - const name = TextDocument("name", { title: "name", _height: 35, _width: 70, _dimMagnitude: 1, system: true }); - const no = FreeformDocument([], { title: "no", _height: 100, _width: 100, system: true }); - const labelTemplate = { - doc: { - type: "doc", content: [{ - type: "paragraph", - content: [{ type: "dashField", attrs: { fieldKey: "PARAMS", hideKey: true } }] - }] - }, - selection: { type: "text", anchor: 1, head: 1 }, - storedMarks: [] - }; - Doc.GetProto(name).text = new RichTextField(JSON.stringify(labelTemplate), "PARAMS"); - Doc.GetProto(yes).backgroundColor = ComputedField.MakeFunction("self[this.PARAMS] ? 'green':'red'"); - // Doc.GetProto(no).backgroundColor = ComputedField.MakeFunction("!self[this.PARAMS] ? 'red':'white'"); - // Doc.GetProto(yes).onClick = ScriptField.MakeScript("self[this.PARAMS] = true"); - Doc.GetProto(yes).onClick = ScriptField.MakeScript("self[this.PARAMS] = !self[this.PARAMS]"); - // Doc.GetProto(no).onClick = ScriptField.MakeScript("self[this.PARAMS] = false"); - const box = MulticolumnDocument([/*no, */ yes, name], { title: "value", _width: 120, _height: 35, system: true }); - box.isTemplateDoc = makeTemplate(box, true, "switch"); - - doc["template-button-switch"] = CurrentUserUtils.createToolButton({ - onDragStart: ScriptField.MakeFunction('copyDragFactory(this.dragFactory)'), - dragFactory: new PrefetchProxy(box) as any as Doc, title: "data switch", icon: "toggle-on", system: true, - btnType: ButtonType.ToolButton - }); - } + // if (doc["template-button-switch"] === undefined) { + // const { FreeformDocument, MulticolumnDocument, TextDocument } = Docs.Create; + + // const yes = FreeformDocument([], { title: "yes", _height: 35, _width: 50, _dimUnit: DimUnit.Pixel, _dimMagnitude: 40, system: true }); + // const name = TextDocument("name", { title: "name", _height: 35, _width: 70, _dimMagnitude: 1, system: true }); + // const no = FreeformDocument([], { title: "no", _height: 100, _width: 100, system: true }); + // const labelTemplate = { + // doc: { + // type: "doc", content: [{ + // type: "paragraph", + // content: [{ type: "dashField", attrs: { fieldKey: "PARAMS", hideKey: true } }] + // }] + // }, + // selection: { type: "text", anchor: 1, head: 1 }, + // storedMarks: [] + // }; + // Doc.GetProto(name).text = new RichTextField(JSON.stringify(labelTemplate), "PARAMS"); + // Doc.GetProto(yes).backgroundColor = ComputedField.MakeFunction("self[this.PARAMS] ? 'green':'red'"); + // // Doc.GetProto(no).backgroundColor = ComputedField.MakeFunction("!self[this.PARAMS] ? 'red':'white'"); + // // Doc.GetProto(yes).onClick = ScriptField.MakeScript("self[this.PARAMS] = true"); + // Doc.GetProto(yes).onClick = ScriptField.MakeScript("self[this.PARAMS] = !self[this.PARAMS]"); + // // Doc.GetProto(no).onClick = ScriptField.MakeScript("self[this.PARAMS] = false"); + // const box = MulticolumnDocument([/*no, */ yes, name], { title: "value", _width: 120, _height: 35, system: true }); + // box.isTemplateDoc = makeTemplate(box, true, "switch"); + + // doc["template-button-switch"] = CurrentUserUtils.createToolButton({ + // onDragStart: ScriptField.MakeFunction('copyDragFactory(this.dragFactory)'), + // dragFactory: new PrefetchProxy(box) as any as Doc, title: "data switch", icon: "toggle-on", system: true, + // btnType: ButtonType.ToolButton + // }); + // } if (doc["template-button-detail"] === undefined) { const { TextDocument, MasonryDocument, CarouselDocument } = Docs.Create; @@ -735,12 +734,16 @@ export class CurrentUserUtils { ]; return docProtoData.map(data => Docs.Create.FontIconDocument({ _nativeWidth: 10, _nativeHeight: 10, _width: 10, _height: 10, title: data.title, icon: data.icon, - _dropAction: data.pointerDown ? "copy" : undefined, ignoreClick: data.ignoreClick, + _dropAction: data.pointerDown ? "copy" : undefined, + ignoreClick: data.ignoreClick, onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined, clipboard: data.clipboard, - onPointerUp: data.pointerUp ? ScriptField.MakeScript(data.pointerUp) : undefined, onPointerDown: data.pointerDown ? ScriptField.MakeScript(data.pointerDown) : undefined, + onPointerUp: data.pointerUp ? ScriptField.MakeScript(data.pointerUp) : undefined, + onPointerDown: data.pointerDown ? ScriptField.MakeScript(data.pointerDown) : undefined, backgroundColor: data.backgroundColor, - _removeDropProperties: new List<string>(["dropAction"]), dragFactory: data.dragFactory, system: true + _removeDropProperties: new List<string>(["dropAction"]), + dragFactory: data.dragFactory, + system: true })); } @@ -987,37 +990,37 @@ export class CurrentUserUtils { title: "Font", toolTip: "Font", width: 100, btnType: ButtonType.DropdownList, ignoreClick: true, list: ["Roboto", "Roboto Mono", "Nunito", "Times New Roman", "Arial", "Georgia", "Comic Sans MS", "Tahoma", "Impact", "Crimson Text"], - script: 'setFont' + script: 'setFont(value, _readOnly_)' }, - { title: "Font size", toolTip: "Font size", width: 75, btnType: ButtonType.NumberButton, numBtnMax: 200, numBtnMin: 0, numBtnType: NumButtonType.DropdownOptions, ignoreClick: true, script: 'setFontSize' }, - { title: "Font color", toolTip: "Font color", btnType: ButtonType.ColorButton, icon: "font", ignoreClick: true, script: 'setFontColor' }, - { title: "Bold", toolTip: "Bold (Ctrl+B)", btnType: ButtonType.ToggleButton, icon: "bold", click: 'toggleBold()', checkResult: 'toggleBold(true)' }, - { title: "Italic", toolTip: "Italic (Ctrl+I)", btnType: ButtonType.ToggleButton, icon: "italic", click: 'toggleItalic()', checkResult: 'toggleItalic(true)' }, - { title: "Underline", toolTip: "Underline (Ctrl+U)", btnType: ButtonType.ToggleButton, icon: "underline", click: 'toggleUnderline()', checkResult: 'toggleUnderline(true)' }, - { title: "Bullet List", toolTip: "Bullet", btnType: ButtonType.ToggleButton, icon: "list", click: 'setBulletList("bullet")', checkResult: 'setBulletList("bullet", true)' }, - { title: "Number List", toolTip: "Number", btnType: ButtonType.ToggleButton, icon: "list-ol", click: 'setBulletList("decimal")', checkResult: 'setBulletList("decimal", true)' }, + { title: "Font size", toolTip: "Font size", width: 75, btnType: ButtonType.NumberButton, numBtnMax: 200, numBtnMin: 0, numBtnType: NumButtonType.DropdownOptions, ignoreClick: true, script: 'setFontSize(value, _readOnly_)' }, + { title: "Font color", toolTip: "Font color", btnType: ButtonType.ColorButton, icon: "font", ignoreClick: true, script: 'setFontColor(value, _readOnly_)' }, + { title: "Bold", toolTip: "Bold (Ctrl+B)", btnType: ButtonType.ToggleButton, icon: "bold", click: 'toggleBold(_readOnly_)' }, + { title: "Italic", toolTip: "Italic (Ctrl+I)", btnType: ButtonType.ToggleButton, icon: "italic", click: 'toggleItalic(_readOnly_)' }, + { title: "Underline", toolTip: "Underline (Ctrl+U)", btnType: ButtonType.ToggleButton, icon: "underline", click: 'toggleUnderline(_readOnly_)' }, + { title: "Bullet List", toolTip: "Bullet", btnType: ButtonType.ToggleButton, icon: "list", click: 'setBulletList("bullet", _readOnly_)' }, + { title: "Number List", toolTip: "Number", btnType: ButtonType.ToggleButton, icon: "list-ol", click: 'setBulletList("decimal", _readOnly_)' }, // { title: "Strikethrough", tooltip: "Strikethrough", btnType: ButtonType.ToggleButton, icon: "strikethrough", click: 'toggleStrikethrough()'}, // { title: "Superscript", tooltip: "Superscript", btnType: ButtonType.ToggleButton, icon: "superscript", click: 'toggleSuperscript()'}, // { title: "Subscript", tooltip: "Subscript", btnType: ButtonType.ToggleButton, icon: "subscript", click: 'toggleSubscript()'}, - { title: "Left align", toolTip: "Left align", btnType: ButtonType.ToggleButton, icon: "align-left", click: 'setAlignment("left")', checkResult: 'setAlignment("left", true)' }, - { title: "Center align", toolTip: "Center align", btnType: ButtonType.ToggleButton, icon: "align-center", click: 'setAlignment("center")', checkResult: 'setAlignment("center", true)' }, - { title: "Right align", toolTip: "Right align", btnType: ButtonType.ToggleButton, icon: "align-right", click: 'setAlignment("right")', checkResult: 'setAlignment("right", true)' }, + { title: "Left align", toolTip: "Left align", btnType: ButtonType.ToggleButton, icon: "align-left", click: 'setAlignment("left", _readOnly_)' }, + { title: "Center align", toolTip: "Center align", btnType: ButtonType.ToggleButton, icon: "align-center", click: 'setAlignment("center", _readOnly_)' }, + { title: "Right align", toolTip: "Right align", btnType: ButtonType.ToggleButton, icon: "align-right", click: 'setAlignment("right", _readOnly_)' }, ]; return tools; } static inkTools(doc: Doc) { const tools: Button[] = [ - { title: "Pen", toolTip: "Pen (Ctrl+P)", btnType: ButtonType.ToggleButton, icon: "pen", click: 'setActiveInkTool("pen")', checkResult: 'setActiveInkTool("pen" , true)' }, - { title: "Eraser", toolTip: "Eraser (Ctrl+E)", btnType: ButtonType.ToggleButton, icon: "eraser", click: 'setActiveInkTool("eraser")', checkResult: 'setActiveInkTool("eraser" , true)' }, - // { title: "Highlighter", toolTip: "Highlighter (Ctrl+H)", btnType: ButtonType.ToggleButton, icon: "highlighter", click: 'setActiveInkTool("highlighter")', checkResult: 'setActiveInkTool("highlighter", true)' }, - { title: "Circle", toolTip: "Circle (Ctrl+Shift+C)", btnType: ButtonType.ToggleButton, icon: "circle", click: 'setActiveInkTool("circle")', checkResult: 'setActiveInkTool("circle" , true)' }, - // { title: "Square", toolTip: "Square (Ctrl+Shift+S)", btnType: ButtonType.ToggleButton, icon: "square", click: 'setActiveInkTool("square")', checkResult: 'setActiveInkTool("square" , true)' }, - { title: "Line", toolTip: "Line (Ctrl+Shift+L)", btnType: ButtonType.ToggleButton, icon: "minus", click: 'setActiveInkTool("line")', checkResult: 'setActiveInkTool("line" , true)' }, - { title: "Fill color", toolTip: "Fill color", btnType: ButtonType.ColorButton, ignoreClick: true, icon: "fill-drip", script: "setFillColor" }, - { title: "Stroke width", toolTip: "Stroke width", btnType: ButtonType.NumberButton, numBtnType: NumButtonType.Slider, numBtnMin: 1, ignoreClick: true, script: 'setStrokeWidth' }, - { title: "Stroke color", toolTip: "Stroke color", btnType: ButtonType.ColorButton, icon: "pen", ignoreClick: true, script: 'setStrokeColor' }, + { title: "Pen", toolTip: "Pen (Ctrl+P)", btnType: ButtonType.ToggleButton, icon: "pen", click: 'setActiveInkTool("pen", _readOnly_)' }, + { title: "Eraser", toolTip: "Eraser (Ctrl+E)", btnType: ButtonType.ToggleButton, icon: "eraser", click: 'setActiveInkTool("eraser", _readOnly_)' }, + // { title: "Highlighter", toolTip: "Highlighter (Ctrl+H)", btnType: ButtonType.ToggleButton, icon: "highlighter", click: 'setActiveInkTool("highlighter")' }, + { title: "Circle", toolTip: "Circle (Ctrl+Shift+C)", btnType: ButtonType.ToggleButton, icon: "circle", click: 'setActiveInkTool("circle", _readOnly_)' }, + // { title: "Square", toolTip: "Square (Ctrl+Shift+S)", btnType: ButtonType.ToggleButton, icon: "square", click: 'setActiveInkTool("square")' }, + { title: "Line", toolTip: "Line (Ctrl+Shift+L)", btnType: ButtonType.ToggleButton, icon: "minus", click: 'setActiveInkTool("line", _readOnly_)' }, + { title: "Fill color", toolTip: "Fill color", btnType: ButtonType.ColorButton, ignoreClick: true, icon: "fill-drip", script: "setFillColor(value, _readOnly_)" }, + { title: "Stroke width", toolTip: "Stroke width", btnType: ButtonType.NumberButton, numBtnType: NumButtonType.Slider, numBtnMin: 1, ignoreClick: true, script: 'setStrokeWidth(value, _readOnly_)' }, + { title: "Stroke color", toolTip: "Stroke color", btnType: ButtonType.ColorButton, icon: "pen", ignoreClick: true, script: 'setStrokeColor(value, _readOnly_)' }, ]; return tools; } @@ -1031,8 +1034,7 @@ export class CurrentUserUtils { btnType: ButtonType.ToggleButton, buttonText: "Show Preview", icon: "eye", - click: 'toggleSchemaPreview()', - checkResult: 'toggleSchemaPreview(true)' + click: 'toggleSchemaPreview(_readOnly_)', }, ]; return tools; @@ -1041,10 +1043,10 @@ export class CurrentUserUtils { static webTools(doc: Doc) { const tools: Button[] = [ - { title: "Back", toolTip: "Go back", btnType: ButtonType.ClickButton, icon: "arrow-left", click: 'webBack()' }, - { title: "Forward", toolTip: "Go forward", btnType: ButtonType.ClickButton, icon: "arrow-right", click: 'webForward()' }, + { title: "Back", toolTip: "Go back", btnType: ButtonType.ClickButton, icon: "arrow-left", click: 'webBack(_readOnly_)' }, + { title: "Forward", toolTip: "Go forward", btnType: ButtonType.ClickButton, icon: "arrow-right", click: 'webForward(_readOnly_)' }, //{ title: "Reload", toolTip: "Reload webpage", btnType: ButtonType.ClickButton, icon: "redo-alt", click: 'webReload()' }, - { title: "URL", toolTip: "URL", width: 250, btnType: ButtonType.EditableText, icon: "lock", ignoreClick: true, script: 'webSetURL' }, + { title: "URL", toolTip: "URL", width: 250, btnType: ButtonType.EditableText, icon: "lock", ignoreClick: true, script: 'webSetURL(value, _readOnly_)' }, ]; return tools; @@ -1059,17 +1061,17 @@ export class CurrentUserUtils { CollectionViewType.Multirow, CollectionViewType.Time, CollectionViewType.Carousel, CollectionViewType.Carousel3D, CollectionViewType.Linear, CollectionViewType.Map, CollectionViewType.Grid], - script: 'setView', + script: 'setView(value, _readOnly_)', }, // Always show { title: "Background Color", toolTip: "Background Color", btnType: ButtonType.ColorButton, ignoreClick: true, icon: "fill-drip", - script: "setBackgroundColor", hidden: 'selectedDocumentType()' + script: "setBackgroundColor(value, _readOnly_)", hidden: 'selectedDocumentType()' }, // Only when a document is selected { title: "Header Color", toolTip: "Header Color", btnType: ButtonType.ColorButton, ignoreClick: true, icon: "heading", - script: "setHeaderColor", hidden: 'selectedDocumentType()', + script: "setHeaderColor(value, _readOnly_)", hidden: 'selectedDocumentType()', }, // Only when a document is selected - { title: "Overlay", toolTip: "Overlay", btnType: ButtonType.ToggleButton, icon: "layer-group", click: 'toggleOverlay()', checkResult: 'toggleOverlay(true)', hidden: 'selectedDocumentType(undefined, "freeform", true)' }, // Only when floating document is selected in freeform + { title: "Overlay", toolTip: "Overlay", btnType: ButtonType.ToggleButton, icon: "layer-group", click: 'toggleOverlay(_readOnly_)', hidden: 'selectedDocumentType(undefined, "freeform", true)' }, // Only when floating document is selected in freeform // { title: "Alias", btnType: ButtonType.ClickButton, icon: "copy", hidden: 'selectedDocumentType()' }, // Only when a document is selected { title: "Text", type: "textTools", subMenu: true, expanded: 'selectedDocumentType("rtf")' }, // Always available { title: "Ink", type: "inkTools", subMenu: true, expanded: 'selectedDocumentType("ink")' }, // Always available @@ -1083,7 +1085,7 @@ export class CurrentUserUtils { if (doc.contextMenuBtns === undefined) { const docList: Doc[] = []; - (await CurrentUserUtils.contextMenuTools(doc)).map(({ title, width, list, toolTip, ignoreClick, icon, type, btnType, click, script, subMenu, hidden, expanded, checkResult }) => { + (await CurrentUserUtils.contextMenuTools(doc)).map(({ title, width, list, toolTip, ignoreClick, icon, type, btnType, click, script, subMenu, hidden, expanded }) => { const menuDocList: Doc[] = []; if (subMenu) { // default is textTools @@ -1105,7 +1107,8 @@ export class CurrentUserUtils { tools = CurrentUserUtils.textTools(doc); break; } - tools.map(({ title, toolTip, icon, btnType, numBtnType, numBtnMax, numBtnMin, click, script, width, list, ignoreClick, switchToggle, checkResult }) => { + tools.map(({ title, toolTip, icon, btnType, numBtnType, numBtnMax, numBtnMin, click, script, width, list, ignoreClick, switchToggle }) => { + const computed = click ? ComputedField.MakeFunction(click) as any : "transparent"; menuDocList.push(Docs.Create.FontIconDocument({ _nativeWidth: width ? width : 25, _nativeHeight: 25, @@ -1116,7 +1119,7 @@ export class CurrentUserUtils { numBtnType, numBtnMin, numBtnMax, - script, + script: script ? ScriptField.MakeScript(script, { value: "any" }) : undefined, btnType: btnType, btnList: new List<string>(list), ignoreClick: ignoreClick, @@ -1128,10 +1131,10 @@ export class CurrentUserUtils { title, switchToggle, color: Colors.WHITE, - backgroundColor: checkResult ? ComputedField.MakeFunction(checkResult) as any : "transparent", + backgroundColor: computed, _dropAction: "alias", _removeDropProperties: new List<string>(["dropAction", "_stayInCollection"]), - onClick: click ? ScriptField.MakeScript(click, { doc: Doc.name }) : undefined + onClick: click ? ScriptField.MakeScript(click) : undefined })); }); docList.push(CurrentUserUtils.linearButtonList({ @@ -1141,7 +1144,7 @@ export class CurrentUserUtils { linearViewExpandable: true, icon: title, _height: 30, - backgroundColor: checkResult ? ComputedField.MakeFunction(checkResult) as any : "transparent", + // backgroundColor: hidden ? ComputedField.MakeFunction(hidden, { }, { _readOnly_: true }) as any : "transparent", linearViewIsExpanded: expanded ? !(ComputedField.MakeFunction(expanded) as any) : undefined, hidden: hidden ? ComputedField.MakeFunction(hidden) as any : undefined, }, menuDocList)); @@ -1153,7 +1156,7 @@ export class CurrentUserUtils { _height: 25, icon, toolTip, - script, + script: script ? ScriptField.MakeScript(script, { value: "any" }) : undefined, btnType, btnList: new List<string>(list), ignoreClick, @@ -1164,11 +1167,11 @@ export class CurrentUserUtils { dontUndo: true, title, color: Colors.WHITE, - backgroundColor: checkResult ? ComputedField.MakeFunction(checkResult) as any : "transparent", + // backgroundColor: checkResult ? ComputedField.MakeFunction(checkResult, {}, {_readOnly_:true}) as any : "transparent", _dropAction: "alias", hidden: hidden ? ComputedField.MakeFunction(hidden) as any : undefined, _removeDropProperties: new List<string>(["dropAction", "_stayInCollection"]), - onClick: click ? ScriptField.MakeScript(click, { scriptContext: "any" }) : undefined + onClick: click ? ScriptField.MakeScript(click, { scriptContext: "any" }, { _readOnly_: false }) : undefined })); } }); diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx index f9dab9f82..dd6448654 100644 --- a/src/client/views/PropertiesButtons.tsx +++ b/src/client/views/PropertiesButtons.tsx @@ -18,6 +18,10 @@ import React = require("react"); import { Colors } from "./global/globalEnums"; import { CollectionFreeFormView } from "./collections/collectionFreeForm"; import { DocumentManager } from "../util/DocumentManager"; +import { pasteImageBitmap } from "./nodes/WebBoxRenderer"; +import { VideoBox } from "./nodes/VideoBox"; +import { Id } from "../../fields/FieldSymbols"; +import { ImageField } from "../../fields/URLField"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -92,6 +96,20 @@ export class PropertiesButtons extends React.Component<{}, {}> { @computed get groupButton() { return this.propertyToggleBtn("Group", "isGroup", on => `Display collection as a Group`, on => "object-group", (dv, doc) => { doc.isGroup = !doc.isGroup; doc.forceActive = doc.isGroup; }); } + @computed get freezeThumb() { + return this.propertyToggleBtn("Freeze\Thumb", "_thumb-frozen", on => `${on ? "Freeze" : "Unfreeze"} thumbnail`, on => "arrows-alt-h", (dv, doc) => { + if (doc["thumb-frozen"]) doc["thumb-frozen"] = undefined; + else { + document.body.focus(); // so that we can access the clipboard without an error + setTimeout(() => + pasteImageBitmap((data: any, error: any) => { + error && console.log(error); + data && VideoBox.convertDataUri(data, doc[Id] + "-thumb-frozen", true).then( + returnedfilename => doc["thumb-frozen"] = new ImageField(returnedfilename)); + })); + } + }); + } @computed get snapButton() { return this.propertyToggleBtn("Snap\xA0Lines", "showSnapLines", on => `Display snapping lines when objects are dragged`, on => "border-all", undefined, true); } @@ -222,6 +240,7 @@ export class PropertiesButtons extends React.Component<{}, {}> { {toggle(this.dictationButton, { display: isNovice ? "none" : "" })} {toggle(this.onClickButton)} {toggle(this.fitWidthButton)} + {toggle(this.freezeThumb)} {toggle(this.fitContentButton, { display: !isFreeForm && !isMap ? "none" : "" })} {toggle(this.autoHeightButton, { display: !isText && !isStacking && !isTree ? "none" : "" })} {toggle(this.maskButton, { display: !isInk ? "none" : "" })} diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 34209ebc9..1e700f250 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -33,7 +33,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: protected _mainCont?: HTMLDivElement; @observable _focusFilters: Opt<string[]>; // docFilters that are overridden when previewing a link to an anchor which has docFilters set on it @observable _focusRangeFilters: Opt<string[]>; // docRangeFilters that are overridden when previewing a link to an anchor which has docRangeFilters set on it - protected createDashEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view + protected createDashEventsTarget = (ele: HTMLDivElement | null) => { //used for stacking and masonry view this.dropDisposer?.(); this.gestureDisposer?.(); this._multiTouchDisposer?.(); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index aeda71d01..3443f33e1 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -109,6 +109,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @observable _viewTransition: number = 0; // sets the pan/zoom transform ease time- used by nudge(), focus() etc to smoothly zoom/pan. set to 0 to use document's transition time or default of 0 @observable _hLines: number[] | undefined; @observable _vLines: number[] | undefined; + @observable _firstRender = true; // this turns off rendering of the collection's content so that there's instant feedback when a tab is switched of what content will be shown. @observable _pullCoords: number[] = [0, 0]; @observable _pullDirection: string = ""; @observable _showAnimTimeline = false; @@ -163,7 +164,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P this.layoutDoc._panY = vals.bounds.cy; this.layoutDoc._viewScale = vals.scale; } - freeformData = (force?: boolean) => this.fitToContent || force ? this.fitToContentVals : undefined; + freeformData = (force?: boolean) => !this._firstRender && (this.fitToContent || force) ? this.fitToContentVals : undefined; reverseNativeScaling = () => this.fitToContent ? true : false; panX = () => this.freeformData()?.bounds.cx ?? NumCast(this.Document._panX); panY = () => this.freeformData()?.bounds.cy ?? NumCast(this.Document._panY); @@ -1144,7 +1145,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P isContentActive = () => this.props.isSelected() || this.props.isContentActive(); - getChildDocView(entry: PoolData) { + getChildDocView(entry: PoolData, renderIndex: number) { const childLayout = entry.pair.layout; const childData = entry.pair.data; const engine = this.props.layoutEngine?.() || StrCast(this.props.Document._layoutEngine); @@ -1153,6 +1154,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P Document={childLayout} renderDepth={this.props.renderDepth + 1} replica={entry.replica} + renderIndex={renderIndex} + renderCutoff={this.NumLoaded} ContainingCollectionView={this.props.CollectionView} ContainingCollectionDoc={this.props.Document} CollectionFreeFormView={this} @@ -1300,6 +1303,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P return { newPool, computedElementData: this.doFreeformLayout(newPool) }; } + @observable _numLoaded = 1; + NumLoaded = () => this._numLoaded; get doLayoutComputation() { const { newPool, computedElementData } = this.doInternalLayoutComputation; const array = Array.from(newPool.entries()); @@ -1318,9 +1323,9 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P this._cachedPool.clear(); Array.from(newPool.entries()).forEach(k => this._cachedPool.set(k[0], k[1])); const elements = computedElementData.slice(); - Array.from(newPool.entries()).filter(entry => this.isCurrent(entry[1].pair.layout)).forEach(entry => + Array.from(newPool.entries()).filter(entry => this.isCurrent(entry[1].pair.layout)).forEach((entry, i) => elements.push({ - ele: this.getChildDocView(entry[1]), + ele: this.getChildDocView(entry[1], i), bounds: this.childDataProvider(entry[1].pair.layout, entry[1].replica) })); @@ -1369,35 +1374,38 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P componentDidMount() { super.componentDidMount?.(); this.props.setContentView?.(this); - this._disposers.layoutComputation = reaction(() => this.doLayoutComputation, - (elements) => this._layoutElements = elements || [], - { fireImmediately: true, name: "doLayout" }); - - this._marqueeRef.current?.addEventListener("dashDragAutoScroll", this.onDragAutoScroll as any); - - this._disposers.groupBounds = reaction(() => { - if (this.props.Document._isGroup && this.childDocs.length === this.childDocList?.length) { - const clist = this.childDocs.map(cd => ({ x: NumCast(cd.x), y: NumCast(cd.y), width: cd[WidthSym](), height: cd[HeightSym]() })); - return aggregateBounds(clist, NumCast(this.layoutDoc._xPadding), NumCast(this.layoutDoc._yPadding)); - } - return undefined; - }, - (cbounds) => { - if (cbounds) { - const c = [NumCast(this.layoutDoc.x) + this.layoutDoc[WidthSym]() / 2, NumCast(this.layoutDoc.y) + this.layoutDoc[HeightSym]() / 2]; - const p = [NumCast(this.layoutDoc._panX), NumCast(this.layoutDoc._panY)]; - const pbounds = { - x: (cbounds.x - p[0]) * this.zoomScaling() + c[0], y: (cbounds.y - p[1]) * this.zoomScaling() + c[1], - r: (cbounds.r - p[0]) * this.zoomScaling() + c[0], b: (cbounds.b - p[1]) * this.zoomScaling() + c[1] - }; - this.layoutDoc._width = (pbounds.r - pbounds.x); - this.layoutDoc._height = (pbounds.b - pbounds.y); - this.layoutDoc._panX = (cbounds.r + cbounds.x) / 2; - this.layoutDoc._panY = (cbounds.b + cbounds.y) / 2; - this.layoutDoc.x = pbounds.x; - this.layoutDoc.y = pbounds.y; + setTimeout(action(() => { + this._firstRender = false; + this._disposers.layoutComputation = reaction(() => this.doLayoutComputation, + (elements) => this._layoutElements = elements || [], + { fireImmediately: true, name: "doLayout" }); + + this._marqueeRef.current?.addEventListener("dashDragAutoScroll", this.onDragAutoScroll as any); + + this._disposers.groupBounds = reaction(() => { + if (this.props.Document._isGroup && this.childDocs.length === this.childDocList?.length) { + const clist = this.childDocs.map(cd => ({ x: NumCast(cd.x), y: NumCast(cd.y), width: cd[WidthSym](), height: cd[HeightSym]() })); + return aggregateBounds(clist, NumCast(this.layoutDoc._xPadding), NumCast(this.layoutDoc._yPadding)); } - }, { fireImmediately: true }); + return undefined; + }, + (cbounds) => { + if (cbounds) { + const c = [NumCast(this.layoutDoc.x) + this.layoutDoc[WidthSym]() / 2, NumCast(this.layoutDoc.y) + this.layoutDoc[HeightSym]() / 2]; + const p = [NumCast(this.layoutDoc._panX), NumCast(this.layoutDoc._panY)]; + const pbounds = { + x: (cbounds.x - p[0]) * this.zoomScaling() + c[0], y: (cbounds.y - p[1]) * this.zoomScaling() + c[1], + r: (cbounds.r - p[0]) * this.zoomScaling() + c[0], b: (cbounds.b - p[1]) * this.zoomScaling() + c[1] + }; + this.layoutDoc._width = (pbounds.r - pbounds.x); + this.layoutDoc._height = (pbounds.b - pbounds.y); + this.layoutDoc._panX = (cbounds.r + cbounds.x) / 2; + this.layoutDoc._panY = (cbounds.b + cbounds.y) / 2; + this.layoutDoc.x = pbounds.x; + this.layoutDoc.y = pbounds.y; + } + }, { fireImmediately: true }); + })); } componentWillUnmount() { @@ -1557,9 +1565,21 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P e.stopPropagation(); } + incrementalRender = action(() => { + if (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath())) { + this._numLoaded++; + } + this._numLoaded < this.views.length && setTimeout(this.incrementalRender, 1); + }); + children = () => { + this.incrementalRender(); const children = typeof this.props.children === "function" ? (this.props.children as any)() as JSX.Element[] : []; - return [...children, ...this.views, <CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />]; + return [ + ...children, + ...this.views, + <CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" /> + ]; } chooseGridSpace = (gridSpace: number): number => { @@ -1642,7 +1662,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } @computed get contentScaling() { - if (this.props.isAnnotationOverlay && !this.props.annotationLayerHostsContent) return 0; + if (this._firstRender || (this.props.isAnnotationOverlay && !this.props.annotationLayerHostsContent)) return 0; const nw = this.nativeWidth; const nh = this.nativeHeight; const hscale = nh ? this.props.PanelHeight() / nh : 1; @@ -1677,7 +1697,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P width: `${100 / (this.contentScaling || 1)}%`, height: this.isAnnotationOverlay && this.Document.scrollHeight ? this.Document.scrollHeight : `${100 / (this.contentScaling || 1)}%`// : this.isAnnotationOverlay ? (this.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight() }}> - {this.Document._freeformLOD && !this.props.isContentActive() && !this.props.isAnnotationOverlay && this.props.renderDepth > 0 ? + {this._firstRender || (this.Document._freeformLOD && !this.props.isContentActive() && !this.props.isAnnotationOverlay && this.props.renderDepth > 0) ? this.placeholder : this.marqueeView} {this.props.noOverlay ? (null) : <CollectionFreeFormOverlayView elements={this.elementFunc} />} diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 08da682bb..b10b0912f 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -30,6 +30,9 @@ import "./MarqueeView.scss"; import React = require("react"); import { StyleLayers } from "../../StyleProvider"; import { TreeView } from "../TreeView"; +import { VideoBox } from "../../nodes/VideoBox"; +import { ImageField, WebField } from "../../../../fields/URLField"; +import { pasteImageBitmap } from "../../nodes/WebBoxRenderer"; interface MarqueeViewProps { getContainerTransform: () => Transform; @@ -134,17 +137,15 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque })(); e.stopPropagation(); } else if (e.key === "b" && e.ctrlKey) { - // e.preventDefault(); - // navigator.clipboard.readText().then(text => { - // const ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== ""); - // if (ns.length === 1 && text.startsWith("http")) { - // this.props.addDocument(Docs.Create.ImageDocument(text, { _nativeWidth: 300, _width: 300, x: x, y: y }));// paste an image from its URL in the paste buffer - // } else { - // this.pasteTable(ns, x, y); - // } - // }); - // e.stopPropagation(); - + document.body.focus(); // so that we can access the clipboard without an error + setTimeout(() => + pasteImageBitmap((data: any, error: any) => { + error && console.log(error); + data && VideoBox.convertDataUri(data, this.props.Document[Id] + "-thumb-frozen").then(returnedfilename => { + this.props.Document["thumb-frozen"] = new ImageField(returnedfilename); + }); + })); + } else if (e.key === "s" && e.ctrlKey) { e.preventDefault(); const slide = Doc.copyDragFactory(Doc.UserDoc().emptySlide as Doc)!; slide.x = x; diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index fe34d6687..460982c8a 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -1,4 +1,4 @@ -import { action, computed, observable } from "mobx"; +import { action, computed, observable, trace } from "mobx"; import { observer } from "mobx-react"; import { Doc, Opt } from "../../../fields/Doc"; import { Document } from "../../../fields/documentSchemas"; @@ -28,6 +28,8 @@ export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { jitterRotation: number; dataTransition?: string; replica: string; + renderCutoff: () => number; + renderIndex: number; CollectionFreeFormView: CollectionFreeFormView; } @@ -176,7 +178,10 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF mixBlendMode, display: this.ZInd === -99 ? "none" : undefined }} > - <DocumentView {...divProps} ref={action((r: DocumentView | null) => this._contentView = r)} /> + {this.props.renderCutoff() >= this.props.renderIndex ? + <DocumentView {...divProps} ref={action((r: DocumentView | null) => this._contentView = r)} /> + : + <div style={{ position: "absolute", width: this.panelWidth(), height: this.panelHeight(), background: "lightGreen" }}></div>} </div>; } } diff --git a/src/client/views/nodes/DocumentLinksButton.scss b/src/client/views/nodes/DocumentLinksButton.scss index 228e1bdcb..9ab3171d3 100644 --- a/src/client/views/nodes/DocumentLinksButton.scss +++ b/src/client/views/nodes/DocumentLinksButton.scss @@ -1,6 +1,8 @@ @import "../global/globalCssVariables.scss"; - +.documentLinksButton-wrapper { + transform-origin: top left; +} .documentLinksButton-menu { width: 100%; height: 100%; diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index 93cd02d93..7e6ca4248 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -20,6 +20,7 @@ import { DocumentView } from "./DocumentView"; import { LinkDescriptionPopup } from "./LinkDescriptionPopup"; import { TaskCompletionBox } from "./TaskCompletedBox"; import React = require("react"); +import { Transform } from "../../util/Transform"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; @@ -31,6 +32,7 @@ interface DocumentLinksButtonProps { AlwaysOn?: boolean; InMenu?: boolean; StartLink?: boolean; //whether the link HAS been started (i.e. now needs to be completed) + ContentScaling?: () => number; } @observer export class DocumentLinksButton extends React.Component<DocumentLinksButtonProps, {}> { @@ -302,16 +304,16 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp const title = this.props.InMenu ? menuTitle : buttonTitle; //render circular tooltip if it isn't set to invisible and show the number of doc links the node has, and render inner-menu link button for starting/stopping links if currently in menu - return !Array.from(this.filteredLinks).length && !this.props.AlwaysOn ? (null) : - this.props.InMenu && (DocumentLinksButton.StartLink || this.props.StartLink) ? - <Tooltip title={<div className="dash-tooltip">{title}</div>}> - {this.linkButtonInner} - </Tooltip> - : - !DocumentLinksButton.LinkEditorDocView && !this.props.InMenu ? - <Tooltip title={<div className="dash-tooltip">{title}</div>}> - {this.linkButtonInner} - </Tooltip> - : this.linkButtonInner; + return (!Array.from(this.filteredLinks).length && !this.props.AlwaysOn) ? (null) : + <div className="documentLinksButton-wrapper" > + { + (this.props.InMenu && (DocumentLinksButton.StartLink || this.props.StartLink)) || + (!DocumentLinksButton.LinkEditorDocView && !this.props.InMenu) ? + <Tooltip title={<div className="dash-tooltip">{title}</div>}> + {this.linkButtonInner} + </Tooltip> + : this.linkButtonInner + } + </div>; } } diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index 9fcd45e72..4565f8504 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -58,9 +58,10 @@ .documentView-audioBackground { display: inline-block; width: 10%; + height: 25; position: absolute; - top: 0px; - left: 0px; + top: 10px; + left: 10px; border-radius: 25px; background: white; opacity: 0.3; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 9fc4b7890..91f2359af 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,5 +1,5 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; +import { action, computed, IReactionDisposer, observable, reaction, runInAction, trace } from "mobx"; import { observer } from "mobx-react"; import { AclAdmin, AclEdit, AclPrivate, DataSym, Doc, DocListCast, Field, Opt, StrListCast } from "../../../fields/Doc"; import { Document } from '../../../fields/documentSchemas'; @@ -9,7 +9,7 @@ import { List } from "../../../fields/List"; import { ObjectField } from "../../../fields/ObjectField"; import { listSpec } from "../../../fields/Schema"; import { ScriptField } from '../../../fields/ScriptField'; -import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from "../../../fields/Types"; +import { BoolCast, Cast, NumCast, ScriptCast, StrCast, ImageCast } from "../../../fields/Types"; import { AudioField } from "../../../fields/URLField"; import { GetEffectiveAcl, SharingPermissions, TraceMobx } from '../../../fields/util'; import { MobileInterface } from '../../../mobile/MobileInterface'; @@ -184,6 +184,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @observable _animateScalingTo = 0; @observable _mediaState = 0; @observable _pendingDoubleClick = false; + private _disposers: { [name: string]: IReactionDisposer } = {}; private _downX: number = 0; private _downY: number = 0; private _firstX: number = -1; @@ -239,6 +240,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps this._multiTouchDisposer?.(); this._holdDisposer?.(); unbrush && Doc.UnBrushDoc(this.props.Document); + Object.values(this._disposers).forEach(disposer => disposer?.()); } handle1PointerHoldStart = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): any => { @@ -485,6 +487,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const func = () => this.onClickHandler.script.run({ this: this.layoutDoc, self: this.rootDoc, + _readOnly_: false, scriptContext: this.props.scriptContext, thisContainer: this.props.ContainingCollectionDoc, documentView: this.props.DocumentView(), @@ -837,12 +840,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } @computed get contents() { TraceMobx(); + const thumb = ImageCast(this.layoutDoc["thumb-frozen"], ImageCast(this.layoutDoc.thumb))?.url.href.replace(".png", "_m.png"); const audioView = !this.layoutDoc._showAudio ? (null) : - <div className="documentView-audioBackground" - onPointerDown={this.recordAudioAnnotation} - onPointerEnter={this.onPointerEnter} - style={{ height: 25, position: "absolute", top: 10, left: 10 }} - > + <div className="documentView-audioBackground" onPointerDown={this.recordAudioAnnotation} onPointerEnter={this.onPointerEnter} > <FontAwesomeIcon className="documentView-audioFont" style={{ color: [DocListCast(this.dataDoc[this.LayoutFieldKey + "-audioAnnotations"]).length ? "blue" : "gray", "green", "red"][this._mediaState] }} icon={!DocListCast(this.dataDoc[this.LayoutFieldKey + "-audioAnnotations"]).length ? "microphone" : "file-audio"} size="sm" /> @@ -864,12 +864,15 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps onClick={this.onClickFunc} focus={this.focus} layoutKey={this.finalLayoutKey} /> + {(this.isContentActive() && !SnappingManager.GetIsDragging()) || !thumb ? (null) : + <img style={{ background: "white", top: 0, position: "absolute" }} src={thumb} // + '?d=' + (new Date()).getTime()} + width={this.props.PanelWidth()} height={this.props.PanelHeight()} />} {this.layoutDoc.hideAllLinks ? (null) : this.allLinkEndpoints} - {this.hideLinkButton || this.props.renderDepth === -1 ? (null) : - <div style={{ transformOrigin: "top left", transform: `scale(${Math.min(1, this.props.ScreenToLocalTransform().scale(this.props.ContentScaling?.() || 1).Scale)})` }}> - <DocumentLinksButton View={this.props.DocumentView()} Offset={[this.topMost ? 0 : -15, undefined, undefined, this.topMost ? 10 : -20]} /> - </div>} - + {this.hideLinkButton || this.props.renderDepth === -1 || SnappingManager.GetIsDragging() ? (null) : + <DocumentLinksButton View={this.props.DocumentView()} + ContentScaling={this.props.ContentScaling} + Offset={[this.topMost ? 0 : -15, undefined, undefined, this.topMost ? 10 : -20]} /> + } {audioView} </div>; } @@ -1047,9 +1050,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps {captionView} </div>; } + @observable _: string = ""; @computed get renderDoc() { TraceMobx(); - const isButton: boolean = this.props.Document.type === DocumentType.FONTICON; + const isButton = this.props.Document.type === DocumentType.FONTICON; if (!(this.props.Document instanceof Doc) || GetEffectiveAcl(this.props.Document[DataSym]) === AclPrivate || this.hidden) return null; return this.docContents ?? <div className={`documentView-node${this.topMost ? "-topmost" : ""}`} @@ -1064,6 +1068,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps transform: this._animateScalingTo ? `scale(${this._animateScalingTo})` : undefined, transition: !this._animateScalingTo ? StrCast(this.Document.dataTransition) : `transform 0.5s ease-${this._animateScalingTo < 1 ? "in" : "out"}`, }}> + {this.innards} {this.onClickHandler && this.props.ContainingCollectionView?.props.Document._viewType === CollectionViewType.Time ? <div className="documentView-contentBlocker" /> : (null)} {this.widgetDecorations ?? null} @@ -1171,10 +1176,10 @@ export class DocumentView extends React.Component<DocumentViewProps> { } return this.props.PanelHeight(); } - @computed get Xshift() { return this.effectiveNativeWidth ? (this.props.PanelWidth() - this.effectiveNativeWidth * this.nativeScaling) / 2 : 0; } - @computed get Yshift() { return this.effectiveNativeWidth && this.effectiveNativeHeight && Math.abs(this.Xshift) < 0.001 ? (this.props.PanelHeight() - this.effectiveNativeHeight * this.nativeScaling) / 2 : 0; } - @computed get centeringX() { return this.props.dontCenter?.includes("x") ? 0 : this.Xshift; } - @computed get centeringY() { return this.fitWidth || this.props.dontCenter?.includes("y") ? 0 : this.Yshift; } + @computed get Xshift() { return 0; } //this.effectiveNativeWidth ? (this.props.PanelWidth() - this.effectiveNativeWidth * this.nativeScaling) / 2 : 0; } + @computed get Yshift() { return 0; }//this.effectiveNativeWidth && this.effectiveNativeHeight && Math.abs(this.Xshift) < 0.001 ? (this.props.PanelHeight() - this.effectiveNativeHeight * this.nativeScaling) / 2 : 0; } + @computed get centeringX() { return 0; }//this.props.dontCenter?.includes("x") ? 0 : this.Xshift; } + @computed get centeringY() { return 0; }//this.fitWidth || this.props.dontCenter?.includes("y") ? 0 : this.Yshift; } toggleNativeDimensions = () => this.docView && Doc.toggleNativeDimensions(this.layoutDoc, this.docView.ContentScale, this.props.PanelWidth(), this.props.PanelHeight()); focus = (doc: Doc, options?: DocFocusOptions) => this.docView?.focus(doc, options); diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index 111509fdb..c44c8c828 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -58,7 +58,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { value = eq ? value.substr(1) : value; const dubEq = value.startsWith(":=") ? "computed" : value.startsWith(";=") ? "script" : false; value = dubEq ? value.substr(2) : value; - const options: ScriptOptions = { addReturn: true, params: { this: "Doc", _last_: "any" }, editable: false }; + const options: ScriptOptions = { addReturn: true, params: { this: Doc.name, self: Doc.name, _last_: "any", _readOnly_: "boolean" }, editable: false }; if (dubEq) options.typecheck = false; const script = CompileScript(value, options); return !script.compiled ? undefined : { script, type: dubEq, onDelegate: eq }; diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 615d595c0..3f3de9da3 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -257,13 +257,14 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } } - public static async convertDataUri(imageUri: string, returnedFilename: string) { + public static async convertDataUri(imageUri: string, returnedFilename: string, nosuffix = false) { try { const posting = Utils.prepend("/uploadURI"); const returnedUri = await rp.post(posting, { body: { uri: imageUri, - name: returnedFilename + name: returnedFilename, + nosuffix }, json: true, }); diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 9956cc36b..dd971aafa 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -10,8 +10,8 @@ import { InkTool } from "../../../fields/InkField"; import { List } from "../../../fields/List"; import { listSpec, makeInterface } from "../../../fields/Schema"; import { ComputedField } from "../../../fields/ScriptField"; -import { Cast, NumCast, StrCast } from "../../../fields/Types"; -import { WebField } from "../../../fields/URLField"; +import { Cast, NumCast, StrCast, WebCast, ImageCast, BoolCast } from "../../../fields/Types"; +import { WebField, ImageField } from "../../../fields/URLField"; import { TraceMobx } from "../../../fields/util"; import { emptyFunction, getWordAtPoint, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, smoothScroll, Utils } from "../../../Utils"; import { Docs } from "../../documents/Documents"; @@ -38,9 +38,11 @@ import { FieldView, FieldViewProps } from './FieldView'; import { LinkDocPreview } from "./LinkDocPreview"; import "./WebBox.scss"; import React = require("react"); +import { RequestOptions } from "https"; +import { VideoBox } from "./VideoBox"; +const { CreateImage } = require("./WebBoxRenderer"); const _global = (window /* browser */ || global /* node */) as any; const htmlToText = require("html-to-text"); - type WebDocument = makeInterface<[typeof documentSchema]>; const WebDocument = makeInterface(documentSchema); @@ -63,6 +65,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @observable private _searching: boolean = false; @observable private _showSidebar = false; @observable private _scrollTimer: any; + @observable private _webPageHasBeenRendered = false; @observable private _overlayAnnoInfo: Opt<Doc>; @observable private _marqueeing: number[] | undefined; @observable private _isAnnotating = false; @@ -76,6 +79,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @computed get allAnnotations() { return DocListCast(this.dataDoc[this.annotationKey]); } @computed get inlineTextAnnotations() { return this.allAnnotations.filter(a => a.textInlineAnnotations); } @computed get webField() { return Cast(this.dataDoc[this.props.fieldKey], WebField)?.url; } + @computed get webThumb() { return ImageCast(this.layoutDoc["thumb-frozen"], ImageCast(this.layoutDoc.thumb))?.url; } constructor(props: any) { super(props); @@ -103,7 +107,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps return true; } async componentDidMount() { - this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link. + this.props.setContentView?.(this); // this tells the DocumentView that this WebBox is the "content" of the document. this allows the DocumentView to call WebBox relevant methods to configure the UI (eg, show back/forward buttons) runInAction(() => { this._annotationKeySuffix = () => this._urlHash + "-annotations"; @@ -111,6 +115,46 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps this.dataDoc[this.fieldKey + "-annotations"] = ComputedField.MakeFunction(`copyField(this["${this.fieldKey}-"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"-annotations"`); this.dataDoc[this.fieldKey + "-sidebar"] = ComputedField.MakeFunction(`copyField(this["${this.fieldKey}-"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"-sidebar"`); }); + reaction(() => this.props.isSelected(), + async (selected) => { + if (selected) { + this._webPageHasBeenRendered = true; + setTimeout(action(() => { + this._scrollHeight = Math.max(this.scrollHeight, this._iframe?.contentDocument?.body.scrollHeight || 0); + if (this._initialScroll !== undefined && this._outerRef.current) { + setTimeout(() => { + this._outerRef.current!.scrollTop = this._initialScroll!; + this._initialScroll = undefined; + }); + } + })); + } else if (!this.props.isContentActive()) { + const imageBitmap = ImageCast(this.layoutDoc["thumb-frozen"])?.url.href; + if (this._iframe && !imageBitmap) { + var htmlString = this._iframe.contentDocument && new XMLSerializer().serializeToString(this._iframe.contentDocument); + if (!htmlString) { + htmlString = await (await fetch(Utils.CorsProxy(this.webField!.href))).text(); + } + this.layoutDoc.thumb = undefined; + const nativeWidth = NumCast(this.layoutDoc.nativeWidth); + CreateImage( + this._webUrl.endsWith("/") ? this._webUrl.substring(0, this._webUrl.length - 1) : this._webUrl, + this._iframe.contentDocument?.styleSheets ?? [], + htmlString, + nativeWidth, + nativeWidth * this.props.PanelHeight() / this.props.PanelWidth(), + NumCast(this.layoutDoc._scrollTop) + ).then + ((dataUrl: any) => { + VideoBox.convertDataUri(dataUrl, this.layoutDoc[Id] + "-thumb" + (new Date()).getTime(), true).then( + returnedfilename => setTimeout(action(() => this.layoutDoc.thumb = new ImageField(returnedfilename)), 500)); + }) + .catch(function (error: any) { + console.error('oops, something went wrong!', error); + }); + } + } + }); this._disposers.autoHeight = reaction(() => this.layoutDoc._autoHeight, autoHeight => { @@ -292,12 +336,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps iframe.contentDocument.addEventListener("pointerdown", this.iframeDown); this._scrollHeight = Math.max(this.scrollHeight, iframe?.contentDocument.body.scrollHeight); setTimeout(action(() => this._scrollHeight = Math.max(this.scrollHeight, iframe?.contentDocument?.body.scrollHeight || 0)), 5000); - const initialScroll = this._initialScroll; - if (initialScroll !== undefined && this._outerRef.current) { - // bcz: not sure why this happens, but if the webpage isn't ready yet, it's scroll height seems to be limited. So we need to wait tp set scroll location to what we want. - setTimeout(() => this._outerRef.current!.scrollTop = initialScroll); - this._initialScroll = undefined; - } iframe.setAttribute("enable-annotation", "true"); iframe.contentDocument.addEventListener("click", undoBatch(action((e: MouseEvent) => { let href = ""; @@ -353,42 +391,46 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } } - @action - forward = () => { + forward = (checkAvailable?: boolean) => { const future = Cast(this.dataDoc[this.fieldKey + "-future"], listSpec("string"), []); const history = Cast(this.dataDoc[this.fieldKey + "-history"], listSpec("string"), []); - if (future.length) { - const curUrl = this._url; - this.dataDoc[this.fieldKey + "-history"] = new List<string>([...history, this._url]); - this.dataDoc[this.fieldKey] = new WebField(new URL(future.pop()!)); - if (this._webUrl === this._url) { - this._webUrl = curUrl; - setTimeout(action(() => this._webUrl = this._url)); - } else { - this._webUrl = this._url; + if (checkAvailable) return future.length; + runInAction(() => { + if (future.length) { + const curUrl = this._url; + this.dataDoc[this.fieldKey + "-history"] = new List<string>([...history, this._url]); + this.dataDoc[this.fieldKey] = new WebField(new URL(future.pop()!)); + if (this._webUrl === this._url) { + this._webUrl = curUrl; + setTimeout(action(() => this._webUrl = this._url)); + } else { + this._webUrl = this._url; + } + return true; } - return true; - } + }); return false; } - @action - back = () => { + back = (checkAvailable?: boolean) => { const future = Cast(this.dataDoc[this.fieldKey + "-future"], listSpec("string")); const history = Cast(this.dataDoc[this.fieldKey + "-history"], listSpec("string"), []); - if (history.length) { - const curUrl = this._url; - if (future === undefined) this.dataDoc[this.fieldKey + "-future"] = new List<string>([this._url]); - else this.dataDoc[this.fieldKey + "-future"] = new List<string>([...future, this._url]); - this.dataDoc[this.fieldKey] = new WebField(new URL(history.pop()!)); - if (this._webUrl === this._url) { - this._webUrl = curUrl; - setTimeout(action(() => this._webUrl = this._url)); - } else { - this._webUrl = this._url; + if (checkAvailable) return history.length; + runInAction(() => { + if (history.length) { + const curUrl = this._url; + if (future === undefined) this.dataDoc[this.fieldKey + "-future"] = new List<string>([this._url]); + else this.dataDoc[this.fieldKey + "-future"] = new List<string>([...future, this._url]); + this.dataDoc[this.fieldKey] = new WebField(new URL(history.pop()!)); + if (this._webUrl === this._url) { + this._webUrl = curUrl; + setTimeout(action(() => this._webUrl = this._url)); + } else { + this._webUrl = this._url; + } + return true; } - return true; - } + }); return false; } @@ -452,8 +494,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps <button className="submitUrl" onClick={() => this.submitURL(this._keyInput.current!.value)} onDragOver={e => e.stopPropagation()} onDrop={this.onWebUrlDrop}> GO </button> - <button className="submitUrl" onClick={this.back}> <FontAwesomeIcon icon="caret-left" size="lg" /> </button> - <button className="submitUrl" onClick={this.forward}> <FontAwesomeIcon icon="caret-right" size="lg" /> </button> + <button className="submitUrl" onClick={() => this.back}> <FontAwesomeIcon icon="caret-left" size="lg" /> </button> + <button className="submitUrl" onClick={() => this.forward}> <FontAwesomeIcon icon="caret-right" size="lg" /> </button> </div> </div> ); @@ -512,7 +554,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } @computed get urlContent() { - if (this._hackHide) return (null); + if (this._hackHide || (this.webThumb && !this._webPageHasBeenRendered)) return (null); const field = this.dataDoc[this.props.fieldKey]; let view; if (field instanceof HtmlField) { @@ -530,6 +572,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps style={{ pointerEvents: this._scrollTimer ? "none" : undefined }} // if we allow pointer events when scrolling is on, then reversing direction does not work smoothly ref={action((r: HTMLIFrameElement | null) => this._iframe = r)} src={"https://crossorigin.me/https://cs.brown.edu"} />; } + setTimeout(action(() => this._webPageHasBeenRendered = true)); return view; } @@ -664,8 +707,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps pointerEvents={this._isAnnotating || SnappingManager.GetIsDragging() ? "all" : "none"} />; return ( <div className="webBox" ref={this._mainCont} - style={{ pointerEvents: this.pointerEvents() }} > - <div className={`webBox-container`} style={{ pointerEvents }} onContextMenu={this.specificContextMenu}> + style={{ pointerEvents: this.pointerEvents(), display: !this.props.isSelected() && !this.isAnyChildContentActive() && this.webThumb ? "none" : undefined }} > + <div className="webBox-container" style={{ pointerEvents }} onContextMenu={this.specificContextMenu}> <div className={"webBox-outerContent"} ref={this._outerRef} style={{ width: `calc(${100 / scale}% - ${this.sidebarWidth() / scale * (this._previewWidth ? scale : 1)}px)`, @@ -703,6 +746,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps </div > <SidebarAnnos ref={this._sidebarRef} {...this.props} + whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} fieldKey={this.fieldKey + "-" + this._urlHash} rootDoc={this.rootDoc} layoutDoc={this.layoutDoc} diff --git a/src/client/views/nodes/WebBoxRenderer.js b/src/client/views/nodes/WebBoxRenderer.js new file mode 100644 index 000000000..08a5746d1 --- /dev/null +++ b/src/client/views/nodes/WebBoxRenderer.js @@ -0,0 +1,395 @@ +/** + * + * @param {StyleSheetList} styleSheets + */ +var ForeignHtmlRenderer = function (styleSheets) { + + const self = this; + + /** + * + * @param {String} binStr + */ + const binaryStringToBase64 = function (binStr) { + return new Promise(function (resolve) { + const reader = new FileReader(); + reader.readAsDataURL(binStr); + reader.onloadend = function () { + resolve(reader.result); + } + }); + }; + + function prepend(extension) { + return window.location.origin + extension; + } + function CorsProxy(url) { + return prepend("/corsProxy/") + encodeURIComponent(url); + } + /** + * + * @param {String} url + * @returns {Promise} + */ + const getResourceAsBase64 = function (webUrl, inurl) { + return new Promise(function (resolve, reject) { + const xhr = new XMLHttpRequest(); + //const url = inurl.startsWith("/") && !inurl.startsWith("//") ? webUrl + inurl : inurl; + //const url = CorsProxy(inurl.startsWith("/") && !inurl.startsWith("//") ? webUrl + inurl : inurl);// inurl.startsWith("http") ? CorsProxy(inurl) : inurl; + var url = inurl; + if (inurl.startsWith("/static")) { + url = (new URL(webUrl).origin + inurl); + } else + if ((inurl.startsWith("/") && !inurl.startsWith("//"))) { + url = CorsProxy(new URL(webUrl).origin + inurl); + } else if (!inurl.startsWith("http") && !inurl.startsWith("//")) { + url = CorsProxy(webUrl + "/" + inurl); + } + xhr.open("GET", url); + xhr.responseType = 'blob'; + + xhr.onreadystatechange = async function () { + if (xhr.readyState === 4 && xhr.status === 200) { + const resBase64 = await binaryStringToBase64(xhr.response); + + resolve( + { + "resourceUrl": inurl, + "resourceBase64": resBase64 + } + ); + } else if (xhr.readyState === 4) { + console.log("COULDN'T FIND: " + (inurl.startsWith("/") ? webUrl + inurl : inurl)); + resolve( + { + "resourceUrl": "", + "resourceBase64": inurl + } + ); + } + }; + + xhr.send(null); + }); + }; + + /** + * + * @param {String[]} urls + * @returns {Promise} + */ + const getMultipleResourcesAsBase64 = function (webUrl, urls) { + const promises = []; + for (let i = 0; i < urls.length; i++) { + promises.push(getResourceAsBase64(webUrl, urls[i])); + } + return Promise.all(promises); + }; + + /** + * + * @param {String} str + * @param {Number} startIndex + * @param {String} prefixToken + * @param {String[]} suffixTokens + * + * @returns {String|null} + */ + const parseValue = function (str, startIndex, prefixToken, suffixTokens) { + const idx = str.indexOf(prefixToken, startIndex); + if (idx === -1) { + return null; + } + + let val = ''; + for (let i = idx + prefixToken.length; i < str.length; i++) { + if (suffixTokens.indexOf(str[i]) !== -1) { + break; + } + + val += str[i]; + } + + return { + "foundAtIndex": idx, + "value": val + } + }; + + /** + * + * @param {String} cssRuleStr + * @returns {String[]} + */ + const getUrlsFromCssString = function (cssRuleStr, selector = "url(", delimiters = [')'], mustEndWithQuote = false) { + const urlsFound = []; + let searchStartIndex = 0; + + while (true) { + const url = parseValue(cssRuleStr, searchStartIndex, selector, delimiters); + if (url === null) { + break; + } + searchStartIndex = url.foundAtIndex + url.value.length; + if (mustEndWithQuote && url.value[url.value.length - 1] !== '"') continue; + const unquoted = removeQuotes(url.value); + if (!unquoted /* || (!unquoted.startsWith('http')&& !unquoted.startsWith("/") )*/ || unquoted === 'http://' || unquoted === 'https://') { + continue; + } + + unquoted && urlsFound.push(unquoted); + } + + return urlsFound; + }; + + /** + * + * @param {String} html + * @returns {String[]} + */ + const getImageUrlsFromFromHtml = function (html) { + return getUrlsFromCssString(html, "src=", [' ', '>', '\t'], true); + }; + const getSourceUrlsFromFromHtml = function (html) { + return getUrlsFromCssString(html, "source=", [' ', '>', '\t'], true); + }; + + /** + * + * @param {String} str + * @returns {String} + */ + const removeQuotes = function (str) { + return str.replace(/["']/g, ""); + }; + + const escapeRegExp = function (string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string + }; + + /** + * + * @param {String} contentHtml + * @param {Number} width + * @param {Number} height + * + * @returns {Promise<String>} + */ + const buildSvgDataUri = async function (webUrl, contentHtml, width, height, scroll) { + + return new Promise(async function (resolve, reject) { + + /* !! The problems !! + * 1. CORS (not really an issue, expect perhaps for images, as this is a general security consideration to begin with) + * 2. Platform won't wait for external assets to load (fonts, images, etc.) + */ + + // copy styles + let cssStyles = ""; + let urlsFoundInCss = []; + + for (let i = 0; i < styleSheets.length; i++) { + try { + const rules = styleSheets[i].cssRules + for (let j = 0; j < rules.length; j++) { + const cssRuleStr = rules[j].cssText; + urlsFoundInCss.push(...getUrlsFromCssString(cssRuleStr)); + cssStyles += cssRuleStr; + } + } catch (e) { + + } + } + + // const fetchedResourcesFromStylesheets = await getMultipleResourcesAsBase64(webUrl, urlsFoundInCss); + // for (let i = 0; i < fetchedResourcesFromStylesheets.length; i++) { + // const r = fetchedResourcesFromStylesheets[i]; + // if (r.resourceUrl) { + // cssStyles = cssStyles.replace(new RegExp(escapeRegExp(r.resourceUrl), "g"), r.resourceBase64); + // } + // } + + contentHtml = contentHtml.replace(/<source[^>]*>/g, "") // <picture> tags have a <source> which has a srcset field of image refs. instead of converting each, just use the default <img> of the picture + .replace(/noscript/g, "div").replace(/<div class="mediaset"><\/div>/g, "") // when scripting isn't available (ie, rendering web pages here), <noscript> tags should become <div>'s. But for Brown CS, there's a layout problem if you leave the empty <mediaset> tag + .replace(/<link[^>]*>/g, "") // don't need to keep any linked style sheets because we've already processed all style sheets above + .replace(/srcset="([^ "]*)[^"]*"/g, "src=\"$1\""); // instead of converting each item in the srcset to a data url, just convert the first one and use that + let urlsFoundInHtml = getImageUrlsFromFromHtml(contentHtml); + const fetchedResources = await getMultipleResourcesAsBase64(webUrl, urlsFoundInHtml); + for (let i = 0; i < fetchedResources.length; i++) { + const r = fetchedResources[i]; + if (r.resourceUrl) { + contentHtml = contentHtml.replace(new RegExp(escapeRegExp(r.resourceUrl), "g"), r.resourceBase64); + } + } + + const styleElem = document.createElement("style"); + styleElem.innerHTML = cssStyles.replace(">", ">").replace("<", "<"); + + const styleElemString = new XMLSerializer().serializeToString(styleElem).replace(/>/g, ">").replace(/</g, "<"); + + // create DOM element string that encapsulates styles + content + const contentRootElem = document.createElement("body"); + contentRootElem.style.zIndex = "1111"; + // contentRootElem.style.transform = "scale(0.08)" + contentRootElem.innerHTML = styleElemString + contentHtml; + contentRootElem.setAttribute("xmlns", "http://www.w3.org/1999/xhtml"); + //document.body.appendChild(contentRootElem); + + const contentRootElemString = new XMLSerializer().serializeToString(contentRootElem); + + // build SVG string + const svg = `<svg xmlns='http://www.w3.org/2000/svg' width='${width}' height='${height}'> + <foreignObject x='0' y='${-scroll}' width='${width}' height='${scroll + height}'> + ${contentRootElemString} + </foreignObject> + </svg>`; + + // convert SVG to data-uri + const dataUri = `data:image/svg+xml;base64,${window.btoa(unescape(encodeURIComponent(svg)))}`; + + resolve(dataUri); + }); + }; + + /** + * @param {String} html + * @param {Number} width + * @param {Number} height + * + * @return {Promise<Image>} + */ + this.renderToImage = async function (webUrl, html, width, height, scroll) { + return new Promise(async function (resolve, reject) { + const img = new Image(); + console.log("BUILDING SVG for:" + webUrl); + img.src = await buildSvgDataUri(webUrl, html, width, height, scroll); + + img.onload = function () { + console.log("IMAGE SVG created:" + webUrl); + resolve(img); + }; + }); + }; + + /** + * @param {String} html + * @param {Number} width + * @param {Number} height + * + * @return {Promise<Image>} + */ + this.renderToCanvas = async function (webUrl, html, width, height, scroll) { + return new Promise(async function (resolve, reject) { + const img = await self.renderToImage(webUrl, html, width, height, scroll); + + const canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + + const canvasCtx = canvas.getContext('2d'); + canvasCtx.drawImage(img, 0, 0, img.width, img.height); + + resolve(canvas); + }); + }; + + /** + * @param {String} html + * @param {Number} width + * @param {Number} height + * + * @return {Promise<String>} + */ + this.renderToBase64Png = async function (webUrl, html, width, height, scroll) { + return new Promise(async function (resolve, reject) { + const canvas = await self.renderToCanvas(webUrl, html, width, height, scroll); + resolve(canvas.toDataURL('image/png')); + }); + }; + +}; + + +export function CreateImage(webUrl, styleSheets, html, width, height, scroll) { + const val = (new ForeignHtmlRenderer(styleSheets)).renderToBase64Png(webUrl, html.replace(/\n/g, "").replace(/<script((?!\/script).)*<\/script>/g, ""), width, height, scroll); + return val; +} + + + +var ClipboardUtils = new function () { + var permissions = { + 'image/bmp': true, + 'image/gif': true, + 'image/png': true, + 'image/jpeg': true, + 'image/tiff': true + }; + + function getType(types) { + for (var j = 0; j < types.length; ++j) { + var type = types[j]; + if (permissions[type]) { + return type; + } + } + return null; + } + function getItem(items) { + for (var i = 0; i < items.length; ++i) { + var item = items[i]; + if (item) { + var type = getType(item.types); + if (type) { + return item.getType(type); + } + } + } + return null; + } + function loadFile(file, callback) { + if (window.FileReader) { + var reader = new FileReader(); + reader.onload = function () { + callback(reader.result, null); + }; + reader.onerror = function () { + callback(null, 'Incorrect file.'); + }; + reader.readAsDataURL(file); + } else { + callback(null, 'File api is not supported.'); + } + } + this.readImage = function (callback) { + if (navigator.clipboard) { + var promise = navigator.clipboard.read(); + promise + .then(function (items) { + var promise = getItem(items); + if (promise == null) { + callback(null, null); + return; + } + promise + .then(function (result) { + loadFile(result, callback); + }) + .catch(function (error) { + callback(null, 'Reading clipboard error.'); + }); + }) + .catch(function (error) { + callback(null, 'Reading clipboard error.'); + }); + } else { + callback(null, 'Clipboard is not supported.'); + } + }; +}; + + +export function pasteImageBitmap(callback) { + return ClipboardUtils.readImage(callback); +}
\ No newline at end of file diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx index bd103dcf7..91365121f 100644 --- a/src/client/views/nodes/button/FontIconBox.tsx +++ b/src/client/views/nodes/button/FontIconBox.tsx @@ -1,7 +1,7 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; -import { action, computed, observable } from 'mobx'; +import { action, computed, observable, trace } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { ColorState, SketchPicker } from 'react-color'; @@ -9,7 +9,7 @@ import { Doc, StrListCast, WidthSym, HeightSym } from '../../../../fields/Doc'; import { InkTool } from '../../../../fields/InkField'; import { createSchema, makeInterface } from '../../../../fields/Schema'; import { ScriptField } from '../../../../fields/ScriptField'; -import { BoolCast, Cast, NumCast, StrCast } from '../../../../fields/Types'; +import { BoolCast, Cast, NumCast, StrCast, ScriptCast } from '../../../../fields/Types'; import { WebField } from '../../../../fields/URLField'; import { DocumentType } from '../../../documents/DocumentTypes'; import { Scripting } from "../../../util/Scripting"; @@ -105,16 +105,11 @@ export class FontIconBox extends DocComponent<ButtonProps, FontIconDocument>(Fon */ @computed get numberButton() { const numBtnType: string = StrCast(this.rootDoc.numBtnType); - const setValue = (value: number) => { - // Script for running the toggle - const script: string = StrCast(this.rootDoc.script) + "(" + value + ")"; - ScriptField.MakeScript(script)?.script.run(); - }; + const numScript = ScriptCast(this.rootDoc.script); + const setValue = (value: number) => numScript?.script.run({ value, _readOnly_: false }); // Script for checking the outcome of the toggle - const checkScript: string = StrCast(this.rootDoc.script) + "(0, true)"; - const checkResult: number = ScriptField.MakeScript(checkScript)?.script.run().result || 0; - + const checkResult: number = numScript?.script.run({ value: 0, _readOnly_: true }).result || 0; if (numBtnType === NumButtonType.Slider) { const dropdown = @@ -237,34 +232,37 @@ export class FontIconBox extends DocComponent<ButtonProps, FontIconDocument>(Fon const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); - const script: string = StrCast(this.rootDoc.script); + const script = ScriptCast(this.rootDoc.script); let noviceList: string[] = []; let text: string | undefined; let dropdown = true; let icon: IconProp = "caret-down"; - - if (script === 'setView') { - const selected = SelectionManager.Docs().lastElement(); - if (selected) { - if (StrCast(selected.type) === DocumentType.COL) { - text = StrCast(selected._viewType); + try { + if (script.script.originalScript.startsWith('setView')) { + const selected = SelectionManager.Docs().lastElement(); + if (selected) { + if (StrCast(selected.type) === DocumentType.COL) { + text = StrCast(selected._viewType); + } else { + dropdown = false; + text = selected.type === DocumentType.RTF ? "Text" : StrCast(selected.type); + icon = Doc.toIcon(selected); + } } else { dropdown = false; - text = selected.type === DocumentType.RTF ? "Text" : StrCast(selected.type); - icon = Doc.toIcon(selected); + icon = "globe-asia"; + text = "User Default"; } - } else { - dropdown = false; - icon = "globe-asia"; - text = "User Default"; + noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Stacking]; + } else if (script.script.originalScript.startsWith('setFont')) { + const editorView = RichTextMenu.Instance?.TextView?.EditorView; + text = StrCast((editorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily); + noviceList = ["Roboto", "Times New Roman", "Arial", "Georgia", + "Comic Sans MS", "Tahoma", "Impact", "Crimson Text"]; } - noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Stacking]; - } else if (script === 'setFont') { - const editorView = RichTextMenu.Instance?.TextView?.EditorView; - text = StrCast((editorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily); - noviceList = ["Roboto", "Times New Roman", "Arial", "Georgia", - "Comic Sans MS", "Tahoma", "Impact", "Crimson Text"]; + } catch (e) { + console.log(e); } // Get items to place into the list @@ -272,18 +270,12 @@ export class FontIconBox extends DocComponent<ButtonProps, FontIconDocument>(Fon if (Doc.UserDoc().noviceMode && !noviceList.includes(value)) { return; } - const click = () => { - const s = ScriptField.MakeScript(script + '("' + value + '")'); - if (s) { - s.script.run().result; - } - }; return <div className="list-item" key={`${value}`} style={{ - fontFamily: script === 'setFont' ? value : undefined, + fontFamily: script.script.originalScript.startsWith('setFont') ? value : undefined, backgroundColor: value === text ? Colors.LIGHT_BLUE : undefined }} - onClick={click}> + onClick={() => script.script.run({ value }).result}> {value[0].toUpperCase() + value.slice(1)} </div>; }); @@ -319,15 +311,12 @@ export class FontIconBox extends DocComponent<ButtonProps, FontIconDocument>(Fon } @observable colorPickerClosed: boolean = true; - @computed get colorScript() { - const script = StrCast(this.rootDoc.script); - return ScriptField.MakeScript(script + '(colValue, checkResult)', { colValue: "string", checkResult: "boolean" }); - } + @computed get colorScript() { return ScriptCast(this.rootDoc.script); } colorPicker = (curColor: string) => { const change = (value: ColorState) => { const s = this.colorScript; - s && undoBatch(() => s.script.run({ colValue: Utils.colorString(value), checkResult: false }).result)(); + s && undoBatch(() => s.script.run({ value: Utils.colorString(value), _readOnly_: false }).result)(); }; const presets = ['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', @@ -343,7 +332,7 @@ export class FontIconBox extends DocComponent<ButtonProps, FontIconDocument>(Fon @computed get colorButton() { const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); - const curColor = this.colorScript?.script.run({ colValue: undefined, checkResult: true }).result ?? "transparent"; + const curColor = this.colorScript?.script.run({ value: undefined, _readOnly_: true }).result ?? "transparent"; const label = !this.label || !Doc.UserDoc()._showLabel ? (null) : <div className="fontIconBox-label" style={{ color, backgroundColor, position: "absolute" }}> @@ -439,52 +428,34 @@ export class FontIconBox extends DocComponent<ButtonProps, FontIconDocument>(Fon @computed get editableText() { // Script for running the toggle - const script: string = StrCast(this.rootDoc.script); - - // Script for checking the outcome of the toggle - const checkScript: string = StrCast(this.rootDoc.script) + "('', true)"; - + const script = ScriptCast(this.rootDoc.script); // Function to run the script - const checkResult = ScriptField.MakeScript(checkScript)?.script.run().result; + const checkResult = script?.script.run({ value: "", _readOnly_: true }).result; - const setValue = (value: string, shiftDown?: boolean): boolean => { - ScriptField.MakeScript(script + "('" + value + "')")?.script.run(); - return true; - }; + const setValue = (value: string, shiftDown?: boolean): boolean => script?.script.run({ value, _readOnly_: false }).result; return ( <div className="menuButton editableText"> <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={"lock"} /> <div style={{ width: "calc(100% - .875em)", paddingLeft: "4px" }}> - <EditableView GetValue={() => checkResult} SetValue={setValue} contents={checkResult} /> + <EditableView GetValue={() => script?.script.run({ value: "", _readOnly_: true }).result} SetValue={setValue} contents={checkResult} /> </div> </div> ); } - render() { const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); - const label = !this.label || !Doc.UserDoc()._showLabel ? (null) : - <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor, position: "absolute" }}> + <div className="fontIconBox-label" style={{ color, backgroundColor, position: "absolute" }}> {this.label} </div>; const menuLabel = !this.label || !Doc.UserDoc()._showMenuLabel ? (null) : - <div className="fontIconBox-label" style={{ color: color, backgroundColor: "transparent" }}> + <div className="fontIconBox-label" style={{ color, backgroundColor: "transparent" }}> {this.label} </div>; - const buttonProps: IButtonProps = { - type: this.type, - rootDoc: this.rootDoc, - label: label, - backgroundColor: backgroundColor, - icon: this.icon, - color: color - }; - const buttonText = StrCast(this.rootDoc.buttonText); // TODO:glr Add label of button type @@ -493,7 +464,7 @@ export class FontIconBox extends DocComponent<ButtonProps, FontIconDocument>(Fon switch (this.type) { case ButtonType.TextButton: button = ( - <div className={`menuButton ${this.type}`} style={{ color: color, backgroundColor: backgroundColor, opacity: 1, gridAutoColumns: `${NumCast(this.rootDoc._height)} auto` }}> + <div className={`menuButton ${this.type}`} style={{ color, backgroundColor, opacity: 1, gridAutoColumns: `${NumCast(this.rootDoc._height)} auto` }}> <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} /> {buttonText ? <div className="button-text"> @@ -522,7 +493,7 @@ export class FontIconBox extends DocComponent<ButtonProps, FontIconDocument>(Fon break; case ButtonType.ToolButton: button = ( - <div className={`menuButton ${this.type}`} style={{ opacity: 1, backgroundColor: backgroundColor, color: color }}> + <div className={`menuButton ${this.type}`} style={{ opacity: 1, backgroundColor, color }}> <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} /> {label} </div> @@ -534,7 +505,7 @@ export class FontIconBox extends DocComponent<ButtonProps, FontIconDocument>(Fon break; case ButtonType.ClickButton: button = ( - <div className={`menuButton ${this.type}`} style={{ color: color, backgroundColor: backgroundColor, opacity: 1 }}> + <div className={`menuButton ${this.type}`} style={{ color, backgroundColor, opacity: 1 }}> <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} /> {label} </div> @@ -544,7 +515,7 @@ export class FontIconBox extends DocComponent<ButtonProps, FontIconDocument>(Fon const trailsIcon = <img src={`/assets/${"presTrails.png"}`} style={{ width: 30, height: 30, filter: `invert(${color === Colors.DARK_GRAY ? "0%" : "100%"})` }} />; button = ( - <div className={`menuButton ${this.type}`} style={{ color: color, backgroundColor: backgroundColor }}> + <div className={`menuButton ${this.type}`} style={{ color, backgroundColor }}> {this.icon === "pres-trail" ? trailsIcon : <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />} {menuLabel} <FontIconBadge collection={Cast(this.rootDoc.watchedDocuments, Doc, null)} /> @@ -658,7 +629,7 @@ Scripting.addGlobal(function setBulletList(mapStyle: "bullet" | "decimal", check // toggle: Set overlay status of selected document Scripting.addGlobal(function setFontColor(color?: string, checkResult?: boolean) { - const editorView = RichTextMenu.Instance.TextView?.EditorView; + const editorView = RichTextMenu.Instance?.TextView?.EditorView; if (checkResult) { return editorView ? RichTextMenu.Instance.fontColor : Doc.UserDoc().fontColor; @@ -687,12 +658,12 @@ Scripting.addGlobal(function setFontHighlight(color?: string, checkResult?: bool // toggle: Set overlay status of selected document Scripting.addGlobal(function setFontSize(size: string | number, checkResult?: boolean) { - if (typeof size === "number") size = size.toString(); - if (size && Number(size).toString() === size) size += "px"; - const editorView = RichTextMenu.Instance.TextView?.EditorView; + const editorView = RichTextMenu.Instance?.TextView?.EditorView; if (checkResult) { return (editorView ? RichTextMenu.Instance.fontSize : StrCast(Doc.UserDoc().fontSize, "10px")).replace("px", ""); } + if (typeof size === "number") size = size.toString(); + if (size && Number(size).toString() === size) size += "px"; if (editorView) RichTextMenu.Instance.setFontSize(size); else Doc.UserDoc()._fontSize = size; }); @@ -810,17 +781,19 @@ Scripting.addGlobal(function webSetURL(url: string, checkResult?: boolean) { //selected.rootDoc.data = new WebField(url); } }); -Scripting.addGlobal(function webForward() { - const selected = SelectionManager.Views().lastElement(); - if (selected?.rootDoc.type === DocumentType.WEB) { - (selected.ComponentView as WebBox).forward(); +Scripting.addGlobal(function webForward(checkResult?: boolean) { + const selected = (SelectionManager.Views().lastElement()?.ComponentView as WebBox); + if (checkResult) { + return selected?.forward(checkResult) ? undefined : "lightGray"; } + selected?.forward(); }); -Scripting.addGlobal(function webBack() { - const selected = SelectionManager.Views().lastElement(); - if (selected?.rootDoc.type === DocumentType.WEB) { - (selected.ComponentView as WebBox).back(); +Scripting.addGlobal(function webBack(checkResult?: boolean) { + const selected = (SelectionManager.Views().lastElement()?.ComponentView as WebBox); + if (checkResult) { + return selected?.back(checkResult) ? undefined : "lightGray"; } + selected?.back(); }); diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 8a5491b4b..0b9a8b3fe 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -520,7 +520,8 @@ export namespace Doc { Doc.SetLayout(alias, Doc.MakeAlias(layout)); } alias.aliasOf = doc; - alias.title = ComputedField.MakeFunction(`renameAlias(this, ${Doc.GetProto(doc).aliasNumber = NumCast(Doc.GetProto(doc).aliasNumber) + 1})`); + alias.aliasNumber = Doc.GetProto(doc).aliasNumber = NumCast(Doc.GetProto(doc).aliasNumber) + 1; + alias.title = ComputedField.MakeFunction(`renameAlias(this)`); alias.author = Doc.CurrentUserEmail; Doc.AddDocToList(Doc.GetProto(doc)[DataSym], "aliases", alias); @@ -1391,7 +1392,7 @@ export namespace Doc { } Scripting.addGlobal(function idToDoc(id: string): any { return DocServer.GetCachedRefField(id); }); -Scripting.addGlobal(function renameAlias(doc: any, n: any) { return StrCast(Doc.GetProto(doc).title).replace(/\([0-9]*\)/, "") + `(${n})`; }); +Scripting.addGlobal(function renameAlias(doc: any) { return StrCast(Doc.GetProto(doc).title).replace(/\([0-9]*\)/, "") + `(${doc.aliasNumber})`; }); Scripting.addGlobal(function getProto(doc: any) { return Doc.GetProto(doc); }); Scripting.addGlobal(function getDocTemplate(doc?: any) { return Doc.getDocTemplate(doc); }); Scripting.addGlobal(function getAlias(doc: any) { return Doc.MakeAlias(doc); }); diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts index bd93bf5fb..85cd73dfe 100644 --- a/src/fields/ScriptField.ts +++ b/src/fields/ScriptField.ts @@ -40,28 +40,9 @@ const scriptSchema = createSimpleSchema({ }); async function deserializeScript(script: ScriptField) { - if (script.script.originalScript === 'copyDragFactory(this.dragFactory)') { - return (script as any).script = (ScriptField.GetCopyOfDragFactory ?? (ScriptField.GetCopyOfDragFactory = ScriptField.MakeFunction('copyDragFactory(this.dragFactory)')))?.script; - } - if (script.script.originalScript === 'links(self)') { - return (script as any).script = (ScriptField.LinksSelf ?? (ScriptField.LinksSelf = ComputedField.MakeFunction('links(self)')))?.script; - } - if (script.script.originalScript === 'openOnRight(copyDragFactory(this.dragFactory))') { - return (script as any).script = (ScriptField.OpenOnRight ?? (ScriptField.OpenOnRight = ComputedField.MakeFunction('openOnRight(copyDragFactory(this.dragFactory))')))?.script; - } - if (script.script.originalScript === 'deiconifyView(self)') { - return (script as any).script = (ScriptField.DeiconifyView ?? (ScriptField.DeiconifyView = ComputedField.MakeFunction('deiconifyView(self)')))?.script; - } - if (script.script.originalScript === 'convertToButtons(dragData)') { - return (script as any).script = (ScriptField.ConvertToButtons ?? (ScriptField.ConvertToButtons = ComputedField.MakeFunction('convertToButtons(dragData)', { dragData: "DocumentDragData" })))?.script; - } - if (script.script.originalScript === 'IsNoviceMode()') { - return (script as any).script = (ScriptField.NoviceMode ?? (ScriptField.NoviceMode = ComputedField.MakeFunction('IsNoviceMode()')))?.script; - } - if (script.script.originalScript === `selectMainMenu(self)`) { - return (script as any).script = (ScriptField.SelectMenu ?? (ScriptField.SelectMenu = ComputedField.MakeFunction('selectMainMenu(self)')))?.script; - } const captures: ProxyField<Doc> = (script as any).captures; + const cache = captures ? undefined : ScriptField.GetScriptFieldCache(script.script.originalScript); + if (cache) return (script as any).script = cache; if (captures) { const doc = (await captures.value())!; const captured: any = {}; @@ -75,6 +56,7 @@ async function deserializeScript(script: ScriptField) { throw new Error("Couldn't compile loaded script"); } (script as any).script = comp; + !captures && ScriptField._scriptFieldCache.set(script.script.originalScript, comp); if (script.setterscript) { const compset = CompileScript(script.setterscript?.originalScript, script.setterscript.options); if (!compset.compiled) { @@ -95,13 +77,9 @@ export class ScriptField extends ObjectField { @serializable(autoObject()) private captures?: ProxyField<Doc>; - public static GetCopyOfDragFactory: Opt<ScriptField>; - public static LinksSelf: Opt<ScriptField>; - public static OpenOnRight: Opt<ScriptField>; - public static DeiconifyView: Opt<ScriptField>; - public static ConvertToButtons: Opt<ScriptField>; - public static NoviceMode: Opt<ScriptField>; - public static SelectMenu: Opt<ScriptField>; + public static _scriptFieldCache: Map<string, Opt<CompiledScript>> = new Map(); + public static GetScriptFieldCache(field: string) { return this._scriptFieldCache.get(field); } + constructor(script: CompiledScript, setterscript?: CompiledScript) { super(); @@ -150,7 +128,13 @@ export class ScriptField extends ObjectField { } public static CompileScript(script: string, params: object = {}, addReturn = false, capturedVariables?: { [name: string]: Field }) { const compiled = CompileScript(script, { - params: { this: Doc?.name || "Doc", self: Doc?.name || "Doc", _last_: "any", ...params }, + params: { + this: Doc?.name || "Doc", // this is the doc that executes the script + self: Doc?.name || "Doc", // self is the root doc of the doc that executes the script + _last_: "any", // _last_ is the previous value of a computed field when it is being triggered to re-run. + _readOnly_: "boolean", // _readOnly_ is set when a computed field is executed to indicate that it should not have mobx side-effects. used for checking the value of a set function (see FontIconBox) + ...params + }, typecheck: false, editable: true, addReturn: addReturn, @@ -175,7 +159,7 @@ export class ComputedField extends ScriptField { _lastComputedResult: any; //TODO maybe add an observable cache based on what is passed in for doc, considering there shouldn't really be that many possible values for doc value = computedFn((doc: Doc) => this._valueOutsideReaction(doc)); - _valueOutsideReaction = (doc: Doc) => this._lastComputedResult = this.script.run({ this: doc, self: Cast(doc.rootDocument, Doc, null) || doc, _last_: this._lastComputedResult }, console.log).result; + _valueOutsideReaction = (doc: Doc) => this._lastComputedResult = this.script.run({ this: doc, self: Cast(doc.rootDocument, Doc, null) || doc, _last_: this._lastComputedResult, _readOnly_: true }, console.log).result; [Copy](): ObjectField { diff --git a/src/fields/Types.ts b/src/fields/Types.ts index 3d784448d..220a30fe4 100644 --- a/src/fields/Types.ts +++ b/src/fields/Types.ts @@ -3,6 +3,7 @@ import { List } from "./List"; import { RefField } from "./RefField"; import { DateField } from "./DateField"; import { ScriptField } from "./ScriptField"; +import { URLField, WebField, ImageField } from "./URLField"; export type ToType<T extends InterfaceValue> = T extends "string" ? string : @@ -91,6 +92,12 @@ export function DateCast(field: FieldResult) { export function ScriptCast(field: FieldResult, defaultVal: ScriptField | null = null) { return Cast(field, ScriptField, defaultVal); } +export function WebCast(field: FieldResult, defaultVal: WebField | null = null) { + return Cast(field, WebField, defaultVal); +} +export function ImageCast(field: FieldResult, defaultVal: ImageField | null = null) { + return Cast(field, ImageField, defaultVal); +} type WithoutList<T extends Field> = T extends List<infer R> ? (R extends RefField ? (R | Promise<R>)[] : R[]) : T; diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts index 02f6462aa..597ff1ce0 100644 --- a/src/server/ApiManagers/UploadManager.ts +++ b/src/server/ApiManagers/UploadManager.ts @@ -255,14 +255,15 @@ export default class UploadManager extends ApiManager { secureHandler: ({ req, res }) => { const uri = req.body.uri; const filename = req.body.name; + const origSuffix = req.body.nosuffix ? SizeSuffix.None : SizeSuffix.Original; if (!uri || !filename) { res.status(401).send("incorrect parameters specified"); return; } - return imageDataUri.outputFile(uri, serverPathToFile(Directory.images, InjectSize(filename, SizeSuffix.Original))).then((savedName: string) => { + return imageDataUri.outputFile(uri, serverPathToFile(Directory.images, InjectSize(filename, origSuffix))).then((savedName: string) => { const ext = extname(savedName).toLowerCase(); const { pngs, jpgs } = AcceptableMedia; - const resizers = [ + const resizers = !origSuffix ? [{ resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: "_m" }] : [ { resizer: sharp().resize(100, undefined, { withoutEnlargement: true }), suffix: "_s" }, { resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: "_m" }, { resizer: sharp().resize(900, undefined, { withoutEnlargement: true }), suffix: "_l" }, diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts index f13580865..800717d99 100644 --- a/src/server/DashUploadUtils.ts +++ b/src/server/DashUploadUtils.ts @@ -25,7 +25,8 @@ export enum SizeSuffix { Small = "_s", Medium = "_m", Large = "_l", - Original = "_o" + Original = "_o", + None = "" } export function InjectSize(filename: string, size: SizeSuffix) { diff --git a/src/server/server_Initialization.ts b/src/server/server_Initialization.ts index de93b64c3..d1b9d8cf2 100644 --- a/src/server/server_Initialization.ts +++ b/src/server/server_Initialization.ts @@ -92,6 +92,13 @@ function buildWithMiddleware(server: express.Express) { passport.session(), (req: express.Request, res: express.Response, next: express.NextFunction) => { res.locals.user = req.user; + if (req.originalUrl.endsWith(".png") && req.method === 'GET') { + const period = 30000; + res.set('Cache-control', `public, max-age=${period}`); + } else { + // for the other requests set strict no caching parameters + res.set('Cache-control', `no-store`); + } next(); } ].forEach(next => server.use(next)); @@ -178,11 +185,21 @@ function proxyServe(req: any, requrl: string, response: any) { return `href="${resolvedServerUrl + "/corsProxy/http" + href}"`; }; const zipToStringDecoder = new (require('string_decoder').StringDecoder)('utf8'); - const htmlText = zipToStringDecoder.write(zlib.gunzipSync(htmlBodyMemoryStream.read()).toString('utf8') - .replace('<head>', '<head> <style>[id ^= "google"] { display: none; } </style>') - .replace(/href="http([^"]*)"/g, replacer) - .replace(/target="_blank"/g, "")); - rewrittenHtmlBody = zlib.gzipSync(htmlText); + // const htmlText = zipToStringDecoder.write(zlib.gunzipSync(htmlBodyMemoryStream.read()).toString('utf8') + // .replace('<head>', '<head> <style>[id ^= "google"] { display: none; } </style>') + // .replace(/href="http([^"]*)"/g, replacer) + // .replace(/target="_blank"/g, "")); + // rewrittenHtmlBody = zlib.gzipSync(htmlText); + const bodyStream = htmlBodyMemoryStream.read(); + if (bodyStream) { + const htmlText = zipToStringDecoder.write(zlib.gunzipSync(bodyStream).toString('utf8') + .replace('<head>', '<head> <style>[id ^= "google"] { display: none; } </style>') + // .replace(/href="http([^"]*)"/g, replacer) + .replace(/target="_blank"/g, "")); + rewrittenHtmlBody = zlib.gzipSync(htmlText); + } else { + console.log("EMPTY body: href"); + } } catch (e) { console.log(e); } } }); |