diff options
author | geireann <60007097+geireann@users.noreply.github.com> | 2020-06-17 19:13:19 +0800 |
---|---|---|
committer | geireann <60007097+geireann@users.noreply.github.com> | 2020-06-17 19:13:19 +0800 |
commit | 79d9ccbe68d0e567eba1daae0500d9ddba177b1b (patch) | |
tree | 2782768aabf6b9b33c27294779aeb66bd0e1b0d8 | |
parent | 3ccc8e92682bfa257ef2052513b1826b811f6881 (diff) | |
parent | c2349c0b00ae15e2aa715e0b535a88d5a3d89f5e (diff) |
Merge branch 'master' into mobile_revision_direct
50 files changed, 1887 insertions, 1308 deletions
diff --git a/deploy/assets/cheat-sheet.pdf b/deploy/assets/cheat-sheet.pdf Binary files differnew file mode 100644 index 000000000..fcc5484ea --- /dev/null +++ b/deploy/assets/cheat-sheet.pdf diff --git a/package-lock.json b/package-lock.json index e692b266d..69e8b19f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -412,15 +412,6 @@ "integrity": "sha512-t7uW6eFafjO+qJ3BIV2gGUyZs27egcNRkUdalkud+Qa3+kg//f129iuOFivHDXQ+vnU3fDXuwgv0cqMCbcE8sw==", "dev": true }, - "@types/chrome": { - "version": "0.0.114", - "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.114.tgz", - "integrity": "sha512-i7qRr74IrxHtbnrZSKUuP5Uvd5EOKwlwJq/yp7+yTPihOXnPhNQO4Z5bqb1XTnrjdbUKEJicaVVbhcgtRijmLA==", - "requires": { - "@types/filesystem": "*", - "@types/har-format": "*" - } - }, "@types/color": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/color/-/color-3.0.1.tgz", @@ -556,19 +547,6 @@ "express-validator": "*" } }, - "@types/filesystem": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.29.tgz", - "integrity": "sha512-85/1KfRedmfPGsbK8YzeaQUyV1FQAvMPMTuWFQ5EkLd2w7szhNO96bk3Rh/SKmOfd9co2rCLf0Voy4o7ECBOvw==", - "requires": { - "@types/filewriter": "*" - } - }, - "@types/filewriter": { - "version": "0.0.28", - "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.28.tgz", - "integrity": "sha1-wFTor02d11205jq8dviFFocU1LM=" - }, "@types/formidable": { "version": "1.0.31", "resolved": "https://registry.npmjs.org/@types/formidable/-/formidable-1.0.31.tgz", @@ -606,11 +584,6 @@ "integrity": "sha512-L8O9HAVFZj0TuiS8h5ORthiMsrrhjxTC8XUusp5k47oXCst4VTm+qWKvrAvmYMybZVokbp4Udco1mNwJrTNZPQ==", "dev": true }, - "@types/har-format": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.4.tgz", - "integrity": "sha512-iUxzm1meBm3stxUMzRqgOVHjj4Kgpgu5w9fm4X7kPRfSgVRzythsucEN7/jtOo8SQzm+HfcxWWzJS0mJDH/3DQ==" - }, "@types/jquery": { "version": "3.3.36", "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.36.tgz", @@ -1201,6 +1174,14 @@ } } }, + "@types/webscopeio__react-textarea-autocomplete": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@types/webscopeio__react-textarea-autocomplete/-/webscopeio__react-textarea-autocomplete-4.6.1.tgz", + "integrity": "sha512-rdiDMsTbyFJRbC2BYKIgiAFG/SFkaDGYQYfzo3U2T+EjMCE0JZ8IHZ9nZrJ2Tm83Blrj66cnaEc0H3iwwRqQMA==", + "requires": { + "@types/react": "*" + } + }, "@types/xregexp": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@types/xregexp/-/xregexp-4.3.0.tgz", @@ -1397,6 +1378,22 @@ "@xtuc/long": "4.2.2" } }, + "@webscopeio/react-textarea-autocomplete": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/@webscopeio/react-textarea-autocomplete/-/react-textarea-autocomplete-4.6.3.tgz", + "integrity": "sha512-09aVXwhxIcfpU3Qyx5zxAec73BDE/32BGTWZkXDCB5yZEzU5Qnw++68Lvv/Z/oB+ifqFsG9JOoXf2S0lz6mwcg==", + "requires": { + "custom-event": "^1.0.1", + "textarea-caret": "3.0.2" + }, + "dependencies": { + "textarea-caret": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/textarea-caret/-/textarea-caret-3.0.2.tgz", + "integrity": "sha1-82DEhpmqGr9xhoCkOjGoUGZcLK8=" + } + } + }, "@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -4231,6 +4228,11 @@ "array-find-index": "^1.0.1" } }, + "custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=" + }, "cyclist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", @@ -16175,6 +16177,11 @@ } } }, + "textarea-caret": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/textarea-caret/-/textarea-caret-3.1.0.tgz", + "integrity": "sha512-cXAvzO9pP5CGa6NKx0WYHl+8CHKZs8byMkt3PCJBCmq2a34YA9pO1NrQET5pzeqnBjBdToF5No4rrmkDUgQC2Q==" + }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", diff --git a/package.json b/package.json index b9d77b923..a9f5d872a 100644 --- a/package.json +++ b/package.json @@ -18,11 +18,6 @@ "tsc": "tsc" }, "devDependencies": { - "@types/chai": "^4.2.7", - "@types/mocha": "^5.2.6", - "@types/react-dom": "^16.9.5", - "@types/webpack-dev-middleware": "^2.0.2", - "@types/webpack-hot-middleware": "^2.25.0", "@types/adm-zip": "^0.4.32", "@types/animejs": "^2.0.2", "@types/archiver": "^3.0.0", @@ -30,6 +25,7 @@ "@types/bcrypt-nodejs": "0.0.30", "@types/bluebird": "^3.5.29", "@types/body-parser": "^1.17.1", + "@types/chai": "^4.2.7", "@types/color": "^3.0.1", "@types/connect-flash": "0.0.34", "@types/cookie-parser": "^1.4.2", @@ -46,6 +42,7 @@ "@types/libxmljs": "^0.18.5", "@types/lodash": "^4.14.149", "@types/mobile-detect": "^1.3.4", + "@types/mocha": "^5.2.6", "@types/mongodb": "^3.3.14", "@types/mongoose": "^5.5.43", "@types/node": "^10.17.13", @@ -69,6 +66,7 @@ "@types/react": "^16.9.19", "@types/react-autosuggest": "^9.3.13", "@types/react-color": "^2.17.3", + "@types/react-dom": "^16.9.5", "@types/react-grid-layout": "^0.17.1", "@types/react-measure": "^2.0.6", "@types/react-table": "^6.8.6", @@ -83,6 +81,8 @@ "@types/uuid": "^3.4.6", "@types/valid-url": "^1.0.3", "@types/webpack": "^4.41.3", + "@types/webpack-dev-middleware": "^2.0.2", + "@types/webpack-hot-middleware": "^2.25.0", "@types/xregexp": "^4.3.0", "@types/youtube": "0.0.39", "awesome-typescript-loader": "^5.2.1", @@ -118,7 +118,8 @@ "@hig/flyout": "^1.2.0", "@hig/theme-context": "^2.1.3", "@hig/theme-data": "^2.13.0", - "@types/chrome": "0.0.114", + "@types/webscopeio__react-textarea-autocomplete": "^4.6.1", + "@webscopeio/react-textarea-autocomplete": "^4.6.3", "adm-zip": "^0.4.13", "archiver": "^3.1.1", "array-batcher": "^1.2.3", @@ -224,6 +225,7 @@ "socket.io-client": "^2.3.0", "solr-node": "^1.2.1", "standard-http-error": "^2.0.1", + "textarea-caret": "^3.1.0", "typescript-collections": "^1.3.3", "typescript-language-server": "^0.4.0", "url-loader": "^1.1.2", diff --git a/src/.DS_Store b/src/.DS_Store Binary files differdeleted file mode 100644 index 5b35884bd..000000000 --- a/src/.DS_Store +++ /dev/null diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts index 06d35038a..7ba21b2f6 100644 --- a/src/client/documents/DocumentTypes.ts +++ b/src/client/documents/DocumentTypes.ts @@ -34,5 +34,6 @@ export enum DocumentType { COMPARISON = "comparison", // before/after view with slider (view of 2 images) LINKDB = "linkdb", // database of links ??? why do we have this + SCRIPTDB = "scriptdb", // database of scripts RECOMMENDATION = "recommendation", // view of a recommendation }
\ No newline at end of file diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 002bfe6a2..56ed6febb 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -1,54 +1,53 @@ -import { CollectionView } from "../views/collections/CollectionView"; -import { CollectionViewType } from "../views/collections/CollectionView"; -import { AudioBox } from "../views/nodes/AudioBox"; -import { FormattedTextBox } from "../views/nodes/formattedText/FormattedTextBox"; -import { ImageBox } from "../views/nodes/ImageBox"; -import { KeyValueBox } from "../views/nodes/KeyValueBox"; -import { PDFBox } from "../views/nodes/PDFBox"; -import { ScriptingBox } from "../views/nodes/ScriptingBox"; -import { VideoBox } from "../views/nodes/VideoBox"; -import { WebBox } from "../views/nodes/WebBox"; -import { OmitKeys, JSONUtils, Utils } from "../../Utils"; -import { Field, Doc, Opt, DocListCastAsync, FieldResult, DocListCast, HeightSym, WidthSym } from "../../fields/Doc"; -import { ImageField, VideoField, AudioField, PdfField, WebField, YoutubeField } from "../../fields/URLField"; +import { runInAction } from "mobx"; +import { extname } from "path"; +import { DateField } from "../../fields/DateField"; +import { Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym } from "../../fields/Doc"; import { HtmlField } from "../../fields/HtmlField"; +import { InkField } from "../../fields/InkField"; import { List } from "../../fields/List"; -import { Cast, NumCast, StrCast, FieldValue } from "../../fields/Types"; +import { ProxyField } from "../../fields/Proxy"; +import { RichTextField } from "../../fields/RichTextField"; +import { SchemaHeaderField } from "../../fields/SchemaHeaderField"; +import { ComputedField, ScriptField } from "../../fields/ScriptField"; +import { Cast, NumCast, StrCast } from "../../fields/Types"; +import { AudioField, ImageField, PdfField, VideoField, WebField, YoutubeField } from "../../fields/URLField"; +import { MessageStore } from "../../server/Message"; +import { OmitKeys, Utils } from "../../Utils"; import { DocServer } from "../DocServer"; import { dropActionType } from "../util/DragManager"; -import { DateField } from "../../fields/DateField"; -import { YoutubeBox } from "../apis/youtube/YoutubeBox"; -import { CollectionDockingView } from "../views/collections/CollectionDockingView"; import { LinkManager } from "../util/LinkManager"; -import { DocumentManager } from "../util/DocumentManager"; -import DirectoryImportBox from "../util/Import & Export/DirectoryImportBox"; import { Scripting } from "../util/Scripting"; -import { LabelBox } from "../views/nodes/LabelBox"; -import { SliderBox } from "../views/nodes/SliderBox"; -import { FontIconBox } from "../views/nodes/FontIconBox"; -import { SchemaHeaderField } from "../../fields/SchemaHeaderField"; -import { PresBox } from "../views/nodes/PresBox"; -import { ComputedField, ScriptField } from "../../fields/ScriptField"; -import { ProxyField } from "../../fields/Proxy"; +import { UndoManager } from "../util/UndoManager"; import { DocumentType } from "./DocumentTypes"; -import { RecommendationsBox } from "../views/RecommendationsBox"; -import { PresElementBox } from "../views/presentationview/PresElementBox"; -import { DashWebRTCVideo } from "../views/webcam/DashWebRTCVideo"; -import { QueryBox } from "../views/nodes/QueryBox"; +import { CollectionDockingView } from "../views/collections/CollectionDockingView"; +import { CollectionView, CollectionViewType } from "../views/collections/CollectionView"; +import { ContextMenu } from "../views/ContextMenu"; +import { ContextMenuProps } from "../views/ContextMenuItem"; +import { ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, InkingStroke } from "../views/InkingStroke"; +import { AudioBox } from "../views/nodes/AudioBox"; import { ColorBox } from "../views/nodes/ColorBox"; +import { ComparisonBox } from "../views/nodes/ComparisonBox"; import { DocHolderBox } from "../views/nodes/DocHolderBox"; -import { InkingStroke, ActiveInkColor, ActiveInkWidth, ActiveInkBezierApprox } from "../views/InkingStroke"; -import { InkField } from "../../fields/InkField"; -import { RichTextField } from "../../fields/RichTextField"; -import { extname } from "path"; -import { MessageStore } from "../../server/Message"; -import { ContextMenuProps } from "../views/ContextMenuItem"; -import { ContextMenu } from "../views/ContextMenu"; +import { FontIconBox } from "../views/nodes/FontIconBox"; +import { FormattedTextBox } from "../views/nodes/formattedText/FormattedTextBox"; +import { ImageBox } from "../views/nodes/ImageBox"; +import { KeyValueBox } from "../views/nodes/KeyValueBox"; +import { LabelBox } from "../views/nodes/LabelBox"; import { LinkBox } from "../views/nodes/LinkBox"; +import { PDFBox } from "../views/nodes/PDFBox"; +import { PresBox } from "../views/nodes/PresBox"; +import { QueryBox } from "../views/nodes/QueryBox"; import { ScreenshotBox } from "../views/nodes/ScreenshotBox"; -import { ComparisonBox } from "../views/nodes/ComparisonBox"; -import { runInAction } from "mobx"; -import { UndoManager } from "../util/UndoManager"; +import { ScriptingBox } from "../views/nodes/ScriptingBox"; +import { SliderBox } from "../views/nodes/SliderBox"; +import { VideoBox } from "../views/nodes/VideoBox"; +import { WebBox } from "../views/nodes/WebBox"; +import { PresElementBox } from "../views/presentationview/PresElementBox"; +import { RecommendationsBox } from "../views/RecommendationsBox"; +import { DashWebRTCVideo } from "../views/webcam/DashWebRTCVideo"; +import { YoutubeBox } from "../apis/youtube/YoutubeBox"; +import { DocumentManager } from "../util/DocumentManager"; +import { DirectoryImportBox } from "../util/Import & Export/DirectoryImportBox"; const path = require('path'); export interface DocumentOptions { @@ -262,6 +261,11 @@ export namespace Docs { layout: { view: EmptyBox, dataField: defaultDataKey }, options: { childDropAction: "alias", title: "Global Link Database" } }], + [DocumentType.SCRIPTDB, { + data: new List<Doc>(), + layout: { view: EmptyBox, dataField: defaultDataKey }, + options: { childDropAction: "alias", title: "Global Script Database" } + }], [DocumentType.SCRIPTING, { layout: { view: ScriptingBox, dataField: defaultDataKey } }], @@ -361,6 +365,13 @@ export namespace Docs { } /** + * A collection of all scripts in the database + */ + export function MainScriptDocument() { + return Prototypes.get(DocumentType.SCRIPTDB); + } + + /** * This is a convenience method that is used to initialize * prototype documents for the first time. * @@ -611,7 +622,10 @@ export namespace Docs { } export function LinkDocument(source: { doc: Doc, ctx?: Doc }, target: { doc: Doc, ctx?: Doc }, options: DocumentOptions = {}, id?: string) { - const doc = InstanceFromProto(Prototypes.get(DocumentType.LINK), undefined, { isLinkButton: true, treeViewHideTitle: true, treeViewOpen: false, removeDropProperties: new List(["isBackground", "isLinkButton"]), ...options }); + const doc = InstanceFromProto(Prototypes.get(DocumentType.LINK), undefined, { + isLinkButton: true, treeViewHideTitle: true, treeViewOpen: false, + removeDropProperties: new List(["isBackground", "isLinkButton"]), ...options + }, id); const linkDocProto = Doc.GetProto(doc); linkDocProto.anchor1 = source.doc; linkDocProto.anchor2 = target.doc; @@ -723,6 +737,11 @@ export namespace Docs { } export function ButtonDocument(options?: DocumentOptions) { + // const btn = InstanceFromProto(Prototypes.get(DocumentType.BUTTON), undefined, { ...(options || {}), "onClick-rawScript": "-script-" }); + // btn.layoutKey = "layout_onClick"; + // btn.height = 250; + // btn.width = 200; + // btn.layout_onClick = ScriptingBox.LayoutString("onClick"); return InstanceFromProto(Prototypes.get(DocumentType.BUTTON), undefined, { ...(options || {}), "onClick-rawScript": "-script-" }); } @@ -824,7 +843,7 @@ export namespace DocUtils { const linkDoc = Docs.Create.LinkDocument(source, target, { linkRelationship, layoutKey: "layout_linkView" }, id); linkDoc.layout_linkView = Cast(Cast(Doc.UserDoc()["template-button-link"], Doc, null).dragFactory, Doc, null); - Doc.GetProto(linkDoc).title = ComputedField.MakeFunction('self.anchor1.title +" (" + (self.linkRelationship||"to") +") " + self.anchor2.title'); + Doc.GetProto(linkDoc).title = ComputedField.MakeFunction('self.anchor1?.title +" (" + (self.linkRelationship||"to") +") " + self.anchor2?.title'); Doc.GetProto(source.doc).links = ComputedField.MakeFunction("links(self)"); Doc.GetProto(target.doc).links = ComputedField.MakeFunction("links(self)"); diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 9ce162897..7b867ed02 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -808,6 +808,7 @@ export class CurrentUserUtils { this.setupDefaultPresentation(doc); // presentation that's initially triggered await this.setupSidebarButtons(doc); // the pop-out left sidebar of tools/panels doc.globalLinkDatabase = Docs.Prototypes.MainLinkDocument(); + doc.globalScriptDatabase = Docs.Prototypes.MainScriptDocument(); // setup reactions to change the highlights on the undo/redo buttons -- would be better to encode this in the undo/redo buttons, but the undo/redo stacks are not wired up that way yet doc["dockedBtn-undo"] && reaction(() => UndoManager.undoStack.slice(), () => Doc.GetProto(doc["dockedBtn-undo"] as Doc).opacity = UndoManager.CanUndo() ? 1 : 0.4, { fireImmediately: true }); @@ -840,6 +841,9 @@ export class CurrentUserUtils { } } -Scripting.addGlobal(function setupMobileInkingDoc(userDoc: Doc) { return CurrentUserUtils.setupMobileInkingDoc(userDoc); }); -Scripting.addGlobal(function setupMobileUploadDoc(userDoc: Doc) { return CurrentUserUtils.setupMobileUploadDoc(userDoc); }); -Scripting.addGlobal(function createNewWorkspace() { return MainView.Instance.createNewWorkspace(); }); +Scripting.addGlobal(function setupMobileInkingDoc(userDoc: Doc) { return CurrentUserUtils.setupMobileInkingDoc(userDoc); }, + "initializes the Mobile inking document", "(userDoc: Doc)"); +Scripting.addGlobal(function setupMobileUploadDoc(userDoc: Doc) { return CurrentUserUtils.setupMobileUploadDoc(userDoc); }, + "initializes the Mobile upload document", "(userDoc: Doc)"); +Scripting.addGlobal(function createNewWorkspace() { return MainView.Instance.createNewWorkspace(); }, + "creates a new workspace when called"); diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 597b72e0c..21564c92e 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -134,7 +134,6 @@ export namespace DragManager { dropAction: dropActionType; removeDropProperties?: string[]; userDropAction: dropActionType; - embedDoc?: boolean; moveDocument?: MoveFunction; removeDocument?: RemoveFunction; isSelectionMove?: boolean; // indicates that an explicitly selected Document is being dragged. this will suppress onDragStart scripts diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts index 752c1cfc5..ea1769d85 100644 --- a/src/client/util/DropConverter.ts +++ b/src/client/util/DropConverter.ts @@ -76,4 +76,5 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) { data.droppedDocuments[i] = dbox; }); } -Scripting.addGlobal(function convertToButtons(dragData: any) { convertDropDataToButtons(dragData as DragManager.DocumentDragData); });
\ No newline at end of file +Scripting.addGlobal(function convertToButtons(dragData: any) { convertDropDataToButtons(dragData as DragManager.DocumentDragData); }, + "converts the dropped data to buttons", "(dragData: any)");
\ No newline at end of file diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx index 25c556697..af6c57e68 100644 --- a/src/client/util/Import & Export/DirectoryImportBox.tsx +++ b/src/client/util/Import & Export/DirectoryImportBox.tsx @@ -1,33 +1,33 @@ -import "fs"; -import React = require("react"); -import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../fields/Doc"; -import { action, observable, runInAction, computed, reaction, IReactionDisposer } from "mobx"; -import { FieldViewProps, FieldView } from "../../views/nodes/FieldView"; -import Measure, { ContentRect } from "react-measure"; import { library } from '@fortawesome/fontawesome-svg-core'; +import { faCloudUploadAlt, faPlus, faTag } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faTag, faPlus, faCloudUploadAlt } from '@fortawesome/free-solid-svg-icons'; -import { Docs, DocumentOptions, DocUtils } from "../../documents/Documents"; +import { BatchedArray } from "array-batcher"; +import "fs"; +import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; -import ImportMetadataEntry, { keyPlaceholder, valuePlaceholder } from "./ImportMetadataEntry"; -import { Utils } from "../../../Utils"; -import { DocumentManager } from "../DocumentManager"; +import * as path from 'path'; +import Measure, { ContentRect } from "react-measure"; +import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../fields/Doc"; import { Id } from "../../../fields/FieldSymbols"; import { List } from "../../../fields/List"; -import { Cast, BoolCast, NumCast } from "../../../fields/Types"; import { listSpec } from "../../../fields/Schema"; -import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils"; import { SchemaHeaderField } from "../../../fields/SchemaHeaderField"; -import "./DirectoryImportBox.scss"; -import { Networking } from "../../Network"; -import { BatchedArray } from "array-batcher"; -import * as path from 'path'; +import { BoolCast, Cast, NumCast } from "../../../fields/Types"; import { AcceptibleMedia, Upload } from "../../../server/SharedMediaTypes"; +import { Utils } from "../../../Utils"; +import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils"; +import { Docs, DocumentOptions, DocUtils } from "../../documents/Documents"; +import { Networking } from "../../Network"; +import { FieldView, FieldViewProps } from "../../views/nodes/FieldView"; +import { DocumentManager } from "../DocumentManager"; +import "./DirectoryImportBox.scss"; +import ImportMetadataEntry, { keyPlaceholder, valuePlaceholder } from "./ImportMetadataEntry"; +import React = require("react"); const unsupported = ["text/html", "text/plain"]; @observer -export default class DirectoryImportBox extends React.Component<FieldViewProps> { +export class DirectoryImportBox extends React.Component<FieldViewProps> { private selector = React.createRef<HTMLInputElement>(); @observable private top = 0; @observable private left = 0; diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx index 81f9b9362..aeb0f670d 100644 --- a/src/client/util/InteractionUtils.tsx +++ b/src/client/util/InteractionUtils.tsx @@ -89,42 +89,40 @@ export namespace InteractionUtils { return myTouches; } - export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number, color: string, width: string, bezier: string, scalex: number, scaley: number, shape: string, pevents: string, drawHalo: boolean) { - var pts = ""; - if (shape) { - //if any of the shape are true - const shapePts = makePolygon(shape, points); - pts = shapePts.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X * scalex - left * scalex},${pt.Y * scaley - top * scaley} `, ""); + export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number, color: string, width: number, strokeWidth: number, bezier: string, scalex: number, scaley: number, shape: string, pevents: string, drawHalo: boolean) { + let pts: { X: number; Y: number; }[] = []; + if (shape) { //if any of the shape are true + pts = makePolygon(shape, points); } else if (points.length > 1 && points[points.length - 1].X === points[0].X && points[points.length - 1].Y === points[0].Y) { //pointer is up (first and last points are the same) - const newPoints: number[][] = []; - const newPts: { X: number; Y: number; }[] = []; - //convert to [][] for fitcurve module - for (var i = 0; i < points.length - 1; i++) { - newPoints.push([points[i].X, points[i].Y]); - } + points.pop(); + const newPoints = points.reduce((p, pts) => { p.push([pts.X, pts.Y]); return p; }, [] as number[][]); + const bezierCurves = fitCurve(newPoints, parseInt(bezier)); for (var i = 0; i < bezierCurves.length; i++) { for (var t = 0; t < 1; t += 0.01) { const point = beziercurve(t, bezierCurves[i]); - newPts.push({ X: point[0], Y: point[1] }); + pts.push({ X: point[0], Y: point[1] }); } } - pts = newPts.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X * scalex - left * scalex},${pt.Y * scaley - top * scaley} `, ""); } else { - //in the middle of drawing - pts = points.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X * scalex - left * scalex},${pt.Y * scaley - top * scaley} `, ""); + pts = points; } + const strpts = pts.reduce((acc: string, pt: { X: number, Y: number }) => acc + + `${(pt.X - left - width / 2) * scalex + width / 2}, + ${(pt.Y - top - width / 2) * scaley + width / 2} `, ""); + return ( <polyline - points={pts} + points={strpts} style={{ filter: drawHalo ? "url(#dangerShine)" : undefined, fill: "none", + opacity: strokeWidth !== width ? 0.5 : undefined, pointerEvents: pevents as any, stroke: color ?? "rgb(0, 0, 0)", - strokeWidth: parseInt(width), + strokeWidth: strokeWidth, strokeLinejoin: "round", strokeLinecap: "round" }} diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index 95528e25a..47b2541bd 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -205,4 +205,5 @@ export class LinkManager { } } -Scripting.addGlobal(function links(doc: any) { return new List(LinkManager.Instance.getAllRelatedLinks(doc)); });
\ No newline at end of file +Scripting.addGlobal(function links(doc: any) { return new List(LinkManager.Instance.getAllRelatedLinks(doc)); }, + "creates a link to inputted document", "(doc: any)");
\ No newline at end of file diff --git a/src/client/util/ScriptManager.ts b/src/client/util/ScriptManager.ts new file mode 100644 index 000000000..785e63d9a --- /dev/null +++ b/src/client/util/ScriptManager.ts @@ -0,0 +1,104 @@ +import { Doc, DocListCast } from "../../fields/Doc"; +import { List } from "../../fields/List"; +import { Scripting } from "./Scripting"; +import { StrCast, Cast } from "../../fields/Types"; +import { listSpec } from "../../fields/Schema"; +import { Docs } from "../documents/Documents"; + +export class ScriptManager { + + static _initialized = false; + private static _instance: ScriptManager; + public static get Instance(): ScriptManager { + return this._instance || (this._instance = new this()); + } + private constructor() { + if (!ScriptManager._initialized) { + ScriptManager._initialized = true; + this.getAllScripts().forEach(scriptDoc => ScriptManager.addScriptToGlobals(scriptDoc)); + } + } + + public get ScriptManagerDoc(): Doc | undefined { + return Docs.Prototypes.MainScriptDocument(); + } + public getAllScripts(): Doc[] { + const sdoc = ScriptManager.Instance.ScriptManagerDoc; + if (sdoc) { + const docs = DocListCast(sdoc.data); + return docs; + } + return []; + } + + public addScript(scriptDoc: Doc): boolean { + + console.log("in add script method"); + + const scriptList = this.getAllScripts(); + scriptList.push(scriptDoc); + if (ScriptManager.Instance.ScriptManagerDoc) { + ScriptManager.Instance.ScriptManagerDoc.data = new List<Doc>(scriptList); + ScriptManager.addScriptToGlobals(scriptDoc); + console.log("script added"); + return true; + } + return false; + } + + public deleteScript(scriptDoc: Doc): boolean { + + console.log("in delete script method"); + + if (scriptDoc.name) { + Scripting.removeGlobal(StrCast(scriptDoc.name)); + } + const scriptList = this.getAllScripts(); + const index = scriptList.indexOf(scriptDoc); + if (index > -1) { + scriptList.splice(index, 1); + if (ScriptManager.Instance.ScriptManagerDoc) { + ScriptManager.Instance.ScriptManagerDoc.data = new List<Doc>(scriptList); + return true; + } + } + return false; + } + + public static addScriptToGlobals(scriptDoc: Doc): void { + + Scripting.removeGlobal(StrCast(scriptDoc.name)); + + const params = Cast(scriptDoc["data-params"], listSpec("string"), []); + console.log(params); + const paramNames = params.reduce((o: string, p: string) => { + if (params.indexOf(p) === params.length - 1) { + o = o + p.split(":")[0].trim(); + } else { + o = o + p.split(":")[0].trim() + ","; + } + return o; + }, "" as string); + + const f = new Function(paramNames, StrCast(scriptDoc.script)); + + console.log(scriptDoc.script); + + Object.defineProperty(f, 'name', { value: StrCast(scriptDoc.name), writable: false }); + + let parameters = "("; + params.forEach((element: string, i: number) => { + if (i === params.length - 1) { + parameters = parameters + element + ")"; + } else { + parameters = parameters + element + ", "; + } + }); + + if (parameters === "(") { + Scripting.addGlobal(f, StrCast(scriptDoc.description)); + } else { + Scripting.addGlobal(f, StrCast(scriptDoc.description), parameters); + } + } +}
\ No newline at end of file diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index ab577315c..e6cf50de3 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -10,6 +10,8 @@ export { ts }; // @ts-ignore import * as typescriptlib from '!!raw-loader!./type_decls.d'; import { Doc, Field } from '../../fields/Doc'; +import { Cast } from "../../fields/Types"; +import { listSpec } from "../../fields/Schema"; export interface ScriptSucccess { success: true; @@ -49,19 +51,34 @@ export function isCompileError(toBeDetermined: CompileResult): toBeDetermined is export namespace Scripting { export function addGlobal(global: { name: string }): void; export function addGlobal(name: string, global: any): void; - export function addGlobal(nameOrGlobal: any, global?: any) { - let n: string; + + export function addGlobal(global: { name: string }, decription?: string, params?: string): void; + + export function addGlobal(first: any, second?: any, third?: string) { + let n: any; let obj: any; - if (global !== undefined && typeof nameOrGlobal === "string") { - n = nameOrGlobal; - obj = global; - } else if (nameOrGlobal && typeof nameOrGlobal.name === "string") { - n = nameOrGlobal.name; - obj = nameOrGlobal; + + if (second !== undefined) { + if (typeof first === "string") { + n = first; + obj = second; + } else { + obj = first; + n = first.name; + _scriptingDescriptions[n] = second; + if (third !== undefined) { + _scriptingParams[n] = third; + } + } + } else if (first && typeof first.name === "string") { + n = first.name; + obj = first; } else { throw new Error("Must either register an object with a name, or give a name and an object"); } - if (_scriptingGlobals.hasOwnProperty(n)) { + if (n === undefined || n === "undefined") { + return false; + } else if (_scriptingGlobals.hasOwnProperty(n)) { throw new Error(`Global with name ${n} is already registered, choose another name`); } _scriptingGlobals[n] = obj; @@ -75,6 +92,20 @@ export namespace Scripting { scriptingGlobals = globals; } + export function removeGlobal(name: string) { + if (getGlobals().includes(name)) { + delete _scriptingGlobals[name]; + if (_scriptingDescriptions[name]){ + delete _scriptingDescriptions[name]; + } + if (_scriptingParams[name]){ + delete _scriptingParams[name]; + } + return true; + } + return false; + } + export function resetScriptingGlobals() { scriptingGlobals = _scriptingGlobals; } @@ -85,7 +116,19 @@ export namespace Scripting { } export function getGlobals() { - return Object.keys(scriptingGlobals); + return Object.keys(_scriptingGlobals); + } + + export function getGlobalObj() { + return _scriptingGlobals; + } + + export function getDescriptions(){ + return _scriptingDescriptions; + } + + export function getParameters(){ + return _scriptingParams; } } @@ -95,6 +138,8 @@ export function scriptingGlobal(constructor: { new(...args: any[]): any }) { const _scriptingGlobals: { [name: string]: any } = {}; let scriptingGlobals: { [name: string]: any } = _scriptingGlobals; +const _scriptingDescriptions: { [name: string]: any } = {}; +const _scriptingParams: { [name: string]: any } = {}; function Run(script: string | undefined, customParams: string[], diagnostics: any[], originalScript: string, options: ScriptOptions): CompileResult { const errors = diagnostics.filter(diag => diag.category === ts.DiagnosticCategory.Error); @@ -133,6 +178,7 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an } return { success: true, result }; } catch (error) { + if (batch) { batch.end(); } diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index 0e15197c4..9888cce48 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -10,6 +10,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Networking } from "../Network"; import { CurrentUserUtils } from "./CurrentUserUtils"; import { Utils } from "../../Utils"; +import { Doc } from "../../fields/Doc"; library.add(fa.faWindowClose); @@ -78,6 +79,10 @@ export default class SettingsManager extends React.Component<{}> { this.errorText = ""; this.successText = ""; } + @action + noviceToggle = (event: any) => { + Doc.UserDoc().noviceMode = !Doc.UserDoc().noviceMode; + } private get settingsInterface() { return ( @@ -91,7 +96,7 @@ export default class SettingsManager extends React.Component<{}> { <div className="settings-body"> <div className="settings-type"> <button onClick={this.onClick} value="password">reset password</button> - <button onClick={this.onClick} value="data">reset data</button> + <button onClick={this.noviceToggle} value="data">{`toggle ${Doc.UserDoc().noviceMode ? "developer" : "novice"} mode`}</button> <button onClick={() => window.location.assign(Utils.prepend("/logout"))}> {CurrentUserUtils.GuestWorkspace ? "Exit" : "Log Out"} </button> diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index a35a8869c..62a95116f 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -281,7 +281,6 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV const dragDocView = this.view0!; const dragData = new DragManager.DocumentDragData([dragDocView.props.Document]); const [left, top] = dragDocView.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); - dragData.embedDoc = true; dragData.dropAction = "alias"; DragManager.StartDocumentDrag([dragDocView.ContentDiv!], dragData, left, top, { offsetX: dragData.offset[0], diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index e21d431b1..ee3ce1cf3 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -5,6 +5,7 @@ import * as Autosuggest from 'react-autosuggest'; import { ObjectField } from '../../fields/ObjectField'; import { SchemaHeaderField } from '../../fields/SchemaHeaderField'; import "./EditableView.scss"; +import { DragManager } from '../util/DragManager'; export interface EditableProps { /** @@ -48,6 +49,8 @@ export interface EditableProps { HeadingObject?: SchemaHeaderField | undefined; toggle?: () => void; color?: string | undefined; + onDrop?: any; + placeholder?: string; } /** @@ -78,6 +81,13 @@ export class EditableView extends React.Component<EditableProps> { // } // } + @action + componentDidMount() { + if (this._ref.current && this.props.onDrop) { + DragManager.MakeDropTarget(this._ref.current, this.props.onDrop.bind(this)); + } + } + _didShow = false; @action @@ -169,6 +179,7 @@ export class EditableView extends React.Component<EditableProps> { onBlur={e => this.finalizeEdit(e.currentTarget.value, false, true)} onPointerDown={this.stopPropagation} onClick={this.stopPropagation} onPointerUp={this.stopPropagation} style={{ display: this.props.display, fontSize: this.props.fontSize }} + placeholder={this.props.placeholder} />; } else { this.props.autosuggestProps?.resetValue(); @@ -176,8 +187,9 @@ export class EditableView extends React.Component<EditableProps> { <div className={`editableView-container-editing${this.props.oneLine ? "-oneLine" : ""}`} ref={this._ref} style={{ display: this.props.display, minHeight: "20px", height: `${this.props.height ? this.props.height : "auto"}`, maxHeight: `${this.props.maxHeight}` }} - onClick={this.onClick}> - <span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize }}>{this.props.contents}</span> + onClick={this.onClick} placeholder={this.props.placeholder}> + + <span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize, color: this.props.contents ? "black" : "grey" }}>{this.props.contents ? this.props.contents?.valueOf() : this.props.placeholder?.valueOf()}</span> </div> ); } diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index 97df86168..e51e2d4e1 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -323,7 +323,7 @@ export default class GestureOverlay extends Touchable { this._thumbY = thumb.clientY; this._menuX = thumb.clientX + 50; this._menuY = thumb.clientY; - this._palette = <HorizontalPalette x={minX} y={minY} thumb={[thumb.clientX, thumb.clientY]} thumbDoc={thumbDoc} />; + this._palette = <HorizontalPalette key="palette" x={minX} y={minY} thumb={[thumb.clientX, thumb.clientY]} thumbDoc={thumbDoc} />; }); } @@ -803,17 +803,18 @@ export default class GestureOverlay extends Touchable { @computed get elements() { const B = this.svgBounds; + const width = Number(ActiveInkWidth()); return [ this.props.children, this._palette, - [this._strokes.map(l => { + [this._strokes.map((l, i) => { const b = this.getBounds(l); - return <svg key={b.left} width={b.width} height={b.height} style={{ transform: `translate(${b.left}px, ${b.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000, overflow: "visible" }}> - {InteractionUtils.CreatePolyline(l, b.left, b.top, ActiveInkColor(), ActiveInkWidth(), ActiveInkBezierApprox(), 1, 1, this.InkShape, "none", false)} + return <svg key={i} width={b.width} height={b.height} style={{ transform: `translate(${b.left}px, ${b.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000, overflow: "visible" }}> + {InteractionUtils.CreatePolyline(l, b.left, b.top, ActiveInkColor(), width, width, ActiveInkBezierApprox(), 1, 1, this.InkShape, "none", false)} </svg>; }), - this._points.length <= 1 ? (null) : <svg width={B.width} height={B.height} style={{ transform: `translate(${B.left}px, ${B.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000, overflow: "visible" }}> - {InteractionUtils.CreatePolyline(this._points, B.left, B.top, ActiveInkColor(), ActiveInkWidth(), ActiveInkBezierApprox(), 1, 1, this.InkShape, "none", false)} + this._points.length <= 1 ? (null) : <svg key="svg" width={B.width} height={B.height} style={{ transform: `translate(${B.left}px, ${B.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000, overflow: "visible" }}> + {InteractionUtils.CreatePolyline(this._points, B.left, B.top, ActiveInkColor(), width, width, ActiveInkBezierApprox(), 1, 1, this.InkShape, "none", false)} </svg>] ]; } @@ -914,7 +915,7 @@ Scripting.addGlobal(function resetPen() { SetActiveInkColor(GestureOverlay.Instance.SavedColor ?? "rgb(0, 0, 0)"); SetActiveInkWidth(GestureOverlay.Instance.SavedWidth ?? "2"); }); -}); +}, "resets the pen tool"); Scripting.addGlobal(function createText(text: any, x: any, y: any) { GestureOverlay.Instance.dispatchGesture("text", [{ X: x, Y: y }], text); -});
\ No newline at end of file +}, "creates a text document with inputted text and coordinates", "(text: any, x: any, y: any)");
\ No newline at end of file diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index a7650163f..7e3bd1c17 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -1,6 +1,5 @@ import { library } from "@fortawesome/fontawesome-svg-core"; import { faPaintBrush } from "@fortawesome/free-solid-svg-icons"; -import { observable, runInAction, action } from "mobx"; import { observer } from "mobx-react"; import { documentSchema } from "../../fields/documentSchemas"; import { InkData, InkField, InkTool } from "../../fields/InkField"; @@ -16,7 +15,6 @@ import { FieldView, FieldViewProps } from "./nodes/FieldView"; import React = require("react"); import { Scripting } from "../util/Scripting"; import { Doc } from "../../fields/Doc"; -import { Id } from "../../fields/FieldSymbols"; library.add(faPaintBrush); @@ -43,25 +41,22 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume render() { TraceMobx(); const data: InkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? []; + const strokeWidth = Number(StrCast(this.layoutDoc.strokeWidth, ActiveInkWidth())); const xs = data.map(p => p.X); const ys = data.map(p => p.Y); - const left = Math.min(...xs); - const top = Math.min(...ys); - const right = Math.max(...xs); - const bottom = Math.max(...ys); + const left = Math.min(...xs) - strokeWidth / 2; + const top = Math.min(...ys) - strokeWidth / 2; + const right = Math.max(...xs) + strokeWidth / 2; + const bottom = Math.max(...ys) + strokeWidth / 2; const width = right - left; const height = bottom - top; - const scaleX = this.props.PanelWidth() / width; - const scaleY = this.props.PanelHeight() / height; - const strokeWidth = Number(StrCast(this.layoutDoc.strokeWidth, ActiveInkWidth())); + const scaleX = (this.props.PanelWidth() - strokeWidth) / (width - strokeWidth); + const scaleY = (this.props.PanelHeight() - strokeWidth) / (height - strokeWidth); const strokeColor = StrCast(this.layoutDoc.color, ActiveInkColor()); - const points = InteractionUtils.CreatePolyline(data, left, top, - strokeColor, - strokeWidth.toString(), + const points = InteractionUtils.CreatePolyline(data, left, top, strokeColor, strokeWidth, strokeWidth, StrCast(this.layoutDoc.strokeBezier, ActiveInkBezierApprox()), scaleX, scaleY, "", "none", this.props.isSelected() && strokeWidth <= 5); const hpoints = InteractionUtils.CreatePolyline(data, left, top, - this.props.isSelected() && strokeWidth > 5 ? strokeColor : "transparent", - (strokeWidth + 15).toString(), + this.props.isSelected() && strokeWidth > 5 ? strokeColor : "transparent", strokeWidth, (strokeWidth + 15), StrCast(this.layoutDoc.strokeBezier, ActiveInkBezierApprox()), scaleX, scaleY, "", this.props.active() ? "visiblestroke" : "none", false); return ( <svg className="inkingStroke" diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss index dca2a1e3e..a7048eb23 100644 --- a/src/client/views/MainView.scss +++ b/src/client/views/MainView.scss @@ -32,9 +32,6 @@ } .mainView-container, .mainView-container-dark { - input { - color: unset !important; - } width: 100%; height: 100%; position: absolute; @@ -50,6 +47,10 @@ .mainView-container { color:dimgray; + .lm_title { + background: #cacaca; + color:black; + } } .mainView-container-dark { @@ -57,6 +58,10 @@ .lm_goldenlayout { background: dimgray; } + .lm_title { + background: black; + color:unset; + } .marquee { border-color: white; } diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx index cfa869fb2..f6e5e1705 100644 --- a/src/client/views/OverlayView.tsx +++ b/src/client/views/OverlayView.tsx @@ -12,7 +12,6 @@ import './OverlayView.scss'; import { Scripting } from "../util/Scripting"; import { ScriptingRepl } from './ScriptingRepl'; import { DragManager } from "../util/DragManager"; -import { listSpec } from "../../fields/Schema"; import { List } from "../../fields/List"; export type OverlayDisposer = () => void; diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss index 2fafcecb2..18d642510 100644 --- a/src/client/views/collections/CollectionDockingView.scss +++ b/src/client/views/collections/CollectionDockingView.scss @@ -2,13 +2,17 @@ .lm_title { margin-top: 3px; - background: black; border-radius: 5px; border: solid 1px dimgray; border-width: 2px 2px 0px; height: 20px; transform: translate(0px, -3px); + cursor: grab; } +.lm_title.focus-visible { + cursor: text; +} + .lm_title_wrap { overflow: hidden; height: 19px; diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 02ba45f9c..c2e297f61 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -44,7 +44,6 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp props: { documentId: document[Id], libraryPath: libraryPath?.map(d => d[Id]) - //collectionDockingView: CollectionDockingView.Instance } }; } @@ -465,7 +464,8 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp if (docids) { const docs = (await Promise.all(docids.map(id => DocServer.GetRefField(id)))).filter(f => f).map(f => f as Doc); - Doc.GetProto(this.props.Document)[this.props.fieldKey] = new List<Doc>(docs); + docs.map(doc => Doc.AddDocToList(Doc.GetProto(this.props.Document), this.props.fieldKey, doc)); + // Doc.GetProto(this.props.Document)[this.props.fieldKey] = new List<Doc>(docs); } } @@ -859,5 +859,6 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { </div >); } } -Scripting.addGlobal(function openOnRight(doc: any) { CollectionDockingView.AddRightSplit(doc); }); +Scripting.addGlobal(function openOnRight(doc: any) { CollectionDockingView.AddRightSplit(doc); }, + "opens up the inputted document on the right side of the screen", "(doc: any)"); Scripting.addGlobal(function useRightSplit(doc: any, shiftKey?: boolean) { CollectionDockingView.UseRightSplit(doc, undefined, shiftKey); }); diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index bd0b69da8..797aabf18 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -476,7 +476,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) transformOrigin: "top left", }} onScroll={action(e => { - if (!this.props.isSelected() && window.innerWidth > 1000) e.currentTarget.scrollTop = this._scroll; + if (!this.props.isSelected() && this.props.renderDepth && window.innerWidth > 1000) e.currentTarget.scrollTop = this._scroll; else this._scroll = e.currentTarget.scrollTop; })} onDrop={this.onExternalDrop.bind(this)} @@ -498,4 +498,4 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) </div> </div> ); } -}
\ No newline at end of file +} diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 180bcdd02..d631a492e 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -83,8 +83,9 @@ class TreeView extends React.Component<TreeViewProps> { private _tref = React.createRef<HTMLDivElement>(); private _docRef = React.createRef<DocumentView>(); + get noviceMode() { return BoolCast(Doc.UserDoc().noviceMode, false); } get displayName() { return "TreeView(" + this.props.document.title + ")"; } // this makes mobx trace() statements more descriptive - get defaultExpandedView() { return this.childDocs ? this.fieldKey : StrCast(this.props.document.defaultExpandedView, "fields"); } + get defaultExpandedView() { return this.childDocs ? this.fieldKey : StrCast(this.props.document.defaultExpandedView, this.noviceMode ? "layout" : "fields"); } @observable _overrideTreeViewOpen = false; // override of the treeViewOpen field allowing the display state to be independent of the document's state set treeViewOpen(c: boolean) { if (this.props.treeViewPreventOpen) this._overrideTreeViewOpen = c; else this.props.document.treeViewOpen = this._overrideTreeViewOpen = c; } @computed get treeViewOpen() { return (!this.props.treeViewPreventOpen && !this.props.document.treeViewPreventOpen && BoolCast(this.props.document.treeViewOpen)) || this._overrideTreeViewOpen; } @@ -420,10 +421,10 @@ class TreeView extends React.Component<TreeViewProps> { <span className="collectionTreeView-keyHeader" key={this.treeViewExpandedView} onPointerDown={action(() => { if (this.treeViewOpen) { - this.props.document.treeViewExpandedView = this.treeViewExpandedView === this.fieldKey ? "fields" : + this.props.document.treeViewExpandedView = this.treeViewExpandedView === this.fieldKey ? (Doc.UserDoc().noviceMode ? "layout" : "fields") : this.treeViewExpandedView === "fields" && Doc.Layout(this.props.document) ? "layout" : this.treeViewExpandedView === "layout" && this.props.document.links ? "links" : - this.childDocs ? this.fieldKey : "fields"; + this.childDocs ? this.fieldKey : (Doc.UserDoc().noviceMode ? "layout" : "fields"); } this.treeViewOpen = true; })}> diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 9f5308ce8..a700d0dfb 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -159,7 +159,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus // this is called with the document that was dragged and the collection to move it into. // if the target collection is the same as this collection, then the move will be allowed. // otherwise, the document being moved must be able to be removed from its container before - // moving it into the target. + // moving it into the target. @action.bound moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => { if (Doc.AreProtosEqual(this.props.Document, targetCollection)) { @@ -171,7 +171,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus showIsTagged = () => { return (null); - // this section would display an icon in the bototm right of a collection to indicate that all + // this section would display an icon in the bototm right of a collection to indicate that all // photos had been processed through Google's content analysis API and Google's tags had been // assigned to the documents googlePhotosTags field. // const children = DocListCast(this.props.Document[this.props.fieldKey]); @@ -282,10 +282,12 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus })); !existingOnClick && ContextMenu.Instance?.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" }); - const more = ContextMenu.Instance?.findByDescription("More..."); - const moreItems = more && "subitems" in more ? more.subitems : []; - moreItems.push({ description: "Export Image Hierarchy", icon: "columns", event: () => ImageUtils.ExportHierarchyToFileSystem(this.props.Document) }); - !more && ContextMenu.Instance?.addItem({ description: "More...", subitems: moreItems, icon: "hand-point-right" }); + if (!Doc.UserDoc().noviceMode) { + const more = ContextMenu.Instance.findByDescription("More..."); + const moreItems = more && "subitems" in more ? more.subitems : []; + moreItems.push({ description: "Export Image Hierarchy", icon: "columns", event: () => ImageUtils.ExportHierarchyToFileSystem(this.props.Document) }); + !more && ContextMenu.Instance.addItem({ description: "More...", subitems: moreItems, icon: "hand-point-right" }); + } } } @@ -525,5 +527,3 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus </div>); } } - - diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index cddac01ad..82d6dee73 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -457,7 +457,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P case GestureUtils.Gestures.Stroke: const points = ge.points; const B = this.getTransform().transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height); - const inkDoc = Docs.Create.InkDocument(ActiveInkColor(), Doc.GetSelectedTool(), ActiveInkWidth(), ActiveInkBezierApprox(), points, { title: "ink stroke", x: B.x, y: B.y, _width: B.width, _height: B.height }); + const inkDoc = Docs.Create.InkDocument(ActiveInkColor(), Doc.GetSelectedTool(), ActiveInkWidth(), ActiveInkBezierApprox(), points, + { title: "ink stroke", x: B.x - Number(ActiveInkWidth()) / 2, y: B.y - Number(ActiveInkWidth()) / 2, _width: B.width + Number(ActiveInkWidth()), _height: B.height + Number(ActiveInkWidth()) }); this.addDocument(inkDoc); e.stopPropagation(); break; diff --git a/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx index e7ef4cbd2..647847b68 100644 --- a/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx +++ b/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx @@ -99,16 +99,14 @@ export default class InkOptionsMenu extends AntimodeMenu { } @computed get shapeButtons() { - return <> - {this._buttons.map((btn, i) => <button - className="antimodeMenu-button" - title={`Draw ${btn}`} - key={btn} - onPointerDown={action(e => GestureOverlay.Instance.InkShape = btn)} - style={{ backgroundColor: btn === GestureOverlay.Instance?.InkShape ? "121212" : "" }}> - {this._icons[i]} - </button>)} - </>; + return this._buttons.map((btn, i) => <button + className="antimodeMenu-button" + title={`Draw ${btn}`} + key={i} + onPointerDown={action(e => GestureOverlay.Instance.InkShape = btn)} + style={{ backgroundColor: btn === GestureOverlay.Instance.InkShape ? "121212" : "" }}> + {this._icons[i]} + </button>); } @computed get bezierButton() { @@ -125,7 +123,7 @@ export default class InkOptionsMenu extends AntimodeMenu { render() { const buttons = [ <button className="antimodeMenu-button" title="Drag" key="drag" onPointerDown={e => this.dragStart(e)}> ✜ </button>, - this.shapeButtons, + ...this.shapeButtons, this.bezierButton, this.widthPicker, this.colorPicker, @@ -149,4 +147,4 @@ Scripting.addGlobal(function activatePen(penBtn: any) { Doc.SetSelectedTool(InkTool.None); InkOptionsMenu.Instance.fadeOut(true); } -});
\ No newline at end of file +}); diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx index f49b1f763..2e6828132 100644 --- a/src/client/views/nodes/ColorBox.tsx +++ b/src/client/views/nodes/ColorBox.tsx @@ -60,12 +60,12 @@ export class ColorBox extends ViewBoxBaseComponent<FieldViewProps, ColorDocument StrCast(selDoc?._backgroundColor, StrCast(selDoc?.backgroundColor, "black")))} /> <div style={{ display: "grid", gridTemplateColumns: "20% 80%", paddingTop: "10px" }}> <div> {ActiveInkWidth() ?? 2}</div> - <input type="range" value={ActiveInkWidth() ?? 2} min={1} max={100} onChange={(e: React.ChangeEvent<HTMLInputElement>) => SetActiveInkWidth(e.target.value)} /> + <input type="range" defaultValue={ActiveInkWidth() ?? 2} min={1} max={100} onChange={(e: React.ChangeEvent<HTMLInputElement>) => SetActiveInkWidth(e.target.value)} /> <div> {ActiveInkBezierApprox() ?? 2}</div> - <input type="range" value={ActiveInkBezierApprox() ?? 2} min={0} max={300} onChange={(e: React.ChangeEvent<HTMLInputElement>) => SetActiveBezierApprox(e.target.value)} /> + <input type="range" defaultValue={ActiveInkBezierApprox() ?? 2} min={0} max={300} onChange={(e: React.ChangeEvent<HTMLInputElement>) => SetActiveBezierApprox(e.target.value)} /> <br /> <br /> </div> </div>; } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index ccae9658f..75627607b 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -43,13 +43,13 @@ import "./DocumentView.scss"; import { LinkAnchorBox } from './LinkAnchorBox'; import { RadialMenu } from './RadialMenu'; import React = require("react"); -import { undo } from 'prosemirror-history'; library.add(fa.faEdit, fa.faTrash, fa.faShare, fa.faDownload, fa.faExpandArrowsAlt, fa.faCompressArrowsAlt, fa.faLayerGroup, fa.faExternalLinkAlt, fa.faAlignCenter, fa.faCaretSquareRight, fa.faSquare, fa.faConciergeBell, fa.faWindowRestore, fa.faFolder, fa.faMapPin, fa.faLink, fa.faFingerprint, fa.faCrosshairs, fa.faDesktop, fa.faUnlock, fa.faLock, fa.faLaptopCode, fa.faMale, - fa.faCopy, fa.faHandPointRight, fa.faCompass, fa.faSnowflake, fa.faMicrophone); + fa.faCopy, fa.faHandPointRight, fa.faCompass, fa.faSnowflake, fa.faMicrophone, fa.faKeyboard, fa.faQuestion); export type DocFocusFunc = () => boolean; + export interface DocumentViewProps { ContainingCollectionView: Opt<CollectionView>; ContainingCollectionDoc: Opt<Doc>; @@ -138,8 +138,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); console.log(SelectionManager.SelectedDocuments()); - console.log("START"); - if (RadialMenu.Instance?._display === false) { + if (RadialMenu.Instance._display === false) { this.addHoldMoveListeners(); this.addHoldEndListeners(); this.onRadialMenu(e, me); @@ -179,8 +178,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @action onRadialMenu = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): void => { - // console.log(InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true)); - // const pt = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true)[0]; const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1]; RadialMenu.Instance.openMenu(pt.pageX - 15, pt.pageY - 15); @@ -189,12 +186,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu RadialMenu.Instance.addItem({ description: "Open in a new tab", event: () => this.props.addDocTab(this.props.Document, "onRight"), icon: "trash", selected: -1 }); RadialMenu.Instance.addItem({ description: "Pin to Presentation", event: () => this.props.pinToPres(this.props.Document), icon: "folder", selected: -1 }); - // if (SelectionManager.IsSelected(this, true)) { - // SelectionManager.SelectDoc(this, false); - // } SelectionManager.DeselectAll(); - - } @action @@ -361,12 +353,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu // depending on the followLinkLocation property of the source (or the link itself as a fallback); followLinkClick = async (altKey: boolean, ctrlKey: boolean, shiftKey: boolean) => { const batch = UndoManager.StartBatch("follow link click"); - // open up target if it's not already in view ... + // open up target if it's not already in view ... const createViewFunc = (doc: Doc, followLoc: string, finished: Opt<() => void>) => { const targetFocusAfterDocFocus = () => { const where = StrCast(this.Document.followLinkLocation) || followLoc; const hackToCallFinishAfterFocus = () => { - finished && setTimeout(finished, 0); // finished() needs to be called right after hackToCallFinishAfterFocus(), but there's no callback for that so we use the hacky timeout. + finished && setTimeout(finished, 0); // finished() needs to be called right after hackToCallFinishAfterFocus(), but there's no callback for that so we use the hacky timeout. return false; // we must return false here so that the zoom to the document is not reversed. If it weren't for needing to call finished(), we wouldn't need this function at all since not having it is equivalent to returning false }; this.props.addDocTab(doc, where) && this.props.focus(doc, BoolCast(this.Document.followLinkZoom, true), undefined, hackToCallFinishAfterFocus); // add the target and focus on it. @@ -511,8 +503,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } onPointerDown = (e: React.PointerEvent): void => { - // console.log(e.button) - // console.log(e.nativeEvent) // continue if the event hasn't been canceled AND we are using a moues or this is has an onClick or onDragStart function (meaning it is a button document) if (!(InteractionUtils.IsType(e, InteractionUtils.MOUSETYPE) || Doc.GetSelectedTool() === InkTool.Highlighter || Doc.GetSelectedTool() === InkTool.Pen)) { if (!InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) { @@ -739,7 +729,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu this.props.contextMenuItems?.().forEach(item => cm.addItem({ description: item.label, event: () => item.script.script.run({ this: this.layoutDoc, self: this.rootDoc }), icon: "sticky-note" })); - let options = cm.findByDescription("Options..."); const optionItems: ContextMenuProps[] = options && "subitems" in options ? options.subitems : []; const templateDoc = Cast(this.props.Document[StrCast(this.props.Document.layoutKey)], Doc, null); @@ -761,7 +750,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu onClicks.push({ description: "Edit onClick Script", event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick"), icon: "edit" }); !existingOnClick && cm.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" }); - const funcs: ContextMenuProps[] = []; if (this.Document.onDragStart) { funcs.push({ description: "Drag an Alias", icon: "edit", event: () => this.Document.dragFactory && (this.Document.onDragStart = ScriptField.MakeFunction('getAlias(this.dragFactory)')) }); @@ -771,98 +759,109 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } const more = cm.findByDescription("More..."); - const moreItems: ContextMenuProps[] = more && "subitems" in more ? more.subitems : []; - moreItems.push({ description: "Make Add Only", event: () => this.setAcl("addOnly"), icon: "concierge-bell" }); - moreItems.push({ description: "Make Read Only", event: () => this.setAcl("readOnly"), icon: "concierge-bell" }); - moreItems.push({ description: "Make Private", event: () => this.setAcl("ownerOnly"), icon: "concierge-bell" }); - moreItems.push({ description: "Test Private", event: () => this.testAcl("ownerOnly"), icon: "concierge-bell" }); - moreItems.push({ description: "Test Readonly", event: () => this.testAcl("readOnly"), icon: "concierge-bell" }); - moreItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc), icon: "concierge-bell" }); - moreItems.push({ description: `${this.Document._chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.Document._chromeStatus = (this.Document._chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" }); - moreItems.push({ description: this.Document.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.Document.lockedPosition) ? "unlock" : "lock" }); - - if (!ClientUtils.RELEASE) { - // let copies: ContextMenuProps[] = []; - moreItems.push({ description: "Copy ID", event: () => Utils.CopyText(Utils.prepend("/doc/" + this.props.Document[Id])), icon: "fingerprint" }); - // cm.addItem({ description: "Copy...", subitems: copies, icon: "copy" }); - } - if (Cast(Doc.GetProto(this.props.Document).data, listSpec(Doc))) { - moreItems.push({ description: "Export to Google Photos Album", event: () => GooglePhotos.Export.CollectionToAlbum({ collection: this.props.Document }).then(console.log), icon: "caret-square-right" }); - moreItems.push({ description: "Tag Child Images via Google Photos", event: () => GooglePhotos.Query.TagChildImages(this.props.Document), icon: "caret-square-right" }); - moreItems.push({ description: "Write Back Link to Album", event: () => GooglePhotos.Transactions.AddTextEnrichment(this.props.Document), icon: "caret-square-right" }); - } - moreItems.push({ - description: "Download document", icon: "download", event: async () => { - const response = await rp.get(Utils.CorsProxy("http://localhost:8983/solr/dash/select"), { - qs: { q: 'world', fq: 'NOT baseProto_b:true AND NOT deleted:true', start: '0', rows: '100', hl: true, 'hl.fl': '*' } - }); - console.log(response ? JSON.parse(response) : undefined); + const moreItems = more && "subitems" in more ? more.subitems : []; + if (!Doc.UserDoc().noviceMode) { + moreItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc), icon: "concierge-bell" }); + moreItems.push({ description: `${this.Document._chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.Document._chromeStatus = (this.Document._chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" }); + + if (Cast(Doc.GetProto(this.props.Document).data, listSpec(Doc))) { + moreItems.push({ description: "Export to Google Photos Album", event: () => GooglePhotos.Export.CollectionToAlbum({ collection: this.props.Document }).then(console.log), icon: "caret-square-right" }); + moreItems.push({ description: "Tag Child Images via Google Photos", event: () => GooglePhotos.Query.TagChildImages(this.props.Document), icon: "caret-square-right" }); + moreItems.push({ description: "Write Back Link to Album", event: () => GooglePhotos.Transactions.AddTextEnrichment(this.props.Document), icon: "caret-square-right" }); } - // const a = document.createElement("a"); - // const url = Utils.prepend(`/downloadId/${this.props.Document[Id]}`); - // a.href = url; - // a.download = `DocExport-${this.props.Document[Id]}.zip`; - // a.click(); - }); - - const recommender_subitems: ContextMenuProps[] = []; - - recommender_subitems.push({ - description: "Internal recommendations", - event: () => this.recommender(), - icon: "brain" - }); - - const ext_recommender_subitems: ContextMenuProps[] = []; - - ext_recommender_subitems.push({ - description: "arXiv", - event: () => this.externalRecommendation("arxiv"), - icon: "brain" - }); - ext_recommender_subitems.push({ - description: "Bing", - event: () => this.externalRecommendation("bing"), - icon: "brain" - }); - - recommender_subitems.push({ - description: "External recommendations", - subitems: ext_recommender_subitems, - icon: "brain" - }); - + moreItems.push({ + description: "Download document", icon: "download", event: async () => { + const response = await rp.get(Utils.CorsProxy("http://localhost:8983/solr/dash/select"), { + qs: { q: 'world', fq: 'NOT baseProto_b:true AND NOT deleted:true', start: '0', rows: '100', hl: true, 'hl.fl': '*' } + }); + console.log(response ? JSON.parse(response) : undefined); + } + // const a = document.createElement("a"); + // const url = Utils.prepend(`/downloadId/${this.props.Document[Id]}`); + // a.href = url; + // a.download = `DocExport-${this.props.Document[Id]}.zip`; + // a.click(); + }); + } + moreItems.push({ description: this.Document.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.Document.lockedPosition) ? "unlock" : "lock" }); + moreItems.push({ description: "Copy ID", event: () => Utils.CopyText(Utils.prepend("/doc/" + this.props.Document[Id])), icon: "fingerprint" }); moreItems.push({ description: "Delete", event: this.deleteClicked, icon: "trash" }); - moreItems.push({ description: "Recommender System", subitems: recommender_subitems, icon: "brain" }); - moreItems.push({ description: "Publish", event: () => DocUtils.Publish(this.props.Document, this.Document.title || "", this.props.addDocument, this.props.removeDocument), icon: "file" }); - moreItems.push({ description: "Undo Debug Test", event: () => UndoManager.TraceOpenBatches(), icon: "exclamation" }); + moreItems.push({ description: "Share", event: () => SharingManager.Instance.open(this), icon: "external-link-alt" }); !more && cm.addItem({ description: "More...", subitems: moreItems, icon: "hand-point-right" }); - cm.moveAfter(cm.findByDescription("More...")!, cm.findByDescription("OnClick...")!); - runInAction(() => { - const setWriteMode = (mode: DocServer.WriteMode) => { - DocServer.AclsMode = mode; - const mode1 = mode; - const mode2 = mode === DocServer.WriteMode.Default ? mode : DocServer.WriteMode.Playground; - DocServer.setFieldWriteMode("x", mode1); - DocServer.setFieldWriteMode("y", mode1); - DocServer.setFieldWriteMode("_width", mode1); - DocServer.setFieldWriteMode("_height", mode1); - - DocServer.setFieldWriteMode("_panX", mode2); - DocServer.setFieldWriteMode("_panY", mode2); - DocServer.setFieldWriteMode("scale", mode2); - DocServer.setFieldWriteMode("_viewType", mode2); - }; - const aclsMenu: ContextMenuProps[] = []; - aclsMenu.push({ description: "Share", event: () => SharingManager.Instance.open(this), icon: "external-link-alt" }); - aclsMenu.push({ description: "Default (write/read all)", event: () => setWriteMode(DocServer.WriteMode.Default), icon: DocServer.AclsMode === DocServer.WriteMode.Default ? "check" : "exclamation" }); - aclsMenu.push({ description: "Playground (write own/no read)", event: () => setWriteMode(DocServer.WriteMode.Playground), icon: DocServer.AclsMode === DocServer.WriteMode.Playground ? "check" : "exclamation" }); - aclsMenu.push({ description: "Live Playground (write own/read others)", event: () => setWriteMode(DocServer.WriteMode.LivePlayground), icon: DocServer.AclsMode === DocServer.WriteMode.LivePlayground ? "check" : "exclamation" }); - aclsMenu.push({ description: "Live Readonly (no write/read others)", event: () => setWriteMode(DocServer.WriteMode.LiveReadonly), icon: DocServer.AclsMode === DocServer.WriteMode.LiveReadonly ? "check" : "exclamation" }); - cm.addItem({ description: "Collaboration ...", subitems: aclsMenu, icon: "share" }); + const help = cm.findByDescription("Help..."); + const helpItems: ContextMenuProps[] = help && "subitems" in help ? help.subitems : []; + helpItems.push({ + description: "Keyboard Shortcuts Ctrl+/", + event: () => this.props.addDocTab(Docs.Create.PdfDocument("http://localhost:1050/assets/cheat-sheet.pdf", { _width: 300, _height: 300 }), "onRight"), + icon: "keyboard" }); + cm.addItem({ description: "Help...", subitems: helpItems, icon: "question" }); + + const existingAcls = cm.findByDescription("Privacy..."); + const aclItems: ContextMenuProps[] = existingAcls && "subitems" in existingAcls ? existingAcls.subitems : []; + aclItems.push({ description: "Make Add Only", event: () => this.setAcl("addOnly"), icon: "concierge-bell" }); + aclItems.push({ description: "Make Read Only", event: () => this.setAcl("readOnly"), icon: "concierge-bell" }); + aclItems.push({ description: "Make Private", event: () => this.setAcl("ownerOnly"), icon: "concierge-bell" }); + aclItems.push({ description: "Test Private", event: () => this.testAcl("ownerOnly"), icon: "concierge-bell" }); + aclItems.push({ description: "Test Readonly", event: () => this.testAcl("readOnly"), icon: "concierge-bell" }); + !existingAcls && cm.addItem({ description: "Privacy...", subitems: aclItems, icon: "question" }); + + // const recommender_subitems: ContextMenuProps[] = []; + + // recommender_subitems.push({ + // description: "Internal recommendations", + // event: () => this.recommender(), + // icon: "brain" + // }); + + // const ext_recommender_subitems: ContextMenuProps[] = []; + + // ext_recommender_subitems.push({ + // description: "arXiv", + // event: () => this.externalRecommendation("arxiv"), + // icon: "brain" + // }); + // ext_recommender_subitems.push({ + // description: "Bing", + // event: () => this.externalRecommendation("bing"), + // icon: "brain" + // }); + + // recommender_subitems.push({ + // description: "External recommendations", + // subitems: ext_recommender_subitems, + // icon: "brain" + // }); + + + //moreItems.push({ description: "Recommender System", subitems: recommender_subitems, icon: "brain" }); + //moreItems.push({ description: "Publish", event: () => DocUtils.Publish(this.props.Document, this.Document.title || "", this.props.addDocument, this.props.removeDocument), icon: "file" }); + //moreItems.push({ description: "Undo Debug Test", event: () => UndoManager.TraceOpenBatches(), icon: "exclamation" }); + + // runInAction(() => { + // const setWriteMode = (mode: DocServer.WriteMode) => { + // DocServer.AclsMode = mode; + // const mode1 = mode; + // const mode2 = mode === DocServer.WriteMode.Default ? mode : DocServer.WriteMode.Playground; + // DocServer.setFieldWriteMode("x", mode1); + // DocServer.setFieldWriteMode("y", mode1); + // DocServer.setFieldWriteMode("_width", mode1); + // DocServer.setFieldWriteMode("_height", mode1); + + // DocServer.setFieldWriteMode("_panX", mode2); + // DocServer.setFieldWriteMode("_panY", mode2); + // DocServer.setFieldWriteMode("scale", mode2); + // DocServer.setFieldWriteMode("_viewType", mode2); + // }; + // const aclsMenu: ContextMenuProps[] = []; + // aclsMenu.push({ description: "Default (write/read all)", event: () => setWriteMode(DocServer.WriteMode.Default), icon: DocServer.AclsMode === DocServer.WriteMode.Default ? "check" : "exclamation" }); + // aclsMenu.push({ description: "Playground (write own/no read)", event: () => setWriteMode(DocServer.WriteMode.Playground), icon: DocServer.AclsMode === DocServer.WriteMode.Playground ? "check" : "exclamation" }); + // aclsMenu.push({ description: "Live Playground (write own/read others)", event: () => setWriteMode(DocServer.WriteMode.LivePlayground), icon: DocServer.AclsMode === DocServer.WriteMode.LivePlayground ? "check" : "exclamation" }); + // aclsMenu.push({ description: "Live Readonly (no write/read others)", event: () => setWriteMode(DocServer.WriteMode.LiveReadonly), icon: DocServer.AclsMode === DocServer.WriteMode.LiveReadonly ? "check" : "exclamation" }); + // cm.addItem({ description: "Collaboration ...", subitems: aclsMenu, icon: "share" }); + // }); runInAction(() => { if (!this.topMost && !(e instanceof Touch)) { // DocumentViews should stop propagation of this event @@ -974,7 +973,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } // does Document set a layout prop - // does Document set a layout prop + // does Document set a layout prop setsLayoutProp = (prop: string) => this.props.Document[prop] !== this.props.Document["default" + prop[0].toUpperCase() + prop.slice(1)] && this.props.Document["default" + prop[0].toUpperCase() + prop.slice(1)]; // get the a layout prop by first choosing the prop from Document, then falling back to the layout doc otherwise. getLayoutPropStr = (prop: string) => StrCast(this.setsLayoutProp(prop) ? this.props.Document[prop] : this.layoutDoc[prop]); @@ -1214,4 +1213,4 @@ Scripting.addGlobal(function toggleDetail(doc: any, layoutKey: string, otherKey: const dv = DocumentManager.Instance.getDocumentView(doc); if (dv?.props.Document.layoutKey === layoutKey) dv?.switchViews(otherKey !== "layout", otherKey.replace("layout_", "")); else dv?.switchViews(true, layoutKey.replace("layout_", "")); -});
\ No newline at end of file +}); diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 73410e02f..f5e996ca4 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -120,7 +120,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD }); const files = await res.json(); const url = Utils.prepend(files[0].path); - // upload to server with known URL + // upload to server with known URL const audioDoc = Docs.Create.AudioDocument(url, { title: "audio test", _width: 200, _height: 32 }); audioDoc.treeViewExpandedView = "layout"; const audioAnnos = Cast(this.dataDoc[this.fieldKey + "-audioAnnotations"], listSpec(Doc)); @@ -182,11 +182,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD !existingAnalyze && ContextMenu.Instance?.addItem({ description: "Analyzers...", subitems: modes, icon: "hand-point-right" }); ContextMenu.Instance?.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); - - - const existingMore = ContextMenu.Instance?.findByDescription("More..."); - const mores: ContextMenuProps[] = existingMore && "subitems" in existingMore ? existingMore.subitems : []; - !existingMore && ContextMenu.Instance?.addItem({ description: "More...", subitems: mores, icon: "hand-point-right" }); } } @@ -483,4 +478,4 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD </CollectionFreeFormView> </div >); } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/ScriptingBox.scss b/src/client/views/nodes/ScriptingBox.scss index 43695f00d..a937364a8 100644 --- a/src/client/views/nodes/ScriptingBox.scss +++ b/src/client/views/nodes/ScriptingBox.scss @@ -5,31 +5,220 @@ flex-direction: column; background-color: rgb(241, 239, 235); padding: 10px; + + .boxed { + border: 1px solid black; + background-color: rgb(212, 198, 179); + width: auto; + height: auto; + font-size: 12px; + position: absolute; + z-index: 100; + padding: 5px; + white-space: nowrap; + overflow: hidden; + } + .scriptingBox-inputDiv { display: flex; flex-direction: column; - height: calc(100% - 30px); + height: 100%; + max-height: 100%; + overflow: hidden; + table-layout: fixed; + + white-space: nowrap; + + .scriptingBox-wrapper { + width: 100%; + height: 100%; + max-height: calc(100%-30px); + display: flex; + flex-direction: row; + overflow: auto; + justify-content: center; + + .descriptor { + overflow: hidden; + } + + .scriptingBox-textArea, .scriptingBox-textArea-inputs { + flex: 70; + height: 100%; + max-width: 95%; + min-width: none; + box-sizing: border-box; + resize: none; + padding: 7px; + overflow-y: auto; + overflow-x: hidden; + + body { + font-family: Arial, Helvetica, sans-serif; + border: 1px solid red; + } + + .rta { + position: relative; + width: 100%; + height: 100%; + margin-bottom: 60px !important; + overflow-y: auto; + overflow-x: hidden; + overflow: hidden; + } + + .rta__textarea { + width: 100%; + height: 100%; + font-size: 10px; + } + + .rta__autocomplete { + position: absolute; + display: block; + margin-top: 1em; + } + + .rta__autocomplete--top { + margin-top: 0; + margin-bottom: 1em; + max-height: 100px; + } + + .rta__list { + margin: 0; + padding: 0; + background: #fff; + border: 1px solid #dfe2e5; + border-radius: 3px; + box-shadow: 0 0 5px rgba(27, 31, 35, 0.1); + list-style: none; + overflow-y: auto; + overflow-x: hidden; + } + + .rta__entity { + background: white; + width: 100%; + text-align: left; + outline: none; + overflow-y: auto; + } + + .rta__entity:hover { + cursor: pointer; + } + + .rta__entity>* { + padding-left: 4px; + padding-right: 4px; + } + + .rta__entity--selected { + color: #fff; + text-decoration: none; + background: #0366d6; + } + } + + .scriptingBox-textArea-inputs { + max-width: 100%; + height: 40%; + width: 100%; + resize: none; + } + .scriptingBox-textArea-script { + resize: none; + height: 100%; + } + + .scriptingBox-plist { + flex: 30; + width: 30%; + height: 100%; + box-sizing: border-box; + resize: none; + padding: 2px; + overflow-y: auto; + + .scriptingBox-pborder { + background-color: rgb(241, 239, 235); + } + + .scriptingBox-viewBase { + display: flex; + + .scriptingBox-viewPicker { + font-size: 75%; + //text-transform: uppercase; + letter-spacing: 2px; + background: rgb(238, 238, 238); + color: grey; + outline-color: black; + border: none; + padding: 12px 10px 11px 10px; + } + + .scriptingBox-viewPicker:active { + outline-color: black; + } + + .commandEntry-outerDiv { + pointer-events: all; + background-color: gray; + display: flex; + flex-direction: row; + } + } + } + + .scriptingBox-paramNames { + flex: 60; + width: 60%; + box-sizing: border-box; + resize: none; + padding: 7px; + overflow-y: clip; + } + + .scriptingBox-paramInputs { + flex: 40; + width: 40%; + box-sizing: border-box; + resize: none; + padding: 2px; + overflow-y: hidden; + } + } + .scriptingBox-errorMessage { overflow: auto; + background: "red"; + background-color: "red"; + height: 45px; } + .scripting-params { - background: "beige"; - } - .scriptingBox-textArea { - width: 100%; - height: 100%; - box-sizing: border-box; - resize: none; - padding: 7px; + background: rgb(241, 239, 235); + outline-style: solid; + outline-color: black; } } .scriptingBox-toolbar { width: 100%; height: 30px; + overflow: hidden; + .scriptingBox-button { - width: 50% + font-size: xx-small; + width: 50%; + resize: auto; } - } -} + .scriptingBox-button-third { + width: 33%; + } + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx index 0944edf60..8912b113c 100644 --- a/src/client/views/nodes/ScriptingBox.tsx +++ b/src/client/views/nodes/ScriptingBox.tsx @@ -1,19 +1,28 @@ -import { action, observable, computed } from "mobx"; +import ReactTextareaAutocomplete from "@webscopeio/react-textarea-autocomplete"; +import "@webscopeio/react-textarea-autocomplete/style.css"; +import { action, computed, observable, runInAction, trace } from "mobx"; import { observer } from "mobx-react"; import * as React from "react"; +import { Doc } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; -import { createSchema, makeInterface, listSpec } from "../../../fields/Schema"; +import { List } from "../../../fields/List"; +import { createSchema, listSpec, makeInterface } from "../../../fields/Schema"; import { ScriptField } from "../../../fields/ScriptField"; -import { StrCast, ScriptCast, Cast } from "../../../fields/Types"; +import { Cast, NumCast, ScriptCast, StrCast, BoolCast } from "../../../fields/Types"; +import { returnEmptyString } from "../../../Utils"; +import { DragManager } from "../../util/DragManager"; import { InteractionUtils } from "../../util/InteractionUtils"; -import { CompileScript, isCompileError, ScriptParam } from "../../util/Scripting"; +import { CompileScript, Scripting, ScriptParam } from "../../util/Scripting"; +import { ScriptManager } from "../../util/ScriptManager"; +import { ContextMenu } from "../ContextMenu"; import { ViewBoxAnnotatableComponent } from "../DocComponent"; import { EditableView } from "../EditableView"; import { FieldView, FieldViewProps } from "../nodes/FieldView"; -import "./ScriptingBox.scss"; import { OverlayView } from "../OverlayView"; import { DocumentIconContainer } from "./DocumentIcon"; -import { List } from "../../../fields/List"; +import "./ScriptingBox.scss"; +import { TraceMobx } from "../../../fields/util"; +const _global = (window /* browser */ || global /* node */) as any; const ScriptingSchema = createSchema({}); type ScriptingDocument = makeInterface<[typeof ScriptingSchema, typeof documentSchema]>; @@ -21,78 +30,664 @@ const ScriptingDocument = makeInterface(ScriptingSchema, documentSchema); @observer export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps, ScriptingDocument>(ScriptingDocument) { + + private dropDisposer?: DragManager.DragDropDisposer; protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer | undefined; public static LayoutString(fieldStr: string) { return FieldView.LayoutString(ScriptingBox, fieldStr); } - - _overlayDisposer?: () => void; + private _overlayDisposer?: () => void; + private _caretPos = 0; @observable private _errorMessage: string = ""; + @observable private _applied: boolean = false; + @observable private _function: boolean = false; + @observable private _spaced: boolean = false; + + @observable private _scriptKeys: any = Scripting.getGlobals(); + @observable private _scriptingDescriptions: any = Scripting.getDescriptions(); + @observable private _scriptingParams: any = Scripting.getParameters(); + + @observable private _currWord: string = ""; + @observable private _suggestions: string[] = []; + + @observable private _suggestionBoxX: number = 0; + @observable private _suggestionBoxY: number = 0; + @observable private _lastChar: string = ""; + + @observable private _suggestionRef: any = React.createRef(); + @observable private _scriptTextRef: any = React.createRef(); + + @observable private _selection: any = 0; + + @observable private _paramSuggestion: boolean = false; + @observable private _scriptSuggestedParams: any = ""; + @observable private _scriptParamsText: any = ""; + + // vars included in fields that store parameters types and names and the script itself + @computed({ keepAlive: true }) get paramsNames() { return this.compileParams.map(p => p.split(":")[0].trim()); } + @computed({ keepAlive: true }) get paramsTypes() { return this.compileParams.map(p => p.split(":")[1].trim()); } + @computed({ keepAlive: true }) get rawScript() { return StrCast(this.dataDoc[this.props.fieldKey + "-rawScript"], ""); } + @computed({ keepAlive: true }) get functionName() { return StrCast(this.dataDoc[this.props.fieldKey + "-functionName"], ""); } + @computed({ keepAlive: true }) get functionDescription() { return StrCast(this.dataDoc[this.props.fieldKey + "-functionDescription"], ""); } + @computed({ keepAlive: true }) get compileParams() { return Cast(this.dataDoc[this.props.fieldKey + "-params"], listSpec("string"), []); } - @computed get rawScript() { return StrCast(this.dataDoc[this.props.fieldKey + "-rawScript"], StrCast(this.layoutDoc[this.props.fieldKey + "-rawScript"])); } - @computed get compileParams() { return Cast(this.dataDoc[this.props.fieldKey + "-params"], listSpec("string"), Cast(this.layoutDoc[this.props.fieldKey + "-params"], listSpec("string"), [])); } set rawScript(value) { this.dataDoc[this.props.fieldKey + "-rawScript"] = value; } - set compileParams(value) { this.dataDoc[this.props.fieldKey + "-params"] = value; } + set functionName(value) { this.dataDoc[this.props.fieldKey + "-functionName"] = value; } + set functionDescription(value) { this.dataDoc[this.props.fieldKey + "-functionDescription"] = value; } + + set compileParams(value) { this.dataDoc[this.props.fieldKey + "-params"] = new List<string>(value); } + + getValue(result: any, descrip: boolean) { + if (typeof result === "object") { + const text = descrip ? result[1] : result[2]; + return text !== undefined ? text : ""; + } else { + return ""; + } + } @action componentDidMount() { - this.rawScript = ScriptCast(this.dataDoc[this.props.fieldKey])?.script?.originalScript || this.rawScript; + this.rawScript = ScriptCast(this.dataDoc[this.props.fieldKey])?.script?.originalScript ?? this.rawScript; + + const observer = new _global.ResizeObserver(action((entries: any) => { + const area = document.querySelector('textarea'); + if (area) { + for (const { } of entries) { + const getCaretCoordinates = require('textarea-caret'); + const caret = getCaretCoordinates(area, this._selection); + this.resetSuggestionPos(caret); + } + } + })); + observer.observe(document.getElementsByClassName("scriptingBox")[0]); } - componentWillUnmount() { this._overlayDisposer?.(); } + @action + resetSuggestionPos(caret: any) { + if (!this._suggestionRef.current || !this._scriptTextRef.current) return; + console.log('(top, left, height) = (%s, %s, %s)', caret.top, caret.left, caret.height); + const suggestionWidth = this._suggestionRef.current.offsetWidth; + const scriptWidth = this._scriptTextRef.current.offsetWidth; + const top = caret.top; + const x = this.dataDoc.x; + let left = caret.left; + if ((left + suggestionWidth) > (x + scriptWidth)) { + const diff = (left + suggestionWidth) - (x + scriptWidth); + left = left - diff; + } + + this._suggestionBoxX = left; + this._suggestionBoxY = top; + } + componentWillUnmount() { + this._overlayDisposer?.(); + } + + protected createDashEventsTarget = (ele: HTMLDivElement, dropFunc: (e: Event, de: DragManager.DropEvent) => void) => { //used for stacking and masonry view + if (ele) { + this.dropDisposer?.(); + this.dropDisposer = DragManager.MakeDropTarget(ele, dropFunc, this.layoutDoc); + } + } + + // only included in buttons, transforms scripting UI to a button + @action + onFinish = () => { + this.rootDoc.layoutKey = "layout"; + this.rootDoc._height = 50; + this.rootDoc._width = 100; + this.dataDoc.documentText = this.rawScript; + } + + // displays error message + @action + onError = (error: any) => { + this._errorMessage = error?.message ? error.message : error?.map((entry: any) => entry.messageText).join(" ") || ""; + } + + // checks if the script compiles using CompileScript method and inputting params @action onCompile = () => { - const params = this.compileParams.reduce((o: ScriptParam, p: string) => { o[p] = "any"; return o; }, {} as ScriptParam); + const params: ScriptParam = {}; + this.compileParams.forEach(p => params[p.split(":")[0].trim()] = p.split(":")[1].trim()); + const result = CompileScript(this.rawScript, { editable: true, transformer: DocumentIconContainer.getTransformer(), params, typecheck: false }); - this._errorMessage = isCompileError(result) ? result.errors.map(e => e.messageText).join("\n") : ""; - return this.dataDoc[this.props.fieldKey] = result.compiled ? new ScriptField(result) : undefined; + this.dataDoc.documentText = this.rawScript; + this.dataDoc.data = result.compiled ? new ScriptField(result) : undefined; + this.onError(result.compiled ? undefined : result.errors); + return result.compiled; } + // checks if the script compiles and then runs the script @action onRun = () => { - this.onCompile()?.script.run({}, err => this._errorMessage = err.map((e: any) => e.messageText).join("\n")); + if (this.onCompile()) { + const bindings: { [name: string]: any } = {}; + this.paramsNames.forEach(key => bindings[key] = this.dataDoc[key]); + // binds vars so user doesnt have to refer to everything as self.<var> + ScriptCast(this.dataDoc.data, null)?.script.run({ self: this.rootDoc, this: this.layoutDoc, ...bindings }, this.onError); + } + } + + // checks if the script compiles and switches to applied UI + @action + onApply = () => { + if (this.onCompile()) { + this._applied = true; + } } + @action + onEdit = () => { + this._errorMessage = ""; + this._applied = false; + this._function = false; + } + + @action + onSave = () => { + if (this.onCompile()) { + this._function = true; + } else { + this._errorMessage = "Can not save script, does not compile"; + } + } + + @action + onCreate = () => { + this._errorMessage = ""; + + if (this.functionName.length === 0) { + this._errorMessage = "Must enter a function name"; + return false; + } + + if (this.functionName.indexOf(" ") > 0) { + this._errorMessage = "Name can not include spaces"; + return false; + } + + if (this.functionName.indexOf(".") > 0) { + this._errorMessage = "Name can not include '.'"; + return false; + } + + this.dataDoc.name = this.functionName; + this.dataDoc.description = this.functionDescription; + //this.dataDoc.parameters = this.compileParams; + this.dataDoc.script = this.rawScript; + + ScriptManager.Instance.addScript(this.dataDoc); + + this._scriptKeys = Scripting.getGlobals(); + this._scriptingDescriptions = Scripting.getDescriptions(); + this._scriptingParams = Scripting.getParameters(); + } + + // overlays document numbers (ex. d32) over all documents when clicked on onFocus = () => { this._overlayDisposer?.(); this._overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 }); } + // sets field of the corresponding field key (param name) to be dropped document + @action + onDrop = (e: Event, de: DragManager.DropEvent, fieldKey: string) => { + this.dataDoc[fieldKey] = de.complete.docDragData?.droppedDocuments[0]; + e.stopPropagation(); + } + + // deletes a param from all areas in which it is stored + @action + onDelete = (num: number) => { + this.dataDoc[this.paramsNames[num]] = undefined; + this.compileParams.splice(num, 1); + return true; + } + + // sets field of the param name to the selected value in drop down box + @action + viewChanged = (e: React.ChangeEvent, name: string) => { + //@ts-ignore + const val = e.target.selectedOptions[0].value; + this.dataDoc[name] = val[0] === "S" ? val.substring(1) : val[0] === "N" ? parseInt(val.substring(1)) : val.substring(1) === "true"; + } + + // creates a copy of the script document + onCopy = () => { + const copy = Doc.MakeCopy(this.rootDoc, true); + copy.x = NumCast(this.dataDoc.x) + NumCast(this.dataDoc._width); + this.props.addDocument?.(copy); + } + + // adds option to create a copy to the context menu + specificContextMenu = (): void => { + const existingOptions = ContextMenu.Instance.findByDescription("Options..."); + const options = existingOptions && "subitems" in existingOptions ? existingOptions.subitems : []; + options.push({ description: "Create a Copy", event: this.onCopy, icon: "copy" }); + !existingOptions && ContextMenu.Instance.addItem({ description: "Options...", subitems: options, icon: "hand-point-right" }); + } + + renderFunctionInputs() { + const descriptionInput = + <textarea + className="scriptingBox-textarea-inputs" + onChange={e => this.functionDescription = e.target.value} + placeholder="enter description here" + value={this.functionDescription} + />; + const nameInput = + <textarea + className="scriptingBox-textarea-inputs" + onChange={e => this.functionName = e.target.value} + placeholder="enter name here" + value={this.functionName} + />; + + return <div className="scriptingBox-inputDiv" onPointerDown={e => this.props.isSelected() && e.stopPropagation()} > + <div className="scriptingBox-wrapper" style={{ maxWidth: "100%" }}> + <div className="container" style={{ maxWidth: "100%" }}> + <div className="descriptor" style={{ textAlign: "center", display: "inline-block", maxWidth: "100%" }}> Enter a function name: </div> + <div style={{ maxWidth: "100%" }}> {nameInput}</div> + <div className="descriptor" style={{ textAlign: "center", display: "inline-block", maxWidth: "100%" }}> Enter a function description: </div> + <div style={{ maxWidth: "100%" }}>{descriptionInput}</div> + </div> + </div> + {this.renderErrorMessage()} + </div>; + } + + renderErrorMessage() { + return !this._errorMessage ? (null) : <div className="scriptingBox-errorMessage"> {this._errorMessage} </div>; + } + + // rendering when a doc's value can be set in applied UI + renderDoc(parameter: string) { + return <div className="scriptingBox-paramInputs" onFocus={this.onFocus} onBlur={() => this._overlayDisposer?.()} + ref={ele => ele && this.createDashEventsTarget(ele, (e, de) => this.onDrop(e, de, parameter))} > + <EditableView display={"block"} maxHeight={72} height={35} fontSize={14} + contents={this.dataDoc[parameter]?.title ?? "undefined"} + GetValue={() => this.dataDoc[parameter]?.title ?? "undefined"} + SetValue={action((value: string) => { + const script = CompileScript(value, { + addReturn: true, + typecheck: false, + transformer: DocumentIconContainer.getTransformer() + }); + const results = script.compiled && script.run(); + if (results && results.success) { + this._errorMessage = ""; + this.dataDoc[parameter] = results.result; + return true; + } + this._errorMessage = "invalid document"; + return false; + })} + /> + </div>; + } + + // rendering when a string's value can be set in applied UI + renderBasicType(parameter: string, isNum: boolean) { + const strVal = (isNum ? NumCast(this.dataDoc[parameter]).toString() : StrCast(this.dataDoc[parameter])); + return <div className="scriptingBox-paramInputs" style={{ overflowY: "hidden" }}> + <EditableView display={"block"} maxHeight={72} height={35} fontSize={14} + contents={strVal ?? "undefined"} + GetValue={() => strVal ?? "undefined"} + SetValue={action((value: string) => { + const setValue = isNum ? parseInt(value) : value; + if (setValue !== undefined && setValue !== " ") { + this._errorMessage = ""; + this.dataDoc[parameter] = setValue; + return true; + } + this._errorMessage = "invalid input"; + return false; + })} + /> + </div>; + } + + // rendering when an enum's value can be set in applied UI (drop down box) + renderEnum(parameter: string, types: (string | boolean | number)[]) { + return <div className="scriptingBox-paramInputs"> + <div className="scriptingBox-viewBase"> + <div className="commandEntry-outerDiv"> + <select className="scriptingBox-viewPicker" + onPointerDown={e => e.stopPropagation()} + onChange={e => this.viewChanged(e, parameter)} + value={typeof (this.dataDoc[parameter]) === "string" ? "S" + StrCast(this.dataDoc[parameter]) : + typeof (this.dataDoc[parameter]) === "number" ? "N" + NumCast(this.dataDoc[parameter]) : + "B" + BoolCast(this.dataDoc[parameter])}> + {types.map(type => + <option className="scriptingBox-viewOption" value={(typeof (type) === "string" ? "S" : typeof (type) === "number" ? "N" : "B") + type}> {type.toString()} </option> + )} + </select> + </div> + </div> + </div>; + } + + // setting a parameter (checking type and name before it is added) + compileParam(value: string, whichParam?: number) { + if (value.includes(":")) { + const ptype = value.split(":")[1].trim(); + const pname = value.split(":")[0].trim(); + if (ptype === "Doc" || ptype === "string" || ptype === "number" || ptype === "boolean" || ptype.split("|")[1]) { + if ((whichParam !== undefined && pname === this.paramsNames[whichParam]) || !this.paramsNames.includes(pname)) { + this._errorMessage = ""; + if (whichParam !== undefined) { + this.compileParams[whichParam] = value; + } else { + this.compileParams = [...value.split(";").filter(s => s), ...this.compileParams]; + } + return true; + } + this._errorMessage = "this name has already been used"; + } else { + this._errorMessage = "this type is not supported"; + } + } else { + this._errorMessage = "must set type of parameter"; + } + return false; + } + + @action + handleToken(str: string) { + this._currWord = str; + this._suggestions = []; + this._scriptKeys.forEach((element: string) => { + if (element.toLowerCase().indexOf(this._currWord.toLowerCase()) >= 0) { + this._suggestions.push(StrCast(element)); + } + }); + return (this._suggestions); + } + + @action + handleFunc(pos: number) { + const scriptString = this.rawScript.slice(0, pos - 2); + this._currWord = scriptString.split(" ")[scriptString.split(" ").length - 1]; + this._suggestions = [StrCast(this._scriptingParams[this._currWord])]; + return (this._suggestions); + } + + + getDescription(value: string) { + const descrip = this._scriptingDescriptions[value]; + return descrip?.length > 0 ? descrip : ""; + } + + getParams(value: string) { + const params = this._scriptingParams[value]; + return params?.length > 0 ? params : ""; + } + + returnParam(item: string) { + const params = item.split(","); + let value = ""; + let first = true; + params.forEach((element) => { + if (first) { + value = element.split(":")[0].trim(); + first = false; + } else { + value = value + ", " + element.split(":")[0].trim(); + } + }); + return value; + } + + getSuggestedParams(pos: number) { + const firstScript = this.rawScript.slice(0, pos); + const indexP = firstScript.lastIndexOf("."); + const indexS = firstScript.lastIndexOf(" "); + const func = firstScript.slice((indexP > indexS ? indexP : indexS) + 1, firstScript.length + 1); + return this._scriptingParams[func]; + } + + @action + suggestionPos = () => { + const getCaretCoordinates = require('textarea-caret'); + const This = this; + document.querySelector('textarea')?.addEventListener("input", function () { + const caret = getCaretCoordinates(this, this.selectionEnd); + This._selection = this; + This.resetSuggestionPos(caret); + }); + } + + @action + keyHandler(e: any, pos: number) { + if (this._lastChar === "Enter") { + this.rawScript = this.rawScript + " "; + } + if (e.key === "(") { + this.suggestionPos(); + + this._scriptParamsText = this.getSuggestedParams(pos); + this._scriptSuggestedParams = this.getSuggestedParams(pos); + + if (this._scriptParamsText !== undefined && this._scriptParamsText.length > 0) { + if (this.rawScript[pos - 2] !== "(") { + this._paramSuggestion = true; + } + } + } else if (e.key === ")") { + this._paramSuggestion = false; + } else { + if (e.key === "Backspace") { + if (this._lastChar === "(") { + this._paramSuggestion = false; + } else if (this._lastChar === ")") { + if (this.rawScript.slice(0, this.rawScript.length - 1).split("(").length - 1 > this.rawScript.slice(0, this.rawScript.length - 1).split(")").length - 1) { + if (this._scriptParamsText.length > 0) { + this._paramSuggestion = true; + } + } + } + } else if (this.rawScript.split("(").length - 1 <= this.rawScript.split(")").length - 1) { + this._paramSuggestion = false; + } + } + this._lastChar = e.key === "Backspace" ? this.rawScript[this.rawScript.length - 2] : e.key; + + if (this._paramSuggestion) { + const parameters = this._scriptParamsText.split(","); + const index = this.rawScript.lastIndexOf("("); + const enteredParams = this.rawScript.slice(index, this.rawScript.length); + const splitEntered = enteredParams.split(","); + const numEntered = splitEntered.length; + + parameters.forEach((element: string, i: number) => { + if (i !== parameters.length - 1) { + parameters[i] = element + ","; + } + }); + + let first = ""; + let last = ""; + + parameters.forEach((element: string, i: number) => { + if (i < numEntered - 1) { + first = first + element; + } else if (i > numEntered - 1) { + last = last + element; + } + }); + + this._scriptSuggestedParams = <div> {first} <b>{parameters[numEntered - 1]}</b> {last} </div>; + } + } + + @action + handlePosChange(number: any) { + this._caretPos = number; + if (this._caretPos === 0) { + this.rawScript = " " + this.rawScript; + } else if (this._spaced) { + this._spaced = false; + if (this.rawScript[this._caretPos - 1] === " ") { + this.rawScript = this.rawScript.slice(0, this._caretPos - 1) + + this.rawScript.slice(this._caretPos, this.rawScript.length); + } + } + } + + @computed({ keepAlive: true }) get renderScriptingBox() { + TraceMobx(); + return <div style={{ width: this.compileParams.length > 0 ? "70%" : "100%" }} ref={this._scriptTextRef}> + <ReactTextareaAutocomplete className="ScriptingBox-textarea-script" + minChar={1} + placeholder="write your script here" + onFocus={this.onFocus} + onBlur={() => this._overlayDisposer?.()} + onChange={e => this.rawScript = e.target.value} + value={this.rawScript} + movePopupAsYouType={true} + loadingComponent={() => <span>Loading</span>} + + trigger={{ + " ": { + dataProvider: (token: any) => this.handleToken(token), + component: ({ entity: value }) => this.renderFuncListElement(value), + output: (item: any, trigger) => { + this._spaced = true; + return trigger + item.trim(); + }, + }, + ".": { + dataProvider: (token: any) => this.handleToken(token), + component: ({ entity: value }) => this.renderFuncListElement(value), + output: (item: any, trigger) => { + this._spaced = true; + return trigger + item.trim(); + }, + } + }} + onKeyDown={(e) => this.keyHandler(e, this._caretPos)} + onCaretPositionChange={(number: any) => this.handlePosChange(number)} + /> + </div>; + } + + renderFuncListElement(value: string) { + return <div> + <div style={{ fontSize: "14px" }}> + {value} + </div> + <div key="desc" style={{ fontSize: "10px" }}>{this.getDescription(value)}</div> + <div key="params" style={{ fontSize: "10px" }}>{this.getParams(value)}</div> + </div>; + } + + // inputs for scripting div (script box, params box, and params column) + @computed({ keepAlive: true }) get renderScriptingInputs() { + + // should there be a border? style={{ borderStyle: "groove", borderBlockWidth: "1px" }} + // params box on bottom + const parameterInput = <div className="scriptingBox-params"> + <EditableView display={"block"} maxHeight={72} height={35} fontSize={22} + contents={""} + GetValue={returnEmptyString} + SetValue={value => value && value !== " " ? this.compileParam(value) : false} + placeholder={"enter parameters here"} + /> + </div>; + + // params column on right side (list) + const definedParameters = !this.compileParams.length ? (null) : + <div className="scriptingBox-plist" style={{ width: "30%" }}> + {this.compileParams.map((parameter, i) => + <div className="scriptingBox-pborder" onKeyPress={e => e.key === "Enter" && this._overlayDisposer?.()} > + <EditableView display={"block"} maxHeight={72} height={35} fontSize={12} background-color={"beige"} + contents={parameter} + GetValue={() => parameter} + SetValue={value => value && value !== " " ? this.compileParam(value, i) : this.onDelete(i)} + /> + </div> + )} + </div>; + + return <div className="scriptingBox-inputDiv" onPointerDown={e => this.props.isSelected() && e.stopPropagation()} > + <div className="scriptingBox-wrapper"> + {this.renderScriptingBox} + {definedParameters} + </div> + {parameterInput} + {this.renderErrorMessage()} + </div>; + } + + // toolbar (with compile and apply buttons) for scripting UI + renderScriptingTools() { + const buttonStyle = "scriptingBox-button" + (this.rootDoc.layoutKey === "layout_onClick" ? "third" : ""); + return <div className="scriptingBox-toolbar"> + <button className={buttonStyle} style={{ width: "33%" }} onPointerDown={e => { this.onCompile(); e.stopPropagation(); }}>Compile</button> + <button className={buttonStyle} style={{ width: "33%" }} onPointerDown={e => { this.onApply(); e.stopPropagation(); }}>Apply</button> + <button className={buttonStyle} style={{ width: "33%" }} onPointerDown={e => { this.onSave(); e.stopPropagation(); }}>Save</button> + + {this.rootDoc.layoutKey !== "layout_onClick" ? (null) : + <button className={buttonStyle} onPointerDown={e => { this.onFinish(); e.stopPropagation(); }}>Finish</button>} + </div>; + } + + // inputs UI for params which allows you to set values for each displayed in a list + renderParamsInputs() { + return <div className="scriptingBox-inputDiv" onPointerDown={e => this.props.isSelected(true) && e.stopPropagation()} > + {!this.compileParams.length || !this.paramsNames ? (null) : + <div className="scriptingBox-plist"> + {this.paramsNames.map((parameter: string, i: number) => + <div className="scriptingBox-pborder" onKeyPress={e => e.key === "Enter" && this._overlayDisposer?.()} > + <div className="scriptingBox-wrapper" style={{ maxHeight: "40px" }}> + <div className="scriptingBox-paramNames" > {`${parameter}:${this.paramsTypes[i]} = `} </div> + {this.paramsTypes[i] === "boolean" ? this.renderEnum(parameter, [true, false]) : (null)} + {this.paramsTypes[i] === "string" ? this.renderBasicType(parameter, false) : (null)} + {this.paramsTypes[i] === "number" ? this.renderBasicType(parameter, true) : (null)} + {this.paramsTypes[i] === "Doc" ? this.renderDoc(parameter) : (null)} + {this.paramsTypes[i]?.split("|")[1] ? this.renderEnum(parameter, this.paramsTypes[i].split("|").map(s => !isNaN(parseInt(s.trim())) ? parseInt(s.trim()) : s.trim())) : (null)} + </div> + </div>)} + </div>} + {this.renderErrorMessage()} + </div>; + } + + // toolbar (with edit and run buttons and error message) for params UI + renderTools(toolSet: string, func: () => void) { + const buttonStyle = "scriptingBox-button" + (this.rootDoc.layoutKey === "layout_onClick" ? "third" : ""); + return <div className="scriptingBox-toolbar"> + <button className={buttonStyle} onPointerDown={e => { this.onEdit(); e.stopPropagation(); }}>Edit</button> + <button className={buttonStyle} onPointerDown={e => { func(); e.stopPropagation(); }}>{toolSet}</button> + {this.rootDoc.layoutKey !== "layout_onClick" ? (null) : + <button className={buttonStyle} onPointerDown={e => { this.onFinish(); e.stopPropagation(); }}>Finish</button>} + </div>; + } + + // renders script UI if _applied = false and params UI if _applied = true render() { - const params = <EditableView - contents={this.compileParams.join(" ")} - display={"block"} - maxHeight={72} - height={35} - fontSize={28} - GetValue={() => ""} - SetValue={value => { this.compileParams = new List<string>(value.split(" ").filter(s => s !== " ")); return true; }} - />; return ( - <div className="scriptingBox-outerDiv" - onWheel={e => this.props.isSelected(true) && e.stopPropagation()}> - <div className="scriptingBox-inputDiv" - onPointerDown={e => this.props.isSelected(true) && e.stopPropagation()} > - <textarea className="scriptingBox-textarea" - placeholder="write your script here" - onChange={e => this.rawScript = e.target.value} - value={this.rawScript} - onFocus={this.onFocus} - onBlur={e => this._overlayDisposer?.()} /> - <div className="scriptingBox-errorMessage" style={{ background: this._errorMessage ? "red" : "" }}>{this._errorMessage}</div> - <div className="scriptingBox-params" >{params}</div> - </div> - {this.rootDoc.layout === "layout" ? <div></div> : (null)} - <div className="scriptingBox-toolbar"> - <button className="scriptingBox-button" onPointerDown={e => { this.onCompile(); e.stopPropagation(); }}>Compile</button> - <button className="scriptingBox-button" onPointerDown={e => { this.onRun(); e.stopPropagation(); }}>Run</button> + <div className={`scriptingBox`} onContextMenu={this.specificContextMenu} + onPointerUp={!this._function ? this.suggestionPos : undefined}> + <div className="scriptingBox-outerDiv" + onWheel={e => this.props.isSelected(true) && e.stopPropagation()}> + {this._paramSuggestion ? <div className="boxed" ref={this._suggestionRef} style={{ left: this._suggestionBoxX + 20, top: this._suggestionBoxY - 15, display: "inline" }}> {this._scriptSuggestedParams} </div> : null} + {!this._applied && !this._function ? this.renderScriptingInputs : null} + {this._applied && !this._function ? this.renderParamsInputs() : null} + {!this._applied && this._function ? this.renderFunctionInputs() : null} + + {!this._applied && !this._function ? this.renderScriptingTools() : null} + {this._applied && !this._function ? this.renderTools("Run", () => this.onRun()) : null} + {!this._applied && this._function ? this.renderTools("Create Function", () => this.onCreate()) : null} </div> </div> ); } -} +}
\ No newline at end of file diff --git a/src/client/views/nodes/formattedText/DashDocCommentView.tsx b/src/client/views/nodes/formattedText/DashDocCommentView.tsx index d56b87ae5..5c75a589a 100644 --- a/src/client/views/nodes/formattedText/DashDocCommentView.tsx +++ b/src/client/views/nodes/formattedText/DashDocCommentView.tsx @@ -1,95 +1,112 @@ -import { IReactionDisposer, observable, reaction, runInAction } from "mobx"; -import { baseKeymap, toggleMark } from "prosemirror-commands"; -import { redo, undo } from "prosemirror-history"; -import { keymap } from "prosemirror-keymap"; -import { DOMOutputSpecArray, Fragment, MarkSpec, Node, NodeSpec, Schema, Slice } from "prosemirror-model"; -import { bulletList, listItem, orderedList } from 'prosemirror-schema-list'; -import { EditorState, NodeSelection, Plugin, TextSelection } from "prosemirror-state"; -import { StepMap } from "prosemirror-transform"; -import { EditorView } from "prosemirror-view"; +import { TextSelection } from "prosemirror-state"; import * as ReactDOM from 'react-dom'; -import { Doc, DocListCast, Field, HeightSym, WidthSym } from "../../../../fields/Doc"; -import { Id } from "../../../../fields/FieldSymbols"; -import { List } from "../../../../fields/List"; -import { ObjectField } from "../../../../fields/ObjectField"; -import { listSpec } from "../../../../fields/Schema"; -import { SchemaHeaderField } from "../../../../fields/SchemaHeaderField"; -import { ComputedField } from "../../../../fields/ScriptField"; -import { BoolCast, Cast, NumCast, StrCast } from "../../../../fields/Types"; -import { emptyFunction, returnEmptyString, returnFalse, returnOne, Utils, returnZero } from "../../../../Utils"; +import { Doc } from "../../../../fields/Doc"; import { DocServer } from "../../../DocServer"; - import React = require("react"); -import { schema } from "./schema_rts"; -interface IDashDocCommentView { - node: any; +// creates an inline comment in a note when '>>' is typed. +// the comment sits on the right side of the note and vertically aligns with its anchor in the text. +// the comment can be toggled on/off with the '<-' text anchor. +export class DashDocCommentView { + _fieldWrapper: HTMLDivElement; // container for label and value + + constructor(node: any, view: any, getPos: any) { + this._fieldWrapper = document.createElement("div"); + this._fieldWrapper.style.width = node.attrs.width; + this._fieldWrapper.style.height = node.attrs.height; + this._fieldWrapper.style.fontWeight = "bold"; + this._fieldWrapper.style.position = "relative"; + this._fieldWrapper.style.display = "inline-block"; + this._fieldWrapper.onkeypress = function (e: any) { e.stopPropagation(); }; + this._fieldWrapper.onkeydown = function (e: any) { e.stopPropagation(); }; + this._fieldWrapper.onkeyup = function (e: any) { e.stopPropagation(); }; + this._fieldWrapper.onmousedown = function (e: any) { e.stopPropagation(); }; + + ReactDOM.render(<DashDocCommentViewInternal view={view} getPos={getPos} docid={node.attrs.docid} />, this._fieldWrapper); + (this as any).dom = this._fieldWrapper; + } + + destroy() { + ReactDOM.unmountComponentAtNode(this._fieldWrapper); + } + + selectNode() { } +} + +interface IDashDocCommentViewInternal { + docid: string; view: any; getPos: any; } -export class DashDocCommentView extends React.Component<IDashDocCommentView>{ - constructor(props: IDashDocCommentView) { +export class DashDocCommentViewInternal extends React.Component<IDashDocCommentViewInternal>{ + + constructor(props: IDashDocCommentViewInternal) { super(props); + this.onPointerLeaveCollapsed = this.onPointerLeaveCollapsed.bind(this); + this.onPointerEnterCollapsed = this.onPointerEnterCollapsed.bind(this); + this.onPointerUpCollapsed = this.onPointerUpCollapsed.bind(this); + this.onPointerDownCollapsed = this.onPointerDownCollapsed.bind(this); } - targetNode = () => { // search forward in the prosemirror doc for the attached dashDocNode that is the target of the comment anchor - for (let i = this.props.getPos() + 1; i < this.props.view.state.doc.content.size; i++) { - const m = this.props.view.state.doc.nodeAt(i); - if (m && m.type === this.props.view.state.schema.nodes.dashDoc && m.attrs.docid === this.props.node.attrs.docid) { - return { node: m, pos: i, hidden: m.attrs.hidden } as { node: any, pos: number, hidden: boolean }; - } - } - const dashDoc = this.props.view.state.schema.nodes.dashDoc.create({ width: 75, height: 35, title: "dashDoc", docid: this.props.node.attrs.docid, float: "right" }); - this.props.view.dispatch(this.props.view.state.tr.insert(this.props.getPos() + 1, dashDoc)); - setTimeout(() => { try { this.props.view.dispatch(this.props.view.state.tr.setSelection(TextSelection.create(this.props.view.state.tr.doc, this.props.getPos() + 2))); } catch (e) { } }, 0); - return undefined; + onPointerLeaveCollapsed(e: any) { + DocServer.GetRefField(this.props.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowUnhighlight()); + e.preventDefault(); + e.stopPropagation(); } - onPointerDownCollapse = (e: any) => e.stopPropagation(); + onPointerEnterCollapsed(e: any) { + DocServer.GetRefField(this.props.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc, false)); + e.preventDefault(); + e.stopPropagation(); + } - onPointerUpCollapse = (e: any) => { + onPointerUpCollapsed(e: any) { const target = this.targetNode(); + if (target) { const expand = target.hidden; const tr = this.props.view.state.tr.setNodeMarkup(target.pos, undefined, { ...target.node.attrs, hidden: target.node.attrs.hidden ? false : true }); this.props.view.dispatch(tr.setSelection(TextSelection.create(tr.doc, this.props.getPos() + (expand ? 2 : 1)))); // update the attrs setTimeout(() => { - expand && DocServer.GetRefField(this.props.node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc)); + expand && DocServer.GetRefField(this.props.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc)); try { this.props.view.dispatch(this.props.view.state.tr.setSelection(TextSelection.create(this.props.view.state.tr.doc, this.props.getPos() + (expand ? 2 : 1)))); } catch (e) { } }, 0); } e.stopPropagation(); } - onPointerEnterCollapse = (e: any) => { - DocServer.GetRefField(this.props.node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc, false)); - e.preventDefault(); + onPointerDownCollapsed(e: any) { e.stopPropagation(); } - onPointerLeaveCollapse = (e: any) => { - DocServer.GetRefField(this.props.node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowUnhighlight()); - e.preventDefault(); - e.stopPropagation(); + targetNode = () => { // search forward in the prosemirror doc for the attached dashDocNode that is the target of the comment anchor + const state = this.props.view.state; + for (let i = this.props.getPos() + 1; i < state.doc.content.size; i++) { + const m = state.doc.nodeAt(i); + if (m && m.type === state.schema.nodes.dashDoc && m.attrs.docid === this.props.docid) { + return { node: m, pos: i, hidden: m.attrs.hidden } as { node: any, pos: number, hidden: boolean }; + } + } + + const dashDoc = state.schema.nodes.dashDoc.create({ width: 75, height: 35, title: "dashDoc", docid: this.props.docid, float: "right" }); + this.props.view.dispatch(state.tr.insert(this.props.getPos() + 1, dashDoc)); + setTimeout(() => { try { this.props.view.dispatch(state.tr.setSelection(TextSelection.create(state.tr.doc, this.props.getPos() + 2))); } catch (e) { } }, 0); + return undefined; } render() { - - const collapsedId = "DashDocCommentView-" + this.props.node.attrs.docid; - return ( <span className="formattedTextBox-inlineComment" - id={collapsedId} - onPointerDown={this.onPointerDownCollapse} - onPointerUp={this.onPointerUpCollapse} - onPointerEnter={this.onPointerEnterCollapse} - onPointerLeave={this.onPointerLeaveCollapse} + id={"DashDocCommentView-" + this.props.docid} + onPointerLeave={this.onPointerLeaveCollapsed} + onPointerEnter={this.onPointerEnterCollapsed} + onPointerUp={this.onPointerUpCollapsed} + onPointerDown={this.onPointerDownCollapsed} > - - </span > + </span> ); } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx index 60a9c2a27..8c16f4a1a 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.tsx +++ b/src/client/views/nodes/formattedText/DashFieldView.tsx @@ -14,7 +14,6 @@ import "./DashFieldView.scss"; import { observer } from "mobx-react"; import { DocUtils } from "../../../documents/Documents"; - export class DashFieldView { _fieldWrapper: HTMLDivElement; // container for label and value @@ -40,12 +39,10 @@ export class DashFieldView { />, this._fieldWrapper); (this as any).dom = this._fieldWrapper; } - destroy() { - ReactDOM.unmountComponentAtNode(this._fieldWrapper); - } + destroy() { ReactDOM.unmountComponentAtNode(this._fieldWrapper); } selectNode() { } - } + interface IDashFieldViewInternal { fieldKey: string; docid: string; diff --git a/src/client/views/nodes/formattedText/FootnoteView.tsx b/src/client/views/nodes/formattedText/FootnoteView.tsx index ee21fb765..1683cc972 100644 --- a/src/client/views/nodes/formattedText/FootnoteView.tsx +++ b/src/client/views/nodes/formattedText/FootnoteView.tsx @@ -6,54 +6,50 @@ import { schema } from "./schema_rts"; import { redo, undo } from "prosemirror-history"; import { StepMap } from "prosemirror-transform"; -import React = require("react"); - -interface IFootnoteView { +export class FootnoteView { innerView: any; outerView: any; node: any; dom: any; getPos: any; -} -export class FootnoteView extends React.Component<IFootnoteView> { - _innerView: any; - _node: any; + constructor(node: any, view: any, getPos: any) { + // We'll need these later + this.node = node; + this.outerView = view; + this.getPos = getPos; + + // The node's representation in the editor (empty, for now) + this.dom = document.createElement("footnote"); - constructor(props: IFootnoteView) { - super(props); - const node = this.props.node; - const outerView = this.props.outerView; - const _innerView = this.props.innerView; - const getPos = this.props.getPos; + this.dom.addEventListener("pointerup", this.toggle, true); + // These are used when the footnote is selected + this.innerView = null; } selectNode() { - const attrs = { ...this.props.node.attrs }; - attrs.visibility = true; this.dom.classList.add("ProseMirror-selectednode"); - if (!this.props.innerView) this.open(); + if (!this.innerView) this.open(); } deselectNode() { - const attrs = { ...this.props.node.attrs }; - attrs.visibility = false; this.dom.classList.remove("ProseMirror-selectednode"); - if (this.props.innerView) this.close(); + if (this.innerView) this.close(); } + open() { // Append a tooltip to the outer node const tooltip = this.dom.appendChild(document.createElement("div")); tooltip.className = "footnote-tooltip"; // And put a sub-ProseMirror into that - this.props.innerView.defineProperty(new EditorView(tooltip, { + this.innerView = new EditorView(tooltip, { // You can use any node as an editor document state: EditorState.create({ - doc: this.props.node, + doc: this.node, plugins: [keymap(baseKeymap), keymap({ - "Mod-z": () => undo(this.props.outerView.state, this.props.outerView.dispatch), - "Mod-y": () => redo(this.props.outerView.state, this.props.outerView.dispatch), + "Mod-z": () => undo(this.outerView.state, this.outerView.dispatch), + "Mod-y": () => redo(this.outerView.state, this.outerView.dispatch), "Mod-b": toggleMark(schema.marks.strong) }), // new Plugin({ @@ -74,11 +70,11 @@ export class FootnoteView extends React.Component<IFootnoteView> { // the parent editor is focused. e.stopPropagation(); document.addEventListener("pointerup", this.ignore, true); - if (this.props.outerView.hasFocus()) this.props.innerView.focus(); + if (this.outerView.hasFocus()) this.innerView.focus(); }) as any } - })); - setTimeout(() => this.props.innerView && this.props.innerView.docView.setSelection(0, 0, this.props.innerView.root, true), 0); + }); + setTimeout(() => this.innerView?.docView.setSelection(0, 0, this.innerView.root, true), 0); } ignore = (e: PointerEvent) => { @@ -86,32 +82,43 @@ export class FootnoteView extends React.Component<IFootnoteView> { document.removeEventListener("pointerup", this.ignore, true); } + toggle = () => { + if (this.innerView) this.close(); + else this.open(); + } + + close() { + this.innerView?.destroy(); + this.innerView = null; + this.dom.textContent = ""; + } + dispatchInner(tr: any) { - const { state, transactions } = this.props.innerView.state.applyTransaction(tr); - this.props.innerView.updateState(state); + const { state, transactions } = this.innerView.state.applyTransaction(tr); + this.innerView.updateState(state); if (!tr.getMeta("fromOutside")) { - const outerTr = this.props.outerView.state.tr, offsetMap = StepMap.offset(this.props.getPos() + 1); + const outerTr = this.outerView.state.tr, offsetMap = StepMap.offset(this.getPos() + 1); for (const transaction of transactions) { - const steps = transaction.steps; - for (const step of steps) { + for (const step of transaction.steps) { outerTr.step(step.map(offsetMap)); } } - if (outerTr.docChanged) this.props.outerView.dispatch(outerTr); + if (outerTr.docChanged) this.outerView.dispatch(outerTr); } } + update(node: any) { - if (!node.sameMarkup(this.props.node)) return false; - this._node = node; //not sure - if (this.props.innerView) { - const state = this.props.innerView.state; + if (!node.sameMarkup(this.node)) return false; + this.node = node; + if (this.innerView) { + const state = this.innerView.state; const start = node.content.findDiffStart(state.doc.content); if (start !== null) { let { a: endA, b: endB } = node.content.findDiffEnd(state.doc.content); const overlap = start - Math.min(endA, endB); if (overlap > 0) { endA += overlap; endB += overlap; } - this.props.innerView.dispatch( + this.innerView.dispatch( state.tr .replace(start, endB, node.slice(start, endA)) .setMeta("fromOutside", true)); @@ -119,44 +126,17 @@ export class FootnoteView extends React.Component<IFootnoteView> { } return true; } - onPointerUp = (e: any) => { - this.toggle(e); - } - - toggle = (e: any) => { - e.preventDefault(); - if (this.props.innerView) this.close(); - else { - this.open(); - } - } - - close() { - this.props.innerView && this.props.innerView.destroy(); - this._innerView = null; - this.dom.textContent = ""; - } destroy() { - if (this.props.innerView) this.close(); + if (this.innerView) this.close(); } stopEvent(event: any) { - return this.props.innerView && this.props.innerView.dom.contains(event.target); + return this.innerView?.dom.contains(event.target); } - ignoreMutation() { return true; } - - - render() { - return ( - <div - className="footnote" - onPointerUp={this.onPointerUp}> - <div className="footnote-tooltip" > - - </div > - </div> - ); + ignoreMutation() { + return true; } } + diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 912bafc46..f3d0d135c 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -34,15 +34,15 @@ import { makeTemplate } from '../../../util/DropConverter'; import buildKeymap from "./ProsemirrorExampleTransfer"; import RichTextMenu from './RichTextMenu'; import { RichTextRules } from "./RichTextRules"; -import { DashDocCommentView, DashDocView, FootnoteView, ImageResizeView, OrderedListView, SummaryView } from "./RichTextSchema"; -// import { DashDocCommentView, DashDocView, DashFieldView, FootnoteView, SummaryView } from "./RichTextSchema"; -// import { OrderedListView } from "./RichTextSchema"; -// import { ImageResizeView } from "./ImageResizeView"; -// import { DashDocCommentView } from "./DashDocCommentView"; -// import { FootnoteView } from "./FootnoteView"; -// import { SummaryView } from "./SummaryView"; -// import { DashDocView } from "./DashDocView"; + +//import { DashDocView } from "./DashDocView"; +import { DashDocView } from "./RichTextSchema"; + +import { DashDocCommentView } from "./DashDocCommentView"; import { DashFieldView } from "./DashFieldView"; +import { SummaryView } from "./SummaryView"; +import { OrderedListView } from "./OrderedListView"; +import { FootnoteView } from "./FootnoteView"; import { schema } from "./schema_rts"; import { SelectionManager } from "../../../util/SelectionManager"; @@ -180,7 +180,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this.linkOnDeselect.set(key, value); const id = Utils.GenerateDeterministicGuid(this.dataDoc[Id] + key); - const link = this._editorView.state.schema.marks.link.create({ href: Utils.prepend("/doc/" + id), location: "onRight", title: value }); + const allHrefs = [{ href: Utils.prepend("/doc/" + id), title: value, targetId: id }]; + const link = this._editorView.state.schema.marks.link.create({ allHrefs, location: "onRight", title: value }); const mval = this._editorView.state.schema.marks.metadataVal.create(); const offset = (tx.selection.to === range!.end - 1 ? -1 : 0); tx = tx.addMark(textEndSelection - value.length + offset, textEndSelection, link).addMark(textEndSelection - value.length + offset, textEndSelection, mval); @@ -239,10 +240,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const lastSel = Math.min(flattened.length - 1, this._searchIndex); this._searchIndex = ++this._searchIndex > flattened.length - 1 ? 0 : this._searchIndex; const alink = DocUtils.MakeLink({ doc: this.rootDoc }, { doc: target }, "automatic")!; - const link = this._editorView.state.schema.marks.link.create({ - href: Utils.prepend("/doc/" + alink[Id]), - title: "a link", location: location, linkId: alink[Id], targetId: target[Id] - }); + const allHrefs = [{ href: Utils.prepend("/doc/" + alink[Id]), title: "a link", targetId: target[Id], linkId: alink[Id] }]; + const link = this._editorView.state.schema.marks.link.create({ allHrefs, title: "a link", location }); this._editorView.dispatch(tr.addMark(flattened[lastSel].from, flattened[lastSel].to, link)); } } @@ -293,8 +292,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new RichTextField(draggedDoc.data.Data, draggedDoc.data.Text); e.stopPropagation(); } - // embed document when dragging with a userDropAction or an embedDoc flag set - } else if (dragData.userDropAction || dragData.embedDoc) { + // embed document when dragg marked as embed + } else if (de.embedKey) { const target = dragData.droppedDocuments[0]; // const link = DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: target }, "Embedded Doc:" + target.title); // if (link) { @@ -483,7 +482,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp }), icon: "eye" }); }); - changeItems.push({ description: "FreeForm", event: undoBatch(() => DocUtils.makeCustomViewClicked(this.rootDoc, Docs.Create.FreeformDocument, "freeform"), "change view"), icon: "eye" }); + changeItems.push({ description: "FreeForm", event: () => DocUtils.makeCustomViewClicked(this.rootDoc, Docs.Create.FreeformDocument, "freeform"), icon: "eye" }); !change && cm.addItem({ description: "Change Perspective...", subitems: changeItems, icon: "external-link-alt" }); } @@ -906,17 +905,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp dispatchTransaction: this.dispatchTransaction, nodeViews: { dashComment(node, view, getPos) { return new DashDocCommentView(node, view, getPos); }, - dashField(node, view, getPos) { return new DashFieldView(node, view, getPos, self); }, dashDoc(node, view, getPos) { return new DashDocView(node, view, getPos, self); }, - // dashDoc(node, view, getPos) { return new DashDocView({ node, view, getPos, self }); }, - - // image(node, view, getPos) { - // //const addDocTab = this.props.addDocTab; - // return new ImageResizeView({ node, view, getPos, addDocTab: this.props.addDocTab }); - // }, - // // WAS : - // //image(node, view, getPos) { return new ImageResizeView(node, view, getPos, this.props.addDocTab); }, - + dashField(node, view, getPos) { return new DashFieldView(node, view, getPos, self); }, summary(node, view, getPos) { return new SummaryView(node, view, getPos); }, ordered_list(node, view, getPos) { return new OrderedListView(); }, footnote(node, view, getPos) { return new FootnoteView(node, view, getPos); } @@ -1112,7 +1102,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp e.stopPropagation(); const view = this._editorView as any; - // this interposes on prosemirror's upHandler to prevent prosemirror's up from invoked multiple times when there + // this interposes on prosemirror's upHandler to prevent prosemirror's up from invoked multiple times when there // are nested prosemirrors. We only want the lowest level prosemirror to be invoked. if (view.mouseDown) { const originalUpHandler = view.mouseDown.up; @@ -1177,7 +1167,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } const mark = e.key !== " " && this._lastTimedMark ? this._lastTimedMark : schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) }); this._lastTimedMark = mark; - this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(mark)); + // this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(mark)); if (!this._undoTyping) { this._undoTyping = UndoManager.StartBatch("undoTyping"); @@ -1226,9 +1216,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : ""; const interactive = Doc.GetSelectedTool() === InkTool.None && !this.layoutDoc.isBackground; if (this.props.isSelected()) { - this._editorView && RichTextMenu.Instance?.updateFromDash(this._editorView, undefined, this.props); + setTimeout(() => this._editorView && RichTextMenu.Instance.updateFromDash(this._editorView, undefined, this.props), 0); } else if (FormattedTextBoxComment.textBox === this) { - FormattedTextBoxComment.Hide(); + setTimeout(() => FormattedTextBoxComment.Hide(), 0); } return ( <div className={"formattedTextBox-cont"} style={{ diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx index 5ac173602..000b3c2e5 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx @@ -50,7 +50,9 @@ export function findEndOfMark(rpos: ResolvedPos, view: EditorView, finder: (mark return after; } - +// this view appears when clicking on text that has a hyperlink which is configured to show a preview of its target. +// this will also display metadata information about text when the view is configured to display things like other people who authored text. +// export class FormattedTextBoxComment { static tooltip: HTMLElement; static tooltipText: HTMLElement; @@ -235,9 +237,6 @@ export class FormattedTextBoxComment { FormattedTextBoxComment.tooltip.style.width = NumCast(target._width) ? `${NumCast(target._width)}` : "100%"; FormattedTextBoxComment.tooltip.style.height = NumCast(target._height) ? `${NumCast(target._height)}` : "100%"; } - // let ext = (target && target.type !== DocumentType.PDFANNO && Doc.fieldExtensionDoc(target, "data")) || target; // try guessing that the target doc's data is in the 'data' field. probably need an 'overviewLayout' and then just display the target Document .... - // let text = ext && StrCast(ext.text); - // ext && (FormattedTextBoxComment.tooltipText.textContent = (target && target.type === DocumentType.PDFANNO ? "Quoted from " : "") + "=> " + (text || StrCast(ext.title))); } }); } diff --git a/src/client/views/nodes/formattedText/ImageResizeView.tsx b/src/client/views/nodes/formattedText/ImageResizeView.tsx deleted file mode 100644 index 401ecd7e6..000000000 --- a/src/client/views/nodes/formattedText/ImageResizeView.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import { NodeSelection } from "prosemirror-state"; -import { Doc } from "../../../../fields/Doc"; -import { DocServer } from "../../../DocServer"; -import { DocumentManager } from "../../../util/DocumentManager"; -import React = require("react"); - -import { schema } from "./schema_rts"; - -interface IImageResizeView { - node: any; - view: any; - getPos: any; - addDocTab: any; -} - -export class ImageResizeView extends React.Component<IImageResizeView> { - constructor(props: IImageResizeView) { - super(props); - } - - onClickImg = (e: any) => { - e.stopPropagation(); - e.preventDefault(); - if (this.props.view.state.selection.node && this.props.view.state.selection.node.type !== this.props.view.state.schema.nodes.image) { - this.props.view.dispatch(this.props.view.state.tr.setSelection(new NodeSelection(this.props.view.state.doc.resolve(this.props.view.state.selection.from - 2)))); - } - } - - onPointerDownImg = (e: any) => { - if (e.ctrlKey) { - e.preventDefault(); - e.stopPropagation(); - DocServer.GetRefField(this.props.node.attrs.docid).then(async linkDoc => - (linkDoc instanceof Doc) && - DocumentManager.Instance.FollowLink(linkDoc, this.props.view.state.schema.Document, - document => this.props.addDocTab(document, this.props.node.attrs.location ? this.props.node.attrs.location : "inTab"), false)); - } - } - - onPointerDownHandle = (e: any) => { - e.preventDefault(); - e.stopPropagation(); - const elementImage = document.getElementById("imageId") as HTMLElement; - const wid = Number(getComputedStyle(elementImage).width.replace(/px/, "")); - const hgt = Number(getComputedStyle(elementImage).height.replace(/px/, "")); - const startX = e.pageX; - const startWidth = parseFloat(this.props.node.attrs.width); - - const onpointermove = (e: any) => { - const elementOuter = document.getElementById("outerId") as HTMLElement; - - const currentX = e.pageX; - const diffInPx = currentX - startX; - elementOuter.style.width = `${startWidth + diffInPx}`; - elementOuter.style.height = `${(startWidth + diffInPx) * hgt / wid}`; - }; - - const onpointerup = () => { - document.removeEventListener("pointermove", onpointermove); - document.removeEventListener("pointerup", onpointerup); - const pos = this.props.view.state.selection.from; - const elementOuter = document.getElementById("outerId") as HTMLElement; - this.props.view.dispatch(this.props.view.state.tr.setNodeMarkup(this.props.getPos(), null, { ...this.props.node.attrs, width: elementOuter.style.width, height: elementOuter.style.height })); - this.props.view.dispatch(this.props.view.state.tr.setSelection(new NodeSelection(this.props.view.state.doc.resolve(pos)))); - }; - - document.addEventListener("pointermove", onpointermove); - document.addEventListener("pointerup", onpointerup); - } - - selectNode() { - const elementImage = document.getElementById("imageId") as HTMLElement; - const elementHandle = document.getElementById("handleId") as HTMLElement; - - elementImage.classList.add("ProseMirror-selectednode"); - elementHandle.style.display = ""; - } - - deselectNode() { - const elementImage = document.getElementById("imageId") as HTMLElement; - const elementHandle = document.getElementById("handleId") as HTMLElement; - - elementImage.classList.remove("ProseMirror-selectednode"); - elementHandle.style.display = "none"; - } - - - render() { - - const outerStyle = { - width: this.props.node.attrs.width, - height: this.props.node.attrs.height, - display: "inline-block", - overflow: "hidden", - float: this.props.node.attrs.float - }; - - const imageStyle = { - width: "100%", - }; - - const handleStyle = { - position: "absolute", - width: "20px", - heiht: "20px", - backgroundColor: "blue", - borderRadius: "15px", - display: "none", - bottom: "-10px", - right: "-10px" - - }; - - - - return ( - <div id="outer" - style={outerStyle} - > - <img - id="imageId" - style={imageStyle} - src={this.props.node.src} - onClick={this.onClickImg} - onPointerDown={this.onPointerDownImg} - - > - </img> - <span - id="handleId" - onPointerDown={this.onPointerDownHandle} - > - - </span> - </div > - ); - } -}
\ No newline at end of file diff --git a/src/client/views/nodes/formattedText/OrderedListView.tsx b/src/client/views/nodes/formattedText/OrderedListView.tsx new file mode 100644 index 000000000..c3595e59b --- /dev/null +++ b/src/client/views/nodes/formattedText/OrderedListView.tsx @@ -0,0 +1,8 @@ +export class OrderedListView { + + update(node: any) { + // if attr's of an ordered_list (e.g., bulletStyle) change, + // return false forces the dom node to be recreated which is necessary for the bullet labels to update + return false; + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts index 77b93b9d2..0a4c52ef9 100644 --- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts +++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts @@ -7,9 +7,10 @@ import { splitListItem, wrapInList, } from "prosemirror-schema-list"; import { EditorState, Transaction, TextSelection } from "prosemirror-state"; import { SelectionManager } from "../../../util/SelectionManager"; import { NumCast, BoolCast, Cast, StrCast } from "../../../../fields/Types"; -import { Doc } from "../../../../fields/Doc"; +import { Doc, DataSym } from "../../../../fields/Doc"; import { FormattedTextBox } from "./FormattedTextBox"; import { Id } from "../../../../fields/FieldSymbols"; +import { Docs } from "../../../documents/Documents"; const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false; @@ -29,6 +30,7 @@ export let updateBullets = (tx2: Transaction, schema: Schema, mapStyle?: string) }); return tx2; }; + export default function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKeys?: KeyMap): KeyMap { const keys: { [key: string]: any } = {}; @@ -41,77 +43,27 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, props: any keys[key] = cmd; } + //History commands bind("Mod-z", undo); - bind("Shift-Mod-z", redo); bind("Backspace", undoInputRule); - + bind("Shift-Mod-z", redo); !mac && bind("Mod-y", redo); - bind("Alt-ArrowUp", joinUp); - bind("Alt-ArrowDown", joinDown); - bind("Mod-BracketLeft", lift); - bind("Escape", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { - dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from))); - (document.activeElement as any).blur?.(); - SelectionManager.DeselectAll(); - }); - + //Commands to modify Mark bind("Mod-b", toggleMark(schema.marks.strong)); bind("Mod-B", toggleMark(schema.marks.strong)); bind("Mod-e", toggleMark(schema.marks.em)); bind("Mod-E", toggleMark(schema.marks.em)); + bind("Mod-*", toggleMark(schema.marks.code)); + bind("Mod-u", toggleMark(schema.marks.underline)); bind("Mod-U", toggleMark(schema.marks.underline)); - bind("Mod-`", toggleMark(schema.marks.code)); - + //Commands for lists bind("Ctrl-.", wrapInList(schema.nodes.bullet_list)); - - bind("Ctrl-n", wrapInList(schema.nodes.ordered_list)); - - bind("Ctrl->", wrapIn(schema.nodes.blockquote)); - - // bind("^", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { - // let newNode = schema.nodes.footnote.create({}); - // if (dispatch && state.selection.from === state.selection.to) { - // let tr = state.tr; - // tr.replaceSelectionWith(newNode); // replace insertion with a footnote. - // dispatch(tr.setSelection(new NodeSelection( // select the footnote node to open its display - // tr.doc.resolve( // get the location of the footnote node by subtracting the nodesize of the footnote from the current insertion point anchor (which will be immediately after the footnote node) - // tr.selection.anchor - tr.selection.$anchor.nodeBefore!.nodeSize)))); - // return true; - // } - // return false; - // }); - - - const cmd = chainCommands(exitCode, (state, dispatch) => { - if (dispatch) { - dispatch(state.tr.replaceSelectionWith(schema.nodes.hard_break.create()).scrollIntoView()); - return true; - } - return false; - }); - bind("Mod-Enter", cmd); - bind("Shift-Enter", cmd); - mac && bind("Ctrl-Enter", cmd); - - - bind("Shift-Ctrl-0", setBlockType(schema.nodes.paragraph)); - - bind("Shift-Ctrl-\\", setBlockType(schema.nodes.code_block)); - - for (let i = 1; i <= 6; i++) { - bind("Shift-Ctrl-" + i, setBlockType(schema.nodes.heading, { level: i })); - } - - const hr = schema.nodes.horizontal_rule; - bind("Mod-_", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { - dispatch(state.tr.replaceSelectionWith(hr.create()).scrollIntoView()); - return true; - }); + bind("Ctrl-i", wrapInList(schema.nodes.ordered_list)); bind("Tab", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { const ref = state.selection; @@ -148,12 +100,49 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, props: any console.log("bullet demote fail"); } }); - bind("Ctrl-Enter", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { + + //Command to create a new Tab with a PDF of all the command shortcuts + bind("Mod-/", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { + const newDoc = Docs.Create.PdfDocument("http://localhost:1050/assets/cheat-sheet.pdf", { _width: 300, _height: 300 }); + props.addDocTab(newDoc, "onRight"); + }); + + //Commands to modify BlockType + bind("Ctrl->", wrapIn(schema.nodes.blockquote)); + bind("Alt-\\", setBlockType(schema.nodes.paragraph)); + bind("Shift-Ctrl-\\", setBlockType(schema.nodes.code_block)); + + for (let i = 1; i <= 6; i++) { + bind("Shift-Ctrl-" + i, setBlockType(schema.nodes.heading, { level: i })); + } + + //Command to create a horizontal break line + const hr = schema.nodes.horizontal_rule; + bind("Mod-_", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { + dispatch(state.tr.replaceSelectionWith(hr.create()).scrollIntoView()); + return true; + }); + + //Command to unselect all + bind("Escape", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { + dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from))); + (document.activeElement as any).blur?.(); + SelectionManager.DeselectAll(); + }); + + const splitMetadata = (marks: any, tx: Transaction) => { + marks && tx.ensureMarks(marks.filter((val: any) => val.type !== schema.marks.metadata && val.type !== schema.marks.metadataKey && val.type !== schema.marks.metadataVal)); + marks && tx.setStoredMarks(marks.filter((val: any) => val.type !== schema.marks.metadata && val.type !== schema.marks.metadataKey && val.type !== schema.marks.metadataVal)); + return tx; + }; + + const addTextOnRight = (force: boolean) => { const layoutDoc = props.Document; const originalDoc = layoutDoc.rootDocument || layoutDoc; - if (originalDoc instanceof Doc) { + if (force || props.Document._singleLine) { const layoutKey = StrCast(originalDoc.layoutKey); const newDoc = Doc.MakeCopy(originalDoc, true); + newDoc[DataSym][Doc.LayoutFieldKey(newDoc)] = undefined; newDoc.y = NumCast(originalDoc.y) + NumCast(originalDoc._height) + 10; if (layoutKey !== "layout" && originalDoc[layoutKey] instanceof Doc) { newDoc[layoutKey] = originalDoc[layoutKey]; @@ -161,20 +150,24 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, props: any Doc.GetProto(newDoc).text = undefined; FormattedTextBox.SelectOnLoad = newDoc[Id]; props.addDocument(newDoc); + return true; } + return false; + }; + + //Command to create a text document to the right of the selected textbox + bind("Alt-Enter", (state: EditorState<S>, dispatch: (tx: Transaction<Schema<any, any>>) => void) => { + return addTextOnRight(true); }); - const splitMetadata = (marks: any, tx: Transaction) => { - marks && tx.ensureMarks(marks.filter((val: any) => val.type !== schema.marks.metadata && val.type !== schema.marks.metadataKey && val.type !== schema.marks.metadataVal)); - marks && tx.setStoredMarks(marks.filter((val: any) => val.type !== schema.marks.metadata && val.type !== schema.marks.metadataKey && val.type !== schema.marks.metadataVal)); - return tx; - }; - const addTextOnRight = (force: boolean) => { + //Command to create a text document to the bottom of the selected textbox + bind("Ctrl-Enter", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { const layoutDoc = props.Document; const originalDoc = layoutDoc.rootDocument || layoutDoc; - if (force || props.Document._singleLine) { + if (originalDoc instanceof Doc) { const layoutKey = StrCast(originalDoc.layoutKey); const newDoc = Doc.MakeCopy(originalDoc, true); + newDoc[DataSym][Doc.LayoutFieldKey(newDoc)] = undefined; newDoc.x = NumCast(originalDoc.x) + NumCast(originalDoc._width) + 10; if (layoutKey !== "layout" && originalDoc[layoutKey] instanceof Doc) { newDoc[layoutKey] = originalDoc[layoutKey]; @@ -182,13 +175,10 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, props: any Doc.GetProto(newDoc).text = undefined; FormattedTextBox.SelectOnLoad = newDoc[Id]; props.addDocument(newDoc); - return true; } - return false; - }; - bind("Alt-Enter", (state: EditorState<S>, dispatch: (tx: Transaction<Schema<any, any>>) => void) => { - return addTextOnRight(true); }); + + //command to break line bind("Enter", (state: EditorState<S>, dispatch: (tx: Transaction<Schema<any, any>>) => void) => { if (addTextOnRight(false)) return true; const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks()); @@ -204,31 +194,73 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, props: any } return true; }); + + //Command to create a blank space bind("Space", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks()); dispatch(splitMetadata(marks, state.tr)); return false; }); + + bind("Alt-ArrowUp", joinUp); + bind("Alt-ArrowDown", joinDown); + bind("Mod-BracketLeft", lift); + + const cmd = chainCommands(exitCode, (state, dispatch) => { + if (dispatch) { + dispatch(state.tr.replaceSelectionWith(schema.nodes.hard_break.create()).scrollIntoView()); + return true; + } + return false; + }); + + // mac && bind("Ctrl-Enter", cmd); + // bind("Mod-Enter", cmd); + bind("Shift-Enter", cmd); + + bind(":", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { const range = state.selection.$from.blockRange(state.selection.$to, (node: any) => { return !node.marks || !node.marks.find((m: any) => m.type === schema.marks.metadata); }); + const path = (state.doc.resolve(state.selection.from - 1) as any).path; + const spaceSeparator = path[path.length - 3].childCount > 1 ? 0 : -1; + const anchor = range!.end - path[path.length - 3].lastChild.nodeSize + spaceSeparator; + if (anchor >= 0) { + const textsel = TextSelection.create(state.doc, anchor, range!.end); + const text = range ? state.doc.textBetween(textsel.from, textsel.to) : ""; + let whitespace = text.length - 1; + for (; whitespace >= 0 && text[whitespace] !== " "; whitespace--) { } if (text.endsWith(":")) { dispatch(state.tr.addMark(textsel.from + whitespace + 1, textsel.to, schema.marks.metadata.create() as any). addMark(textsel.from + whitespace + 1, textsel.to - 2, schema.marks.metadataKey.create() as any)); } } + return false; }); + // bind("^", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { + // let newNode = schema.nodes.footnote.create({}); + // if (dispatch && state.selection.from === state.selection.to) { + // let tr = state.tr; + // tr.replaceSelectionWith(newNode); // replace insertion with a footnote. + // dispatch(tr.setSelection(new NodeSelection( // select the footnote node to open its display + // tr.doc.resolve( // get the location of the footnote node by subtracting the nodesize of the footnote from the current insertion point anchor (which will be immediately after the footnote node) + // tr.selection.anchor - tr.selection.$anchor.nodeBefore!.nodeSize)))); + // return true; + // } + // return false; + // }); return keys; } + diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index fbd6c87bb..91187edf9 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -30,7 +30,7 @@ export class RichTextRules { // > blockquote wrappingInputRule(/^\s*>\s$/, schema.nodes.blockquote), - // 1. ordered list + // 1. create numerical ordered list wrappingInputRule( /^1\.\s$/, schema.nodes.ordered_list, @@ -42,49 +42,29 @@ export class RichTextRules { }, (type: any) => ({ type: type, attrs: { mapStyle: "decimal", bulletStyle: 1 } }) ), - // a. alphabbetical list + + // A. create alphabetical ordered list wrappingInputRule( - /^a\.\s$/, + /^A\.\s$/, schema.nodes.ordered_list, // match => { () => { - return ({ mapStyle: "alpha", bulletStyle: 1 }); + return ({ mapStyle: "multi", bulletStyle: 1 }); // return ({ order: +match[1] }) }, (match: any, node: any) => { return node.childCount + node.attrs.order === +match[1]; }, - (type: any) => ({ type: type, attrs: { mapStyle: "alpha", bulletStyle: 1 } }) + (type: any) => ({ type: type, attrs: { mapStyle: "multi", bulletStyle: 1 } }) ), - // * bullet list + // * + - create bullet list wrappingInputRule(/^\s*([-+*])\s$/, schema.nodes.bullet_list), - // ``` code block + // ``` create code block textblockTypeInputRule(/^```$/, schema.nodes.code_block), - // create an inline view of a tag stored under the '#' field - new InputRule( - new RegExp(/#([a-zA-Z_\-]+[a-zA-Z_;\-0-9]*)\s$/), - (state, match, start, end) => { - const tag = match[1]; - if (!tag) return state.tr; - const multiple = tag.split(";"); - this.Document[DataSym]["#"] = multiple.length > 1 ? new List(multiple) : tag; - const fieldView = state.schema.nodes.dashField.create({ fieldKey: "#" }); - return state.tr.deleteRange(start, end).insert(start, fieldView); - }), - - // # heading - textblockTypeInputRule( - new RegExp(/^(#{1,6})\s$/), - schema.nodes.heading, - match => { - return ({ level: match[1].length }); - } - ), - - // set the font size using #<font-size> + // %<font-size> set the font size new InputRule( new RegExp(/%([0-9]+)\s$/), (state, match, start, end) => { @@ -92,51 +72,7 @@ export class RichTextRules { return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontSize.create({ fontSize: size })); }), - // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document [[ <fieldKey> : <Doc>]] // [[:Doc]] => hyperlink [[fieldKey]] => show field [[fieldKey:Doc]] => show field of doc - new InputRule( - new RegExp(/\[\[([a-zA-Z_@\? \-0-9]*)(=[a-zA-Z_@\? /\-0-9]*)?(:[a-zA-Z_@\? \-0-9]+)?\]\]$/), - (state, match, start, end) => { - const fieldKey = match[1]; - const docid = match[3]?.substring(1); - const value = match[2]?.substring(1); - if (!fieldKey) { - if (docid) { - DocServer.GetRefField(docid).then(docx => { - const target = ((docx instanceof Doc) && docx) || Docs.Create.FreeformDocument([], { title: docid, _width: 500, _height: 500, _LODdisable: true, }, docid); - DocUtils.Publish(target, docid, returnFalse, returnFalse); - DocUtils.MakeLink({ doc: this.Document }, { doc: target }, "portal to"); - }); - const link = state.schema.marks.link.create({ href: Utils.prepend("/doc/" + docid), location: "onRight", title: docid, targetId: docid }); - return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 2).addMark(start, end - 3, link); - } - return state.tr; - } - if (value !== "" && value !== undefined) { - const num = value.match(/^[0-9.]$/); - this.Document[DataSym][fieldKey] = value === "true" ? true : value === "false" ? false : (num ? Number(value) : value); - } - const fieldView = state.schema.nodes.dashField.create({ fieldKey, docid }); - return state.tr.deleteRange(start, end).insert(start, fieldView); - }), - // create an inline view of a document {{ <layoutKey> : <Doc> }} // {{:Doc}} => show default view of document {{<layout>}} => show layout for this doc {{<layout> : Doc}} => show layout for another doc - new InputRule( - new RegExp(/\{\{([a-zA-Z_ \-0-9]*)(\([a-zA-Z0-9…._/\-]*\))?(:[a-zA-Z_ \-0-9]+)?\}\}$/), - (state, match, start, end) => { - const fieldKey = match[1] || ""; - const fieldParam = match[2]?.replace("…", "...") || ""; - const docid = match[3]?.substring(1); - if (!fieldKey && !docid) return state.tr; - docid && DocServer.GetRefField(docid).then(docx => { - if (!(docx instanceof Doc && docx)) { - const docx = Docs.Create.FreeformDocument([], { title: docid, _width: 500, _height: 500, _LODdisable: true }, docid); - DocUtils.Publish(docx, docid, returnFalse, returnFalse); - } - }); - const node = (state.doc.resolve(start) as any).nodeAfter; - const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 75, title: "dashDoc", docid, fieldKey: fieldKey + fieldParam, float: "unset", alias: Utils.GenerateGuid() }); - const sm = state.storedMarks || undefined; - return node ? state.tr.replaceRangeWith(start, end, dashDoc).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr; - }), + //Create annotation to a field on the text document new InputRule( new RegExp(/>>$/), (state, match, start, end) => { @@ -161,25 +97,7 @@ export class RichTextRules { state.tr; return replaced; }), - // stop using active style - new InputRule( - new RegExp(/%%$/), - (state, match, start, end) => { - const tr = state.tr.deleteRange(start, end); - const marks = state.tr.selection.$anchor.nodeBefore?.marks; - return marks ? Array.from(marks).filter(m => m !== state.schema.marks.user_mark).reduce((tr, m) => tr.removeStoredMark(m), tr) : tr; - }), - // set the Todo user-tag on the current selection (assumes % was used to initiate an EnteringStyle mode) - new InputRule( - new RegExp(/[ti!x]$/), - (state, match, start, end) => { - if (state.selection.to === state.selection.from || !this.EnteringStyle) return null; - const tag = match[0] === "t" ? "todo" : match[0] === "i" ? "ignore" : match[0] === "x" ? "disagree" : match[0] === "!" ? "important" : "??"; - const node = (state.doc.resolve(start) as any).nodeAfter; - if (node?.marks.findIndex((m: any) => m.type === schema.marks.user_tag) !== -1) return state.tr.removeMark(start, end, schema.marks.user_tag); - return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: tag, modified: Math.round(Date.now() / 1000 / 60) })) : state.tr; - }), // set the First-line indent node type for the selection's paragraph (assumes % was used to initiate an EnteringStyle mode) new InputRule( @@ -214,6 +132,7 @@ export class RichTextRules { } return null; }), + // set the Quoted indent node type for the current selection's paragraph (assumes % was used to initiate an EnteringStyle mode) new InputRule( new RegExp(/(%q|q)$/), @@ -235,56 +154,56 @@ export class RichTextRules { return null; }), - // center justify text new InputRule( - new RegExp(/%\^$/), + new RegExp(/%\^/), (state, match, start, end) => { - const node = (state.doc.resolve(start) as any).nodeAfter; - const sm = state.storedMarks || undefined; - const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "center" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : - state.tr; - return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2))); + const resolved = state.doc.resolve(start) as any; + if (resolved?.parent.type.name === "paragraph") { + return state.tr.deleteRange(start, end).setNodeMarkup(resolved.path[resolved.path.length - 4], schema.nodes.paragraph, { ...resolved.parent.attrs, align: "center" }, resolved.parent.marks); + } else { + const node = resolved.nodeAfter; + const sm = state.storedMarks || undefined; + const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "center" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : + state.tr; + return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2))); + } }), + // left justify text new InputRule( - new RegExp(/%\[$/), + new RegExp(/%\[/), (state, match, start, end) => { - const node = (state.doc.resolve(start) as any).nodeAfter; - const sm = state.storedMarks || undefined; - const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "left" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : - state.tr; - return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2))); + const resolved = state.doc.resolve(start) as any; + if (resolved?.parent.type.name === "paragraph") { + return state.tr.deleteRange(start, end).setNodeMarkup(resolved.path[resolved.path.length - 4], schema.nodes.paragraph, { ...resolved.parent.attrs, align: "left" }, resolved.parent.marks); + } else { + const node = resolved.nodeAfter; + const sm = state.storedMarks || undefined; + const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "left" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : + state.tr; + return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2))); + } }), + // right justify text new InputRule( - new RegExp(/%\]$/), - (state, match, start, end) => { - const node = (state.doc.resolve(start) as any).nodeAfter; - const sm = state.storedMarks || undefined; - const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "right" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : - state.tr; - return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2))); - }), - new InputRule( - new RegExp(/%\(/), - (state, match, start, end) => { - const node = (state.doc.resolve(start) as any).nodeAfter; - const sm = state.storedMarks || []; - const mark = state.schema.marks.summarizeInclusive.create(); - sm.push(mark); - const selected = state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).addMark(start, end, mark); - const content = selected.selection.content(); - const replaced = node ? selected.replaceRangeWith(start, end, - schema.nodes.summary.create({ visibility: true, text: content, textslice: content.toJSON() })) : - state.tr; - return replaced.setSelection(new TextSelection(replaced.doc.resolve(end + 1))).setStoredMarks([...node.marks, ...sm]); - }), - new InputRule( - new RegExp(/%\)/), + new RegExp(/%\]/), (state, match, start, end) => { - return state.tr.deleteRange(start, end).removeStoredMark(state.schema.marks.summarizeInclusive.create()); + const resolved = state.doc.resolve(start) as any; + if (resolved?.parent.type.name === "paragraph") { + return state.tr.deleteRange(start, end).setNodeMarkup(resolved.path[resolved.path.length - 4], schema.nodes.paragraph, { ...resolved.parent.attrs, align: "right" }, resolved.parent.marks); + } else { + const node = resolved.nodeAfter; + const sm = state.storedMarks || undefined; + const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "right" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : + state.tr; + return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2))); + } }), + + + // %f create footnote new InputRule( new RegExp(/%f$/), (state, match, start, end) => { @@ -296,26 +215,160 @@ export class RichTextRules { tr.selection.anchor - tr.selection.$anchor.nodeBefore!.nodeSize))); }), - // activate a style by name using prefix '%' + // activate a style by name using prefix '%<color name>' new InputRule( new RegExp(/%[a-z]+$/), (state, match, start, end) => { + const color = match[0].substring(1, match[0].length); const marks = RichTextMenu.Instance._brushMap.get(color); + if (marks) { const tr = state.tr.deleteRange(start, end); return marks ? Array.from(marks).reduce((tr, m) => tr.addStoredMark(m), tr) : tr; } + const isValidColor = (strColor: string) => { const s = new Option().style; s.color = strColor; return s.color === strColor.toLowerCase(); // 'false' if color wasn't assigned }; + if (isValidColor(color)) { return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontColor.create({ color: color })); } + return null; }), + + // stop using active style + new InputRule( + new RegExp(/%%$/), + (state, match, start, end) => { + + const tr = state.tr.deleteRange(start, end); + const marks = state.tr.selection.$anchor.nodeBefore?.marks; + + return marks ? Array.from(marks).filter(m => m !== state.schema.marks.user_mark).reduce((tr, m) => tr.removeStoredMark(m), tr) : tr; + }), + + // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document + // [[ <fieldKey> : <Doc>]] + // [[:Doc]] => hyperlink + // [[fieldKey]] => show field + // [[fieldKey:Doc]] => show field of doc + new InputRule( + new RegExp(/\[\[([a-zA-Z_@\? \-0-9]*)(=[a-zA-Z_@\? /\-0-9]*)?(:[a-zA-Z_@\? \-0-9]+)?\]\]$/), + (state, match, start, end) => { + const fieldKey = match[1]; + const docid = match[3]?.substring(1); + const value = match[2]?.substring(1); + if (!fieldKey) { + if (docid) { + DocServer.GetRefField(docid).then(docx => { + const target = ((docx instanceof Doc) && docx) || Docs.Create.FreeformDocument([], { title: docid, _width: 500, _height: 500, _LODdisable: true, }, docid); + DocUtils.Publish(target, docid, returnFalse, returnFalse); + DocUtils.MakeLink({ doc: this.Document }, { doc: target }, "portal to"); + }); + const link = state.schema.marks.link.create({ href: Utils.prepend("/doc/" + docid), location: "onRight", title: docid, targetId: docid }); + return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 2).addMark(start, end - 3, link); + } + return state.tr; + } + if (value !== "" && value !== undefined) { + const num = value.match(/^[0-9.]$/); + this.Document[DataSym][fieldKey] = value === "true" ? true : value === "false" ? false : (num ? Number(value) : value); + } + const fieldView = state.schema.nodes.dashField.create({ fieldKey, docid }); + return state.tr.deleteRange(start, end).insert(start, fieldView); + }), + + // create an inline view of a document {{ <layoutKey> : <Doc> }} + // {{:Doc}} => show default view of document + // {{<layout>}} => show layout for this doc + // {{<layout> : Doc}} => show layout for another doc + new InputRule( + new RegExp(/\{\{([a-zA-Z_ \-0-9]*)(\([a-zA-Z0-9…._/\-]*\))?(:[a-zA-Z_ \-0-9]+)?\}\}$/), + (state, match, start, end) => { + const fieldKey = match[1] || ""; + const fieldParam = match[2]?.replace("…", "...") || ""; + const docid = match[3]?.substring(1); + if (!fieldKey && !docid) return state.tr; + docid && DocServer.GetRefField(docid).then(docx => { + if (!(docx instanceof Doc && docx)) { + const docx = Docs.Create.FreeformDocument([], { title: docid, _width: 500, _height: 500, _LODdisable: true }, docid); + DocUtils.Publish(docx, docid, returnFalse, returnFalse); + } + }); + const node = (state.doc.resolve(start) as any).nodeAfter; + const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 75, title: "dashDoc", docid, fieldKey: fieldKey + fieldParam, float: "unset", alias: Utils.GenerateGuid() }); + const sm = state.storedMarks || undefined; + return node ? state.tr.replaceRangeWith(start, end, dashDoc).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr; + }), + + + + // create an inline view of a tag stored under the '#' field + new InputRule( + new RegExp(/#([a-zA-Z_\-]+[a-zA-Z_;\-0-9]*)\s$/), + (state, match, start, end) => { + const tag = match[1]; + if (!tag) return state.tr; + const multiple = tag.split(";"); + this.Document[DataSym]["#"] = multiple.length > 1 ? new List(multiple) : tag; + const fieldView = state.schema.nodes.dashField.create({ fieldKey: "#" }); + return state.tr.deleteRange(start, end).insert(start, fieldView); + }), + + + // # heading + textblockTypeInputRule( + new RegExp(/^(#{1,6})\s$/), + schema.nodes.heading, + match => { + return ({ level: match[1].length }); + } + ), + + // set the Todo user-tag on the current selection (assumes % was used to initiate an EnteringStyle mode) + new InputRule( + new RegExp(/[ti!x]$/), + (state, match, start, end) => { + + if (state.selection.to === state.selection.from || !this.EnteringStyle) return null; + + const tag = match[0] === "t" ? "todo" : match[0] === "i" ? "ignore" : match[0] === "x" ? "disagree" : match[0] === "!" ? "important" : "??"; + const node = (state.doc.resolve(start) as any).nodeAfter; + + if (node?.marks.findIndex((m: any) => m.type === schema.marks.user_tag) !== -1) return state.tr.removeMark(start, end, schema.marks.user_tag); + + return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: tag, modified: Math.round(Date.now() / 1000 / 60) })) : state.tr; + }), + + new InputRule( + new RegExp(/%\(/), + (state, match, start, end) => { + const node = (state.doc.resolve(start) as any).nodeAfter; + const sm = state.storedMarks || []; + const mark = state.schema.marks.summarizeInclusive.create(); + + sm.push(mark); + const selected = state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).addMark(start, end, mark); + const content = selected.selection.content(); + const replaced = node ? selected.replaceRangeWith(start, end, + schema.nodes.summary.create({ visibility: true, text: content, textslice: content.toJSON() })) : + state.tr; + + return replaced.setSelection(new TextSelection(replaced.doc.resolve(end + 1))).setStoredMarks([...node.marks, ...sm]); + }), + + new InputRule( + new RegExp(/%\)/), + (state, match, start, end) => { + + return state.tr.deleteRange(start, end).removeStoredMark(state.schema.marks.summarizeInclusive.create()); + }), + ] }; } diff --git a/src/client/views/nodes/formattedText/RichTextSchema.tsx b/src/client/views/nodes/formattedText/RichTextSchema.tsx index 1cc7ec8bf..05557e22a 100644 --- a/src/client/views/nodes/formattedText/RichTextSchema.tsx +++ b/src/client/views/nodes/formattedText/RichTextSchema.tsx @@ -1,188 +1,19 @@ -import { IReactionDisposer, observable, reaction, runInAction } from "mobx"; -import { baseKeymap, toggleMark } from "prosemirror-commands"; -import { redo, undo } from "prosemirror-history"; -import { keymap } from "prosemirror-keymap"; -import { DOMOutputSpecArray, Fragment, MarkSpec, Node, NodeSpec, Schema, Slice } from "prosemirror-model"; -import { bulletList, listItem, orderedList } from 'prosemirror-schema-list'; -import { EditorState, NodeSelection, Plugin, TextSelection } from "prosemirror-state"; -import { StepMap } from "prosemirror-transform"; -import { EditorView } from "prosemirror-view"; +import { IReactionDisposer, reaction } from "mobx"; +import { NodeSelection } from "prosemirror-state"; import * as ReactDOM from 'react-dom'; -import { Doc, DocListCast, Field, HeightSym, WidthSym } from "../../../../fields/Doc"; +import { Doc, HeightSym, WidthSym } from "../../../../fields/Doc"; import { Id } from "../../../../fields/FieldSymbols"; -import { List } from "../../../../fields/List"; import { ObjectField } from "../../../../fields/ObjectField"; -import { listSpec } from "../../../../fields/Schema"; -import { SchemaHeaderField } from "../../../../fields/SchemaHeaderField"; import { ComputedField } from "../../../../fields/ScriptField"; -import { BoolCast, Cast, NumCast, StrCast, FieldValue } from "../../../../fields/Types"; -import { emptyFunction, returnEmptyString, returnFalse, returnOne, Utils, returnZero } from "../../../../Utils"; +import { BoolCast, Cast, NumCast, StrCast } from "../../../../fields/Types"; +import { emptyFunction, returnEmptyString, returnFalse, returnZero, Utils } from "../../../../Utils"; import { DocServer } from "../../../DocServer"; import { Docs, DocUtils } from "../../../documents/Documents"; -import { CollectionViewType } from "../../collections/CollectionView"; +import { Transform } from "../../../util/Transform"; import { DocumentView } from "../DocumentView"; import { FormattedTextBox } from "./FormattedTextBox"; -import { DocumentManager } from "../../../util/DocumentManager"; -import { Transform } from "../../../util/Transform"; import React = require("react"); -import { schema } from "./schema_rts"; - -export class OrderedListView { - update(node: any) { - return false; // if attr's of an ordered_list (e.g., bulletStyle) change, return false forces the dom node to be recreated which is necessary for the bullet labels to update - } -} - -export class ImageResizeView { - _handle: HTMLElement; - _img: HTMLElement; - _outer: HTMLElement; - constructor(node: any, view: any, getPos: any, addDocTab: any) { - //moved - this._handle = document.createElement("span"); - this._img = document.createElement("img"); - this._outer = document.createElement("span"); - this._outer.style.position = "relative"; - this._outer.style.width = node.attrs.width; - this._outer.style.height = node.attrs.height; - this._outer.style.display = "inline-block"; - this._outer.style.overflow = "hidden"; - (this._outer.style as any).float = node.attrs.float; - //moved - this._img.setAttribute("src", node.attrs.src); - this._img.style.width = "100%"; - this._handle.style.position = "absolute"; - this._handle.style.width = "20px"; - this._handle.style.height = "20px"; - this._handle.style.backgroundColor = "blue"; - this._handle.style.borderRadius = "15px"; - this._handle.style.display = "none"; - this._handle.style.bottom = "-10px"; - this._handle.style.right = "-10px"; - const self = this; - //moved - this._img.onclick = function (e: any) { - e.stopPropagation(); - e.preventDefault(); - if (view.state.selection.node && view.state.selection.node.type !== view.state.schema.nodes.image) { - view.dispatch(view.state.tr.setSelection(new NodeSelection(view.state.doc.resolve(view.state.selection.from - 2)))); - } - }; - //moved - this._img.onpointerdown = function (e: any) { - if (e.ctrlKey) { - e.preventDefault(); - e.stopPropagation(); - DocServer.GetRefField(node.attrs.docid).then(async linkDoc => - (linkDoc instanceof Doc) && - DocumentManager.Instance.FollowLink(linkDoc, view.state.schema.Document, - document => addDocTab(document, node.attrs.location ? node.attrs.location : "inTab"), false)); - } - }; - //moved - this._handle.onpointerdown = function (e: any) { - e.preventDefault(); - e.stopPropagation(); - const wid = Number(getComputedStyle(self._img).width.replace(/px/, "")); - const hgt = Number(getComputedStyle(self._img).height.replace(/px/, "")); - const startX = e.pageX; - const startWidth = parseFloat(node.attrs.width); - const onpointermove = (e: any) => { - const currentX = e.pageX; - const diffInPx = currentX - startX; - self._outer.style.width = `${startWidth + diffInPx}`; - self._outer.style.height = `${(startWidth + diffInPx) * hgt / wid}`; - }; - - const onpointerup = () => { - document.removeEventListener("pointermove", onpointermove); - document.removeEventListener("pointerup", onpointerup); - const pos = view.state.selection.from; - view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { ...node.attrs, width: self._outer.style.width, height: self._outer.style.height })); - view.dispatch(view.state.tr.setSelection(new NodeSelection(view.state.doc.resolve(pos)))); - }; - - document.addEventListener("pointermove", onpointermove); - document.addEventListener("pointerup", onpointerup); - }; - //Moved - this._outer.appendChild(this._img); - this._outer.appendChild(this._handle); - (this as any).dom = this._outer; - } - - selectNode() { - this._img.classList.add("ProseMirror-selectednode"); - - this._handle.style.display = ""; - } - - deselectNode() { - this._img.classList.remove("ProseMirror-selectednode"); - - this._handle.style.display = "none"; - } -} - -export class DashDocCommentView { - _collapsed: HTMLElement; - _view: any; - constructor(node: any, view: any, getPos: any) { - //moved - this._collapsed = document.createElement("span"); - this._collapsed.className = "formattedTextBox-inlineComment"; - this._collapsed.id = "DashDocCommentView-" + node.attrs.docid; - this._view = view; - //moved - const targetNode = () => { // search forward in the prosemirror doc for the attached dashDocNode that is the target of the comment anchor - for (let i = getPos() + 1; i < view.state.doc.content.size; i++) { - const m = view.state.doc.nodeAt(i); - if (m && m.type === view.state.schema.nodes.dashDoc && m.attrs.docid === node.attrs.docid) { - return { node: m, pos: i, hidden: m.attrs.hidden } as { node: any, pos: number, hidden: boolean }; - } - } - const dashDoc = view.state.schema.nodes.dashDoc.create({ width: 75, height: 35, title: "dashDoc", docid: node.attrs.docid, float: "right" }); - view.dispatch(view.state.tr.insert(getPos() + 1, dashDoc)); - setTimeout(() => { try { view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.tr.doc, getPos() + 2))); } catch (e) { } }, 0); - return undefined; - }; - //moved - this._collapsed.onpointerdown = (e: any) => { - e.stopPropagation(); - }; - //moved - this._collapsed.onpointerup = (e: any) => { - const target = targetNode(); - if (target) { - const expand = target.hidden; - const tr = view.state.tr.setNodeMarkup(target.pos, undefined, { ...target.node.attrs, hidden: target.node.attrs.hidden ? false : true }); - view.dispatch(tr.setSelection(TextSelection.create(tr.doc, getPos() + (expand ? 2 : 1)))); // update the attrs - setTimeout(() => { - expand && DocServer.GetRefField(node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc)); - try { view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.tr.doc, getPos() + (expand ? 2 : 1)))); } catch (e) { } - }, 0); - } - e.stopPropagation(); - }; - //moved - this._collapsed.onpointerenter = (e: any) => { - DocServer.GetRefField(node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc, false)); - e.preventDefault(); - e.stopPropagation(); - }; - //moved - this._collapsed.onpointerleave = (e: any) => { - DocServer.GetRefField(node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowUnhighlight()); - e.preventDefault(); - e.stopPropagation(); - }; - - (this as any).dom = this._collapsed; - } - //moved - selectNode() { } -} export class DashDocView { _dashSpan: HTMLDivElement; @@ -192,16 +23,22 @@ export class DashDocView { _renderDisposer: IReactionDisposer | undefined; _textBox: FormattedTextBox; + //moved getDocTransform = () => { const { scale, translateX, translateY } = Utils.GetScreenTransform(this._outer); return new Transform(-translateX, -translateY, 1).scale(1 / this.contentScaling() / scale); } + + //moved contentScaling = () => NumCast(this._dashDoc!._nativeWidth) > 0 ? this._dashDoc![WidthSym]() / NumCast(this._dashDoc!._nativeWidth) : 1; + //moved outerFocus = (target: Doc) => this._textBox.props.focus(this._textBox.props.Document); // ideally, this would scroll to show the focus target constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) { + //moved this._textBox = tbox; + this._dashSpan = document.createElement("div"); this._outer = document.createElement("span"); this._outer.style.position = "relative"; @@ -218,28 +55,35 @@ export class DashDocView { this._dashSpan.style.position = "absolute"; this._dashSpan.style.display = "inline-block"; this._dashSpan.style.whiteSpace = "normal"; + this._dashSpan.onpointerleave = () => { const ele = document.getElementById("DashDocCommentView-" + node.attrs.docid); if (ele) { (ele as HTMLDivElement).style.backgroundColor = ""; } }; + this._dashSpan.onpointerenter = () => { const ele = document.getElementById("DashDocCommentView-" + node.attrs.docid); if (ele) { (ele as HTMLDivElement).style.backgroundColor = "orange"; } }; + const removeDoc = () => { + console.log("DashDocView.removeDoc"); // SMM const pos = getPos(); const ns = new NodeSelection(view.state.doc.resolve(pos)); view.dispatch(view.state.tr.setSelection(ns).deleteSelection()); return true; }; - const alias = node.attrs.alias; + const alias = node.attrs.alias; + const self = this; const docid = node.attrs.docid || tbox.props.Document[Id];// tbox.props.DataDoc?.[Id] || tbox.dataDoc?.[Id]; + DocServer.GetRefField(docid + alias).then(async dashDoc => { + if (!(dashDoc instanceof Doc)) { alias && DocServer.GetRefField(docid).then(async dashDocBase => { if (dashDocBase instanceof Doc) { @@ -253,7 +97,8 @@ export class DashDocView { self.doRender(dashDoc, removeDoc, node, view, getPos); } }); - const self = this; + + this._dashSpan.onkeydown = function (e: any) { e.stopPropagation(); if (e.key === "Tab" || e.key === "Enter") { @@ -281,6 +126,7 @@ export class DashDocView { this._dashSpan.style.height = this._outer.style.height = Math.max(20, dim[1]) + "px"; this._outer.style.border = "1px solid " + StrCast(finalLayout.color, (Cast(Doc.UserDoc().activeWorkspace, Doc, null).darkScheme ? "dimGray" : "lightGray")); }, { fireImmediately: true }); + const doReactRender = (finalLayout: Doc, resolvedDataDoc: Doc) => { ReactDOM.unmountComponentAtNode(this._dashSpan); @@ -310,6 +156,7 @@ export class DashDocView { ContainingCollectionDoc={this._textBox.props.ContainingCollectionDoc} ContentScaling={this.contentScaling} />, this._dashSpan); + if (node.attrs.width !== dashDoc._width + "px" || node.attrs.height !== dashDoc._height + "px") { try { // bcz: an exception will be thrown if two aliases are open at the same time when a doc view comment is made view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { ...node.attrs, width: dashDoc._width + "px", height: dashDoc._height + "px" })); @@ -318,6 +165,7 @@ export class DashDocView { } } }; + this._renderDisposer?.(); this._renderDisposer = reaction(() => { // if (!Doc.AreProtosEqual(finalLayout, dashDoc)) { @@ -337,202 +185,9 @@ export class DashDocView { { fireImmediately: true }); } } + destroy() { ReactDOM.unmountComponentAtNode(this._dashSpan); this._reactionDisposer?.(); } } - -export class FootnoteView { - innerView: any; - outerView: any; - node: any; - dom: any; - getPos: any; - - constructor(node: any, view: any, getPos: any) { - // We'll need these later - this.node = node; - this.outerView = view; - this.getPos = getPos; - - // The node's representation in the editor (empty, for now) - this.dom = document.createElement("footnote"); - this.dom.addEventListener("pointerup", this.toggle, true); - // These are used when the footnote is selected - this.innerView = null; - } - selectNode() { - const attrs = { ...this.node.attrs }; - attrs.visibility = true; - this.dom.classList.add("ProseMirror-selectednode"); - if (!this.innerView) this.open(); - } - - deselectNode() { - const attrs = { ...this.node.attrs }; - attrs.visibility = false; - this.dom.classList.remove("ProseMirror-selectednode"); - if (this.innerView) this.close(); - } - open() { - // Append a tooltip to the outer node - const tooltip = this.dom.appendChild(document.createElement("div")); - tooltip.className = "footnote-tooltip"; - // And put a sub-ProseMirror into that - this.innerView = new EditorView(tooltip, { - // You can use any node as an editor document - state: EditorState.create({ - doc: this.node, - plugins: [keymap(baseKeymap), - keymap({ - "Mod-z": () => undo(this.outerView.state, this.outerView.dispatch), - "Mod-y": () => redo(this.outerView.state, this.outerView.dispatch), - "Mod-b": toggleMark(schema.marks.strong) - }), - // new Plugin({ - // view(newView) { - // // TODO -- make this work with RichTextMenu - // // return FormattedTextBox.getToolTip(newView); - // } - // }) - ], - - }), - // This is the magic part - dispatchTransaction: this.dispatchInner.bind(this), - handleDOMEvents: { - pointerdown: ((view: any, e: PointerEvent) => { - // Kludge to prevent issues due to the fact that the whole - // footnote is node-selected (and thus DOM-selected) when - // the parent editor is focused. - e.stopPropagation(); - document.addEventListener("pointerup", this.ignore, true); - if (this.outerView.hasFocus()) this.innerView.focus(); - }) as any - } - - }); - setTimeout(() => this.innerView && this.innerView.docView.setSelection(0, 0, this.innerView.root, true), 0); - } - - ignore = (e: PointerEvent) => { - e.stopPropagation(); - document.removeEventListener("pointerup", this.ignore, true); - } - - toggle = () => { - if (this.innerView) this.close(); - else { - this.open(); - } - } - close() { - this.innerView && this.innerView.destroy(); - this.innerView = null; - this.dom.textContent = ""; - } - - dispatchInner(tr: any) { - const { state, transactions } = this.innerView.state.applyTransaction(tr); - this.innerView.updateState(state); - - if (!tr.getMeta("fromOutside")) { - const outerTr = this.outerView.state.tr, offsetMap = StepMap.offset(this.getPos() + 1); - for (const transaction of transactions) { - const steps = transaction.steps; - for (const step of steps) { - outerTr.step(step.map(offsetMap)); - } - } - if (outerTr.docChanged) this.outerView.dispatch(outerTr); - } - } - update(node: any) { - if (!node.sameMarkup(this.node)) return false; - this.node = node; - if (this.innerView) { - const state = this.innerView.state; - const start = node.content.findDiffStart(state.doc.content); - if (start !== null) { - let { a: endA, b: endB } = node.content.findDiffEnd(state.doc.content); - const overlap = start - Math.min(endA, endB); - if (overlap > 0) { endA += overlap; endB += overlap; } - this.innerView.dispatch( - state.tr - .replace(start, endB, node.slice(start, endA)) - .setMeta("fromOutside", true)); - } - } - return true; - } - - destroy() { - if (this.innerView) this.close(); - } - - stopEvent(event: any) { - return this.innerView && this.innerView.dom.contains(event.target); - } - - ignoreMutation() { return true; } -} - -export class SummaryView { - _collapsed: HTMLElement; - _view: any; - constructor(node: any, view: any, getPos: any) { - this._collapsed = document.createElement("span"); - this._collapsed.className = this.className(node.attrs.visibility); - this._view = view; - const js = node.toJSON; - node.toJSON = function () { - return js.apply(this, arguments); - }; - - this._collapsed.onpointerdown = (e: any) => { - const visible = !node.attrs.visibility; - const attrs = { ...node.attrs, visibility: visible }; - let textSelection = TextSelection.create(view.state.doc, getPos() + 1); - if (!visible) { // update summarized text and save in attrs - textSelection = this.updateSummarizedText(getPos() + 1); - attrs.text = textSelection.content(); - attrs.textslice = attrs.text.toJSON(); - } - view.dispatch(view.state.tr. - setSelection(textSelection). // select the current summarized text (or where it will be if its collapsed) - replaceSelection(!visible ? new Slice(Fragment.fromArray([]), 0, 0) : node.attrs.text). // collapse/expand it - setNodeMarkup(getPos(), undefined, attrs)); // update the attrs - e.preventDefault(); - e.stopPropagation(); - this._collapsed.className = this.className(visible); - }; - (this as any).dom = this._collapsed; - } - selectNode() { } - - deselectNode() { } - - className = (visible: boolean) => "formattedTextBox-summarizer" + (visible ? "" : "-collapsed"); - - updateSummarizedText(start?: any) { - const mtype = this._view.state.schema.marks.summarize; - const mtypeInc = this._view.state.schema.marks.summarizeInclusive; - let endPos = start; - - const visited = new Set(); - for (let i: number = start + 1; i < this._view.state.doc.nodeSize - 1; i++) { - let skip = false; - this._view.state.doc.nodesBetween(start, i, (node: Node, pos: number, parent: Node, index: number) => { - if (node.isLeaf && !visited.has(node) && !skip) { - if (node.marks.find((m: any) => m.type === mtype || m.type === mtypeInc)) { - visited.add(node); - endPos = i + node.nodeSize - 1; - } - else skip = true; - } - }); - } - return TextSelection.create(this._view.state.doc, start, endPos); - } -}
\ No newline at end of file diff --git a/src/client/views/nodes/formattedText/SummaryView.tsx b/src/client/views/nodes/formattedText/SummaryView.tsx index 89908d8ee..922285bd0 100644 --- a/src/client/views/nodes/formattedText/SummaryView.tsx +++ b/src/client/views/nodes/formattedText/SummaryView.tsx @@ -1,81 +1,81 @@ import { TextSelection } from "prosemirror-state"; import { Fragment, Node, Slice } from "prosemirror-model"; - +import * as ReactDOM from 'react-dom'; import React = require("react"); -interface ISummaryView { - node: any; - view: any; - getPos: any; - self: any; -} -export class SummaryView extends React.Component<ISummaryView> { +// an elidable textblock that collapses when its '<-' is clicked and expands when its '...' anchor is clicked. +// this node actively edits prosemirror (as opposed to just changing how things are rendered) and thus doesn't +// really need a react view. However, it would be cleaner to figure out how to do this just as a react rendering +// method instead of changing prosemirror's text when the expand/elide buttons are clicked. +export class SummaryView { + _fieldWrapper: HTMLSpanElement; // container for label and value - onPointerDown = (e: any) => { - const visible = !this.props.node.attrs.visibility; - const attrs = { ...this.props.node.attrs, visibility: visible }; - let textSelection = TextSelection.create(this.props.view.state.doc, this.props.getPos() + 1); - if (!visible) { // update summarized text and save in attrs - textSelection = this.updateSummarizedText(this.props.getPos() + 1); - attrs.text = textSelection.content(); - attrs.textslice = attrs.text.toJSON(); - } - this.props.view.dispatch(this.props.view.state.tr. - setSelection(textSelection). // select the current summarized text (or where it will be if its collapsed) - replaceSelection(!visible ? new Slice(Fragment.fromArray([]), 0, 0) : this.props.node.attrs.text). // collapse/expand it - setNodeMarkup(this.props.getPos(), undefined, attrs)); // update the attrs - e.preventDefault(); - e.stopPropagation(); - const _collapsed = document.getElementById('collapse') as HTMLElement; - _collapsed.className = this.className(visible); + constructor(node: any, view: any, getPos: any) { + const self = this; + this._fieldWrapper = document.createElement("span"); + this._fieldWrapper.className = this.className(node.attrs.visibility); + this._fieldWrapper.onpointerdown = function (e: any) { self.onPointerDown(e, node, view, getPos); } + this._fieldWrapper.onkeypress = function (e: any) { e.stopPropagation(); }; + this._fieldWrapper.onkeydown = function (e: any) { e.stopPropagation(); }; + this._fieldWrapper.onkeyup = function (e: any) { e.stopPropagation(); }; + this._fieldWrapper.onmousedown = function (e: any) { e.stopPropagation(); }; + + const js = node.toJSON; + node.toJSON = function () { return js.apply(this, arguments); }; + + ReactDOM.render(<SummaryViewInternal />, this._fieldWrapper); + (this as any).dom = this._fieldWrapper; } - updateSummarizedText(start?: any) { - const mtype = this.props.view.state.schema.marks.summarize; - const mtypeInc = this.props.view.state.schema.marks.summarizeInclusive; + className = (visible: boolean) => "formattedTextBox-summarizer" + (visible ? "" : "-collapsed"); + destroy() { ReactDOM.unmountComponentAtNode(this._fieldWrapper); } + selectNode() { } + + updateSummarizedText(start: any, view: any) { + const mtype = view.state.schema.marks.summarize; + const mtypeInc = view.state.schema.marks.summarizeInclusive; let endPos = start; const visited = new Set(); - for (let i: number = start + 1; i < this.props.view.state.doc.nodeSize - 1; i++) { + for (let i: number = start + 1; i < view.state.doc.nodeSize - 1; i++) { let skip = false; - this.props.view.state.doc.nodesBetween(start, i, (node: Node, pos: number, parent: Node, index: number) => { - if (this.props.node.isLeaf && !visited.has(node) && !skip) { - if (this.props.node.marks.find((m: any) => m.type === mtype || m.type === mtypeInc)) { + view.state.doc.nodesBetween(start, i, (node: Node, pos: number, parent: Node, index: number) => { + if (node.isLeaf && !visited.has(node) && !skip) { + if (node.marks.find((m: any) => m.type === mtype || m.type === mtypeInc)) { visited.add(node); - endPos = i + this.props.node.nodeSize - 1; + endPos = i + node.nodeSize - 1; } else skip = true; } }); } - return TextSelection.create(this.props.view.state.doc, start, endPos); + return TextSelection.create(view.state.doc, start, endPos); } - className = (visible: boolean) => "formattedTextBox-summarizer" + (visible ? "" : "-collapsed"); - - selectNode() { } - - deselectNode() { } + onPointerDown = (e: any, node: any, view: any, getPos: any) => { + const visible = !node.attrs.visibility; + const attrs = { ...node.attrs, visibility: visible }; + let textSelection = TextSelection.create(view.state.doc, getPos() + 1); + if (!visible) { // update summarized text and save in attrs + textSelection = this.updateSummarizedText(getPos() + 1, view); + attrs.text = textSelection.content(); + attrs.textslice = attrs.text.toJSON(); + } + view.dispatch(view.state.tr. + setSelection(textSelection). // select the current summarized text (or where it will be if its collapsed) + replaceSelection(!visible ? new Slice(Fragment.fromArray([]), 0, 0) : node.attrs.text). // collapse/expand it + setNodeMarkup(getPos(), undefined, attrs)); // update the attrs + e.preventDefault(); + e.stopPropagation(); + this._fieldWrapper.className = this.className(visible); + } +} +interface ISummaryView { +} +// currently nothing needs to be rendered for the internal view of a summary. +export class SummaryViewInternal extends React.Component<ISummaryView> { render() { - const _view = this.props.node.view; - const js = this.props.node.toJSon; - - this.props.node.toJSON = function () { - return js.apply(this, arguments); - }; - - const spanCollapsedClassName = this.className(this.props.node.attrs.visibility); - - return ( - <span - className={spanCollapsedClassName} - id='collapse' - onPointerDown={this.onPointerDown} - > - - </span> - ); - + return <> </>; } }
\ No newline at end of file diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts index c735155d8..49d5c96a4 100644 --- a/src/client/views/nodes/formattedText/marks_rts.ts +++ b/src/client/views/nodes/formattedText/marks_rts.ts @@ -42,7 +42,7 @@ export const marks: { [index: string]: MarkSpec } = { node.attrs.allHrefs.length === 1 ? ["a", { ...node.attrs, class: linkids, targetids, title: `${node.attrs.title}`, href: node.attrs.allHrefs[0].href }, 0] : ["div", { class: "prosemirror-anchor" }, - ["button", { class: "prosemirror-linkBtn" }, + ["span", { class: "prosemirror-linkBtn" }, ["a", { ...node.attrs, class: linkids, targetids, title: `${node.attrs.title}` }, 0], ["input", { class: "prosemirror-hrefoptions" }], ], diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 8b4792ccc..9cdbea2da 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -126,16 +126,24 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu // file address of the pdf const { url: { href } } = Cast(this.dataDoc[this.props.fieldKey], PdfField)!; const { url: relative } = this.props; - const pathComponents = relative.split("/pdfs/")[1].split("/"); - const coreFilename = pathComponents.pop()!.split(".")[0]; - const params: any = { - coreFilename, - pageNum: this.Document.curPage || 1, - }; - if (pathComponents.length) { - params.subtree = `${pathComponents.join("/")}/`; + if (relative.includes("/pdfs/")) { + const pathComponents = relative.split("/pdfs/")[1].split("/"); + const coreFilename = pathComponents.pop()!.split(".")[0]; + const params: any = { + coreFilename, + pageNum: this.Document.curPage || 1, + }; + if (pathComponents.length) { + params.subtree = `${pathComponents.join("/")}/`; + } + this._coverPath = href.startsWith(window.location.origin) ? await Networking.PostToServer("/thumbnail", params) : { width: 100, height: 100, path: "" }; + } else { + const params: any = { + coreFilename: relative.split("/")[relative.split("/").length - 1], + pageNum: this.Document.curPage || 1, + }; + this._coverPath = "http://cs.brown.edu/~bcz/face.gif";//href.startsWith(window.location.origin) ? await Networking.PostToServer("/thumbnail", params) : { width: 100, height: 100, path: "" }; } - this._coverPath = href.startsWith(window.location.origin) ? await Networking.PostToServer("/thumbnail", params) : { width: 100, height: 100, path: "" }; runInAction(() => this._showWaiting = this._showCover = true); this.props.startupLive && this.setupPdfJsViewer(); this._mainCont.current!.scrollTop = this.layoutDoc._scrollTop || 0; diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index b205a4a10..96587af44 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -117,7 +117,7 @@ export function fetchProto(doc: Doc) { } if (doc.proto instanceof Promise) { - doc.proto.then(proto => fetchProto(proto)); + doc.proto.then(fetchProto); return doc.proto; } } @@ -670,7 +670,7 @@ export namespace Doc { copy[key] = cfield[Copy]();// ComputedField.MakeFunction(cfield.script.originalScript); } else if (field instanceof ObjectField) { copy[key] = doc[key] instanceof Doc ? - key.includes("layout[") ? Doc.MakeCopy(doc[key] as Doc, false) : doc[key] : // reference documents except copy documents that are expanded teplate fields + key.includes("layout[") ? undefined : doc[key] : // reference documents except remove documents that are expanded teplate fields ObjectField.MakeCopy(field); } else if (field instanceof Promise) { debugger; //This shouldn't happend... diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts index fc7f9ca80..11b3b0524 100644 --- a/src/fields/ScriptField.ts +++ b/src/fields/ScriptField.ts @@ -161,7 +161,7 @@ export class ComputedField extends ScriptField { Scripting.addGlobal(function getIndexVal(list: any[], index: number) { return list.reduce((p, x, i) => (i <= index && x !== undefined) || p === undefined ? x : p, undefined as any); -}); +}, "returns the value at a given index of a list", "(list: any[], index: number)"); export namespace ComputedField { let useComputed = true; diff --git a/src/server/Message.ts b/src/server/Message.ts index 80f372733..ff0381fd3 100644 --- a/src/server/Message.ts +++ b/src/server/Message.ts @@ -1,7 +1,5 @@ import { Utils } from "../Utils"; import { Point } from "../pen-gestures/ndollar"; -import { Doc } from "../fields/Doc"; -import { Image } from "canvas"; import { AnalysisResult, ImportResults } from "../scraping/buxton/final/BuxtonImporter"; export class Message<T> { diff --git a/src/server/database.ts b/src/server/database.ts index a5f23c4b1..2372cbcf2 100644 --- a/src/server/database.ts +++ b/src/server/database.ts @@ -2,7 +2,6 @@ import * as mongodb from 'mongodb'; import { Transferable } from './Message'; import { Opt } from '../fields/Doc'; import { Utils, emptyFunction } from '../Utils'; -import { Credentials } from 'google-auth-library'; import { GoogleApiServerUtils } from './apis/google/GoogleApiServerUtils'; import { IDatabase, DocumentsCollection } from './IDatabase'; import { MemoryDatabase } from './MemoryDatabase'; |