aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2022-02-09 08:59:32 -0500
committerbobzel <zzzman@gmail.com>2022-02-09 08:59:32 -0500
commita6d904bcd18a2c9962abfd9b5b325340f6b18b0d (patch)
tree32ab8f94e8f742d9303ec06029247931f1ab3895 /src
parent66fb0e4bbf88a5e9bcb5869dd7c8b8a7714de024 (diff)
speeding up rendering using bitmaps for webpages and other heavyweight docs.
Diffstat (limited to 'src')
-rw-r--r--src/.DS_Storebin8196 -> 8196 bytes
-rw-r--r--src/client/documents/Documents.ts64
-rw-r--r--src/client/util/CurrentUserUtils.ts64
-rw-r--r--src/client/util/Scripting.ts1
-rw-r--r--src/client/views/PropertiesButtons.tsx19
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx23
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx9
-rw-r--r--src/client/views/nodes/DocumentView.tsx15
-rw-r--r--src/client/views/nodes/VideoBox.tsx5
-rw-r--r--src/client/views/nodes/WebBox.tsx61
-rw-r--r--src/client/views/nodes/WebBoxRenderer.js395
-rw-r--r--src/fields/Doc.ts5
-rw-r--r--src/fields/ScriptField.ts32
-rw-r--r--src/fields/Types.ts7
-rw-r--r--src/server/ApiManagers/UploadManager.ts5
-rw-r--r--src/server/server_Initialization.ts20
16 files changed, 624 insertions, 101 deletions
diff --git a/src/.DS_Store b/src/.DS_Store
index bdc161da2..04487bc70 100644
--- a/src/.DS_Store
+++ b/src/.DS_Store
Binary files differ
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index df573a377..0cb4f4c5e 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -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)" }
}],
]);
@@ -591,7 +594,12 @@ export namespace Docs {
const options: DocumentOptions = {
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..9ecb2c277 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -162,37 +162,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;
@@ -1010,7 +1010,7 @@ export class CurrentUserUtils {
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: "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)' },
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index 40b94024e..66cc7766f 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -269,6 +269,7 @@ function forEachNode(node: ts.Node, onEnter: Traverser, onExit?: Traverser, inde
}
export function CompileScript(script: string, options: ScriptOptions = {}): CompileResult {
+ console.log("script = " + script);
const { requiredType = "", addReturn = false, params = {}, capturedVariables = {}, typecheck = true } = options;
if (options.params && !options.params.this) options.params.this = Doc.name;
if (options.params && !options.params.self) options.params.self = Doc.name;
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/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..28a65f628 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -154,6 +154,14 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
returnThis = () => this;
render() {
TraceMobx();
+
+ const panx = this.props.CollectionFreeFormView.panX();
+ const pany = this.props.CollectionFreeFormView.panY();
+ const viewWidth = this.props.CollectionFreeFormView.props.PanelWidth() / this.props.CollectionFreeFormView.zoomScaling() / 2;
+ const viewHeight = this.props.CollectionFreeFormView.props.PanelHeight() / this.props.CollectionFreeFormView.zoomScaling() / 2;
+ const hideContent = !this.props.CollectionFreeFormView.props.isAnnotationOverlay &&
+ (Math.min(Math.abs(panx - (this.X + this.panelWidth())), Math.abs(panx - (this.X))) > viewWidth ||
+ Math.min(Math.abs(pany - (this.Y + this.panelHeight())), Math.abs(pany - (this.Y))) > viewHeight) ? true : false;
const divProps: DocumentViewProps = {
...this.props,
CollectionFreeFormDocumentView: this.returnThis,
@@ -162,6 +170,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
ScreenToLocalTransform: this.screenToLocalTransform,
PanelWidth: this.panelWidth,
PanelHeight: this.panelHeight,
+ hideContent
};
const background = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
const mixBlendMode = StrCast(this.layoutDoc.mixBlendMode) as any || (typeof background === "string" && DashColor(background).alpha() !== 1 ? "multiply" : undefined);
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 9fc4b7890..a83fba37d 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -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';
@@ -148,6 +148,7 @@ export interface DocumentViewSharedProps {
export interface DocumentViewProps extends DocumentViewSharedProps {
// properties specific to DocumentViews but not to FieldView
freezeDimensions?: boolean;
+ hideContent?: boolean; // whether to show content or not (used to speed up rendering complex scenes when documents are not in view)
hideResizeHandles?: boolean; // whether to suppress DocumentDecorations when this document is selected
hideTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings
hideDecorationTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings
@@ -184,6 +185,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 +241,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 => {
@@ -837,6 +840,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
}
@computed get contents() {
TraceMobx();
+ const thumb = ImageCast(this.layoutDoc["thumb-frozen"], ImageCast(this.layoutDoc["thumb"]))?.url.href;
const audioView = !this.layoutDoc._showAudio ? (null) :
<div className="documentView-audioBackground"
onPointerDown={this.recordAudioAnnotation}
@@ -864,6 +868,9 @@ 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)})` }}>
@@ -1047,9 +1054,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 +1072,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}
@@ -1079,7 +1088,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
highlighting = highlighting && this.props.focus !== emptyFunction && this.layoutDoc.title !== "[pres element template]"; // bcz: hack to turn off highlighting onsidebar panel documents. need to flag a document as not highlightable in a more direct way
const borderPath = this.props.styleProvider?.(this.props.Document, this.props, StyleProp.BorderPath) || { path: undefined };
- const internal = PresBox.EffectsProvider(this.layoutDoc, this.renderDoc) || this.renderDoc;
+ const internal = this.props.hideContent ? (null) : PresBox.EffectsProvider(this.layoutDoc, this.renderDoc) || this.renderDoc;
const boxShadow = this.props.treeViewDoc ? null : highlighting && this.borderRounding && highlightStyle !== "dashed" ? `0 0 0 ${highlightIndex}px ${highlightColor}` :
this.boxShadow || (this.props.Document.isTemplateForField ? "black 0.2vw 0.2vw 0.8vw" : undefined);
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..a1910c722 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 } 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);
@@ -76,6 +78,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);
@@ -108,9 +111,47 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
runInAction(() => {
this._annotationKeySuffix = () => this._urlHash + "-annotations";
// bcz: need to make sure that doc.data-annotations points to the currently active web page's annotations (this could/should be when the doc is created)
- 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"`);
+ this.dataDoc[this.fieldKey + "-annotations"] = ComputedField.GetAnnoCopyField(this.fieldKey);
+ this.dataDoc[this.fieldKey + "-sidebar"] = ComputedField.GetSidebarCopyField(this.fieldKey);
});
+ reaction(() => this.props.isSelected(),
+ async (selected) => {
+ if (selected) {
+ 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();
+ }
+ 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
+ (action((dataUrl: any) => {
+ VideoBox.convertDataUri(dataUrl, this.layoutDoc[Id] + "-thumb", true).then(
+ returnedfilename => this.layoutDoc.thumb = new ImageField(returnedfilename));
+ }))
+ .catch(function (error: any) {
+ console.error('oops, something went wrong!', error);
+ });
+ }
+ }
+ });
this._disposers.autoHeight = reaction(() => this.layoutDoc._autoHeight,
autoHeight => {
@@ -292,12 +333,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 = "";
@@ -665,7 +700,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
return (
<div className="webBox" ref={this._mainCont}
style={{ pointerEvents: this.pointerEvents() }} >
- <div className={`webBox-container`} style={{ pointerEvents }} onContextMenu={this.specificContextMenu}>
+ <div hidden={!this.props.isSelected() && !this.isAnyChildContentActive() ? true : false}
+ 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 +739,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("&gt;", ">").replace("&lt;", "<");
+
+ const styleElemString = new XMLSerializer().serializeToString(styleElem).replace(/&gt;/g, ">").replace(/&lt;/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/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..c84eb7d88 100644
--- a/src/fields/ScriptField.ts
+++ b/src/fields/ScriptField.ts
@@ -40,11 +40,21 @@ const scriptSchema = createSimpleSchema({
});
async function deserializeScript(script: ScriptField) {
+ //console.log("Scripting = " + script.script.originalScript)
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.startsWith("renameAlias")) { // === 'renameAlias(this)') {
+ return (script as any).script = (ScriptField.RenameAlias as Opt<ScriptField>)?.script;
+ }
+ if (script.script.originalScript.startsWith("copyField") && script.script.originalScript.endsWith("annotations\"")) {
+ return (script as any).script = ScriptField.GetAnnoCopyField(script.script.originalScript.replace(/.*\["([^"]*)/, "$1"))?.script;
+ }
+ if (script.script.originalScript.startsWith("copyField") && script.script.originalScript.endsWith("sidebar\"")) {
+ return (script as any).script = ScriptField.GetSidebarCopyField(script.script.originalScript.replace(/.*\["([^"]*)/, "$1"))?.script;
+ }
if (script.script.originalScript === 'links(self)') {
- return (script as any).script = (ScriptField.LinksSelf ?? (ScriptField.LinksSelf = ComputedField.MakeFunction('links(self)')))?.script;
+ return (script as any).script = (ScriptField.LinksSelf as Opt<ScriptField>)?.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;
@@ -56,7 +66,7 @@ async function deserializeScript(script: ScriptField) {
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;
+ return (script as any).script = (ScriptField.NoviceMode as Opt<ScriptField>)?.script;
}
if (script.script.originalScript === `selectMainMenu(self)`) {
return (script as any).script = (ScriptField.SelectMenu ?? (ScriptField.SelectMenu = ComputedField.MakeFunction('selectMainMenu(self)')))?.script;
@@ -96,11 +106,25 @@ export class ScriptField extends ObjectField {
private captures?: ProxyField<Doc>;
public static GetCopyOfDragFactory: Opt<ScriptField>;
- public static LinksSelf: Opt<ScriptField>;
+ public static _annoCopyField: Map<string, Opt<ScriptField>> = new Map();
+ public static GetAnnoCopyField(field: string) {
+ if (!this._annoCopyField.get(field)) this._annoCopyField.set(field, ComputedField.MakeFunction(`copyField(this["${field}-"+urlHash(this["${field}"]?.url?.toString())+"-annotations"`))
+ return this._annoCopyField.get(field)?.[Copy];
+ }
+ public static _sidebarCopyField: Map<string, Opt<ScriptField>> = new Map();
+ public static GetSidebarCopyField(field: string) {
+ if (!this._sidebarCopyField.get(field)) this._sidebarCopyField.set(field, ComputedField.MakeFunction(`copyField(this["${field}-"+urlHash(this["${field}"]?.url?.toString())+"-sidebar"`))
+ return this._sidebarCopyField.get(field)?.[Copy];
+ }
+ public static _renameAlias: Opt<ScriptField>;
+ public static get RenameAlias() { return (this._renameAlias ?? (this._renameAlias = ComputedField.MakeFunction('renameAlias(this)'))); }
+ public static _linksSelf: Opt<ScriptField>;
+ public static get LinksSelf() { return (this._linksSelf ?? (this._linksSelf = ComputedField.MakeFunction('links(self)'))); }
public static OpenOnRight: Opt<ScriptField>;
public static DeiconifyView: Opt<ScriptField>;
public static ConvertToButtons: Opt<ScriptField>;
- public static NoviceMode: Opt<ScriptField>;
+ public static _noviceMode: Opt<ScriptField>;
+ public static get NoviceMode() { return (this._noviceMode ?? (this._noviceMode = ComputedField.MakeFunction("IsNoviceMode()"))); }
public static SelectMenu: Opt<ScriptField>;
constructor(script: CompiledScript, setterscript?: CompiledScript) {
super();
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..102e9fd5c 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.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(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/server_Initialization.ts b/src/server/server_Initialization.ts
index de93b64c3..b06437907 100644
--- a/src/server/server_Initialization.ts
+++ b/src/server/server_Initialization.ts
@@ -178,11 +178,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); }
}
});