diff options
147 files changed, 4017 insertions, 2694 deletions
diff --git a/deploy/assets/endLink.png b/deploy/assets/endLink.png Binary files differnew file mode 100644 index 000000000..abadc9550 --- /dev/null +++ b/deploy/assets/endLink.png diff --git a/deploy/assets/link.png b/deploy/assets/link.png Binary files differnew file mode 100644 index 000000000..2dbcd61ce --- /dev/null +++ b/deploy/assets/link.png diff --git a/deploy/assets/startLink.png b/deploy/assets/startLink.png Binary files differnew file mode 100644 index 000000000..8f3825682 --- /dev/null +++ b/deploy/assets/startLink.png diff --git a/deploy/index.html b/deploy/index.html index 29a3f15cd..82e8c53a3 100644 --- a/deploy/index.html +++ b/deploy/index.html @@ -8,7 +8,7 @@ <script src="https://cdnjs.cloudflare.com/ajax/libs/typescript/3.3.1/typescript.min.js"></script> </head> -<body> +<body style="display:flex"> <!-- <script src="https://hypothes.is/embed.js" async></script> --> <div id="root" style="position:relative;width:100%;height:100%"></div> <script src="/bundle.js"></script> diff --git a/package-lock.json b/package-lock.json index ad181758c..698bd60cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -711,6 +711,12 @@ "express-validator": "*" } }, + "@types/file-saver": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.1.tgz", + "integrity": "sha512-g1QUuhYVVAamfCifK7oB7G3aIl4BbOyzDOqVyUfEr4tfBKrXfeH+M+Tg7HKCXSrbzxYdhyCP7z9WbKo0R2hBCw==", + "dev": true + }, "@types/formidable": { "version": "1.0.31", "resolved": "https://registry.npmjs.org/@types/formidable/-/formidable-1.0.31.tgz", @@ -6069,6 +6075,11 @@ } } }, + "file-saver": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.2.tgz", + "integrity": "sha512-Wz3c3XQ5xroCxd1G8b7yL0Ehkf0TC9oYC6buPFkNnU9EnaPlifeAFCyCh+iewXTyFRcg0a6j3J7FmJsIhlhBdw==" + }, "file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -7280,6 +7291,11 @@ } } }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=" + }, "import-fresh": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", @@ -8160,6 +8176,33 @@ "promise": "^7.0.1" } }, + "jszip": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.5.0.tgz", + "integrity": "sha512-WRtu7TPCmYePR1nazfrtuF216cIVon/3GWOvHS9QR5bIwSbnxtdpma6un3jyGGNhHsKCSzn5Ypk+EkDRvTGiFA==", + "requires": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "set-immediate-shim": "~1.0.1" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + } + } + }, "jwa": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", @@ -8300,6 +8343,14 @@ } } }, + "lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "requires": { + "immediate": "~3.0.5" + } + }, "lines-and-columns": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", @@ -14256,6 +14307,11 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, + "react-loading": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/react-loading/-/react-loading-2.0.3.tgz", + "integrity": "sha512-Vdqy79zq+bpeWJqC+xjltUjuGApyoItPgL0vgVfcJHhqwU7bAMKzysfGW/ADu6i0z0JiOCRJjo+IkFNkRNbA3A==" + }, "react-measure": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/react-measure/-/react-measure-2.3.0.tgz", @@ -15137,6 +15193,11 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=" + }, "set-value": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", diff --git a/package.json b/package.json index 5a6c6f0b1..6c466825e 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "child_process": "empty" }, "scripts": { - "start-release": "cross-env RELEASE=true NODE_OPTIONS=--max_old_space_size=4096 ts-node-dev --transpile-only -- src/server/index.ts", + "start-release": "cross-env RELEASE=true NODE_OPTIONS=--max_old_space_size=4096 ts-node-dev -- src/server/index.ts", "start": "cross-env NODE_OPTIONS=--max_old_space_size=4096 ts-node-dev --transpile-only -- src/server/index.ts", "oldstart": "cross-env NODE_OPTIONS=--max_old_space_size=4096 ts-node-dev -- src/server/index.ts", "debug": "cross-env NODE_OPTIONS=--max_old_space_size=8192 ts-node-dev --transpile-only --inspect -- src/server/index.ts", @@ -37,6 +37,7 @@ "@types/express-flash": "0.0.0", "@types/express-session": "^1.15.16", "@types/express-validator": "^3.0.0", + "@types/file-saver": "^2.0.1", "@types/formidable": "^1.0.31", "@types/google-maps-react": "^2.0.5", "@types/jquery": "^3.5.0", @@ -154,6 +155,7 @@ "express-session": "^1.17.0", "express-validator": "^5.3.1", "expressjs": "^1.0.1", + "file-saver": "^2.0.2", "find-in-files": "^0.5.0", "fit-curve": "^0.1.7", "flexlayout-react": "^0.3.11", @@ -172,6 +174,7 @@ "image-size-stream": "^1.1.0", "js-datepicker": "^4.6.6", "jsonschema": "^1.2.5", + "jszip": "^3.5.0", "libxmljs": "^0.19.7", "lodash": "^4.17.15", "material-ui": "^0.20.2", @@ -221,6 +224,7 @@ "react-grid-layout": "^0.18.3", "react-image-lightbox-with-rotate": "^5.1.1", "react-jsx-parser": "^1.25.1", + "react-loading": "^2.0.3", "react-measure": "^2.2.4", "react-resizable": "^1.10.1", "react-select": "^3.1.0", diff --git a/src/client/ClientRecommender.tsx b/src/client/ClientRecommender.tsx index d18669b02..3f875057e 100644 --- a/src/client/ClientRecommender.tsx +++ b/src/client/ClientRecommender.tsx @@ -50,7 +50,6 @@ export class ClientRecommender extends React.Component<RecommenderProps> { @observable private corr_matrix = [[0, 0], [0, 0]]; // for testing constructor(props: RecommenderProps) { - //console.log("creating client recommender..."); super(props); if (!ClientRecommender.Instance) ClientRecommender.Instance = this; ClientRecommender.Instance.docVectors = new Set(); @@ -383,7 +382,6 @@ export class ClientRecommender extends React.Component<RecommenderProps> { case 200: const title_vals: string[] = []; const url_vals: string[] = []; - //console.log(result); if (xml) { const titles = xml.getElementsByTagName("title"); let counter = 1; diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index 2a7a7c59a..bac324c77 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -31,7 +31,7 @@ export namespace DocServer { export enum WriteMode { Default = 0, //Anything goes - Playground = 1, //Playground (write own/no read) + Playground = 1, //Playground (write own/no read other updates) LiveReadonly = 2,//Live Readonly (no write/read others) LivePlayground = 3,//Live Playground (write own/read others) } @@ -39,9 +39,9 @@ export namespace DocServer { const docsWithUpdates: { [field: string]: Set<Doc> } = {}; export var PlaygroundFields: string[]; - export function setPlaygroundFields(livePlayougroundFields: string[]) { - DocServer.PlaygroundFields = livePlayougroundFields; - livePlayougroundFields.forEach(f => DocServer.setFieldWriteMode(f, DocServer.WriteMode.LivePlayground)); + export function setPlaygroundFields(livePlaygroundFields: string[]) { + DocServer.PlaygroundFields = livePlaygroundFields; + livePlaygroundFields.forEach(f => DocServer.setFieldWriteMode(f, DocServer.WriteMode.LivePlayground)); } export function setFieldWriteMode(field: string, writeMode: WriteMode) { diff --git a/src/client/apis/GoogleAuthenticationManager.tsx b/src/client/apis/GoogleAuthenticationManager.tsx index bf4469aeb..117d1fa1e 100644 --- a/src/client/apis/GoogleAuthenticationManager.tsx +++ b/src/client/apis/GoogleAuthenticationManager.tsx @@ -146,7 +146,7 @@ export default class GoogleAuthenticationManager extends React.Component<{}> { private get dialogueBoxStyle() { const borderColor = this.success === undefined ? "black" : this.success ? "green" : "red"; - return { borderColor, transition: "0.2s borderColor ease" }; + return { borderColor, transition: "0.2s borderColor ease", zIndex: 1002 }; } render() { @@ -155,8 +155,10 @@ export default class GoogleAuthenticationManager extends React.Component<{}> { isDisplayed={this.openState} interactive={true} contents={this.renderPrompt} - overlayDisplayedOpacity={0.9} + // overlayDisplayedOpacity={0.9} dialogueBoxStyle={this.dialogueBoxStyle} + overlayStyle={{ zIndex: 1001 }} + closeOnExternalClick={action(() => this.isOpen = false)} /> ); } diff --git a/src/client/apis/HypothesisAuthenticationManager.tsx b/src/client/apis/HypothesisAuthenticationManager.tsx index 9a6d78bac..d4a81b3eb 100644 --- a/src/client/apis/HypothesisAuthenticationManager.tsx +++ b/src/client/apis/HypothesisAuthenticationManager.tsx @@ -142,7 +142,7 @@ export default class HypothesisAuthenticationManager extends React.Component<{}> private get dialogueBoxStyle() { const borderColor = this.success === undefined ? "black" : this.success ? "green" : "red"; - return { borderColor, transition: "0.2s borderColor ease" }; + return { borderColor, transition: "0.2s borderColor ease", zIndex: 1002 }; } render() { @@ -151,8 +151,10 @@ export default class HypothesisAuthenticationManager extends React.Component<{}> isDisplayed={this.openState} interactive={true} contents={this.renderPrompt} - overlayDisplayedOpacity={0.9} + // overlayDisplayedOpacity={0.9} dialogueBoxStyle={this.dialogueBoxStyle} + overlayStyle={{ zIndex: 1001 }} + closeOnExternalClick={action(() => this.isOpen = false)} /> ); } diff --git a/src/client/apis/IBM_Recommender.ts b/src/client/apis/IBM_Recommender.ts index 480b9cb1c..e6265fcb5 100644 --- a/src/client/apis/IBM_Recommender.ts +++ b/src/client/apis/IBM_Recommender.ts @@ -29,10 +29,8 @@ // export const analyze = async (_parameters: any): Promise<Opt<string>> => { // try { // const response = await naturalLanguageUnderstanding.analyze(_parameters); -// console.log(response); // return (JSON.stringify(response, null, 2)); // } catch (err) { -// console.log('error: ', err); // return undefined; // } // }; diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts index 13bfb3a91..92eaf2e73 100644 --- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts +++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts @@ -156,8 +156,6 @@ export namespace GooglePhotos { const values = Object.values(ContentCategories).filter(value => value !== ContentCategories.NONE); for (const value of values) { const searched = (await ContentSearch({ included: [value] }))?.mediaItems?.map(({ id }) => id); - console.log("Searching " + value); - console.log(searched); searched?.forEach(async id => { const image = await Cast(idMapping[id], Doc); if (image) { diff --git a/src/client/cognitive_services/CognitiveServices.ts b/src/client/cognitive_services/CognitiveServices.ts index 6b0b3e029..80961af14 100644 --- a/src/client/cognitive_services/CognitiveServices.ts +++ b/src/client/cognitive_services/CognitiveServices.ts @@ -377,7 +377,6 @@ export namespace CognitiveServices { console.log("successful vectorization!"); const vectorValues = new List<number>(); indices.forEach((ind: any) => { - //console.log(wordvec.word); vectorValues.push(wordvecs[ind]); }); ClientRecommender.Instance.processVector(vectorValues, dataDoc, mainDoc); @@ -385,7 +384,6 @@ export namespace CognitiveServices { else { console.log("unsuccessful :( word(s) not in vocabulary"); } - //console.log(vectorValues.size); } ); } diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 7b85d2e46..f1d4ec46c 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -70,6 +70,7 @@ export interface DocumentOptions { _showTitle?: string; // which field to display in the title area. leave empty to have no title _showCaption?: string; // which field to display in the caption area. leave empty to have no caption _scrollTop?: number; // scroll location for pdfs + _noAutoscroll?: boolean;// whether collections autoscroll when this item is dragged _chromeStatus?: string; _viewType?: string; // sub type of a collection _gridGap?: number; // gap between items in masonry view @@ -92,7 +93,8 @@ export interface DocumentOptions { layoutKey?: string; type?: string; title?: string; - label?: string; // short form of title for use as an icon label + label?: string; + toolTip?: string; // tooltip to display on hover style?: string; page?: number; description?: string; // added for links @@ -103,6 +105,8 @@ export interface DocumentOptions { childLayoutTemplate?: Doc; // template for collection to use to render its children (see PresBox or Buxton layout in tree view) childLayoutString?: string; // template string for collection to use to render its children hideFilterView?: boolean; // whether to hide the filter popout on collections + hideLinkButton?: boolean; // whether the blue link counter button should be hidden + hideAllLinks?: boolean; // whether all individual blue anchor dots should be hidden _columnsHideIfEmpty?: boolean; // whether stacking view column headings should be hidden isTemplateForField?: string; // the field key for which the containing document is a rendering template isTemplateDoc?: boolean; @@ -124,7 +128,7 @@ export interface DocumentOptions { isBackground?: boolean; isLinkButton?: boolean; _columnWidth?: number; - _fontSize?: number; + _fontSize?: string; _fontFamily?: string; curPage?: number; currentTimecode?: number; // the current timecode of a time-based document (e.g., current time of a video) value is in seconds @@ -137,6 +141,8 @@ export interface DocumentOptions { dontRegisterChildViews?: boolean; lookupField?: ScriptField; // script that returns the value of a field. This script is passed the rootDoc, layoutDoc, field, and container of the document. see PresBox. "onDoubleClick-rawScript"?: string; // onDoubleClick script in raw text form + "onChildDoubleClick-rawScript"?: string; // onChildDoubleClick script in raw text form + "onChildClick-rawScript"?: string; // on ChildClick script in raw text form "onClick-rawScript"?: string; // onClick script in raw text form "onCheckedClick-rawScript"?: string; // onChecked script in raw text form "onCheckedClick-params"?: List<string>; // parameter list for onChecked treeview functions @@ -490,7 +496,7 @@ export namespace Docs { Doc.Get.FromJson({ data: device, appendToExisting: { targetDoc: Doc.GetProto(doc) } }); Doc.AddDocToList(parentProto, "data", doc); } else if (errors) { - console.log(errors); + console.log("Documents:" + errors); } else { alert("A Buxton document import was completely empty (??)"); } @@ -546,6 +552,11 @@ export namespace Docs { const dataDoc = MakeDataDelegate(proto, protoProps, data, fieldKey); const viewDoc = Doc.MakeDelegate(dataDoc, delegId); + // so that the list of annotations is already initialised, prevents issues in addonly. + // without this, if a doc has no annotations but the user has AddOnly privileges, they won't be able to add an annotation because they would have needed to create the field's list which they don't have permissions to do. + + dataDoc[fieldKey + "-annotations"] = new List<Doc>(); + proto.links = ComputedField.MakeFunction("links(self)"); viewDoc.author = Doc.CurrentUserEmail; @@ -672,12 +683,12 @@ export namespace Docs { I.type = DocumentType.INK; I.layout = InkingStroke.LayoutString("data"); I.color = color; - I.strokeWidth = strokeWidth; - I.strokeBezier = strokeBezier; I.fillColor = fillColor; - I.arrowStart = arrowStart; - I.arrowEnd = arrowEnd; - I.dash = dash; + I.strokeWidth = Number(strokeWidth); + I.strokeBezier = strokeBezier; + I.strokeStartMarker = arrowStart; + I.strokeEndMarker = arrowEnd; + I.strokeDash = dash; I.tool = tool; I.title = "ink"; I.x = options.x; @@ -686,14 +697,9 @@ export namespace Docs { I._width = options._width; I._height = options._height; I.author = Doc.CurrentUserEmail; + I.rotation = 0; I.data = new InkField(points); return I; - // return I; - // const doc = InstanceFromProto(Prototypes.get(DocumentType.INK), new InkField(points), options); - // doc.color = color; - // doc.strokeWidth = strokeWidth; - // doc.tool = tool; - // return doc; } export function PdfDocument(url: string, options: DocumentOptions = {}) { @@ -783,7 +789,7 @@ export namespace Docs { export function FontIconDocument(options?: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.FONTICON), undefined, { ...(options || {}) }); + return InstanceFromProto(Prototypes.get(DocumentType.FONTICON), undefined, { hideLinkButton: true, ...(options || {}) }); } export function PresElementBoxDocument(options?: DocumentOptions) { @@ -916,6 +922,8 @@ export namespace DocUtils { if (target.doc === Doc.UserDoc()) return undefined; const linkDoc = Docs.Create.LinkDocument(source, target, { linkRelationship, layoutKey: "layout_linkView", description }, id); + linkDoc.linkDisplay = true; + linkDoc.hidden = true; 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'); @@ -978,6 +986,7 @@ export namespace DocUtils { } if (type.indexOf("pdf") !== -1) { ctor = Docs.Create.PdfDocument; + if (!options._fitWidth) options._fitWidth = true; if (!options._width) options._width = 400; if (!options._height) options._height = options._width * 1200 / 927; } diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index cb2a025cb..ff33d35e0 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -4,7 +4,7 @@ import { Utils } from "../../Utils"; import { DocServer } from "../DocServer"; import { Docs, DocumentOptions, DocUtils } from "../documents/Documents"; import { UndoManager } from "./UndoManager"; -import { Doc, DocListCast, DocListCastAsync } from "../../fields/Doc"; +import { Doc, DocListCast, DocListCastAsync, DataSym } from "../../fields/Doc"; import { List } from "../../fields/List"; import { listSpec } from "../../fields/Schema"; import { ScriptField, ComputedField } from "../../fields/ScriptField"; @@ -22,6 +22,7 @@ import { DocumentType } from "../documents/DocumentTypes"; import { SchemaHeaderField } from "../../fields/SchemaHeaderField"; import { DimUnit } from "../views/collections/collectionMulticolumn/CollectionMulticolumnView"; import { LabelBox } from "../views/nodes/LabelBox"; +import { LinkManager } from "./LinkManager"; export class CurrentUserUtils { private static curr_id: string; @@ -91,29 +92,29 @@ export class CurrentUserUtils { } if (doc["template-button-description"] === undefined) { - const descriptionTemplate = Docs.Create.TextDocument(" ", { title: "header", _height: 100 }, "header"); // text needs to be a space to allow templateText to be created - Doc.GetProto(descriptionTemplate).layout = + const descriptionTemplate = Doc.MakeDelegate(Docs.Create.TextDocument(" ", { title: "header", _height: 100 }, "header")); // text needs to be a space to allow templateText to be created + descriptionTemplate[DataSym].layout = "<div>" + " <FormattedTextBox {...props} height='{this._headerHeight||75}px' background='{this._headerColor||`orange`}' fieldKey={'header'}/>" + " <FormattedTextBox {...props} position='absolute' top='{(this._headerHeight||75)*scale}px' height='calc({100/scale}% - {this._headerHeight||75}px)' fieldKey={'text'}/>" + "</div>"; - descriptionTemplate.isTemplateDoc = makeTemplate(descriptionTemplate, true, "descriptionView"); + (descriptionTemplate.proto as Doc).isTemplateDoc = makeTemplate(descriptionTemplate.proto as Doc, true, "descriptionView"); doc["template-button-description"] = CurrentUserUtils.ficon({ - onDragStart: ScriptField.MakeFunction('makeDelegate(this.dragFactory)'), + onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'), dragFactory: new PrefetchProxy(descriptionTemplate) as any as Doc, removeDropProperties: new List<string>(["dropAction"]), title: "description view", icon: "window-maximize" }); } if (doc["template-button-link"] === undefined) { // set _backgroundColor to transparent to prevent link dot from obscuring document it's attached to. - const linkTemplate = Docs.Create.TextDocument(" ", { title: "header", _height: 100 }, "header"); // text needs to be a space to allow templateText to be created + const linkTemplate = Doc.MakeDelegate(Docs.Create.TextDocument(" ", { title: "header", _height: 100 }, "header")); // text needs to be a space to allow templateText to be created Doc.GetProto(linkTemplate).layout = "<div>" + " <FormattedTextBox {...props} height='{this._headerHeight||75}px' background='{this._headerColor||`lightGray`}' fieldKey={'header'}/>" + " <FormattedTextBox {...props} position='absolute' top='{(this._headerHeight||75)*scale}px' height='calc({100/scale}% - {this._headerHeight||75}px)' fieldKey={'text'}/>" + "</div>"; - linkTemplate.isTemplateDoc = makeTemplate(linkTemplate, true, "linkView"); + (linkTemplate.proto as Doc).isTemplateDoc = makeTemplate(linkTemplate.proto as Doc, true, "linkView"); const rtf2 = { doc: { @@ -146,7 +147,7 @@ export class CurrentUserUtils { linkTemplate.header = new RichTextField(JSON.stringify(rtf2), ""); doc["template-button-link"] = CurrentUserUtils.ficon({ - onDragStart: ScriptField.MakeFunction('makeDelegate(this.dragFactory)'), + onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'), dragFactory: new PrefetchProxy(linkTemplate) as any as Doc, removeDropProperties: new List<string>(["dropAction"]), title: "link view", icon: "window-maximize" }); @@ -211,7 +212,7 @@ export class CurrentUserUtils { details.text = new RichTextField(JSON.stringify(detailedTemplate), buxtonFieldKeys.join(" ")); const shared = { _chromeStatus: "disabled", _autoHeight: true, _xMargin: 0 }; - const detailViewOpts = { title: "detailView", _width: 300, _fontFamily: "Arial", _fontSize: 12 }; + const detailViewOpts = { title: "detailView", _width: 300, _fontFamily: "Arial", _fontSize: "12pt" }; const descriptionWrapperOpts = { title: "descriptions", _height: 300, _columnWidth: -1, treeViewHideTitle: true, _pivotField: "title" }; const descriptionWrapper = MasonryDocument([details, short, long], { ...shared, ...descriptionWrapperOpts }); @@ -373,7 +374,7 @@ export class CurrentUserUtils { } static creatorBtnDescriptors(doc: Doc): { - title: string, label: string, icon: string, drag?: string, ignoreClick?: boolean, + title: string, toolTip: string, icon: string, drag?: string, ignoreClick?: boolean, click?: string, ischecked?: string, activeInkPen?: Doc, backgroundColor?: string, dragFactory?: Doc }[] { if (doc.emptyPresentation === undefined) { @@ -390,6 +391,21 @@ export class CurrentUserUtils { if (doc.emptyScript === undefined) { doc.emptyScript = Docs.Create.ScriptingDocument(undefined, { _width: 200, _height: 250, title: "script" }); } + if (doc.emptyScreenshot === undefined) { + doc.emptyScreenshot = Docs.Create.ScreenshotDocument("", { _width: 400, _height: 200, title: "screen snapshot" }); + } + if (doc.emptyAudio === undefined) { + doc.emptyAudio = Docs.Create.AudioDocument(nullAudio, { _width: 200, title: "ready to record audio" }); + } + if (doc.emptyImage === undefined) { + doc.emptyImage = Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { _width: 250, _nativeWidth: 250, title: "an image of a cat" }); + } + if (doc.emptyButton === undefined) { + doc.emptyButton = Docs.Create.ButtonDocument({ _width: 150, _height: 50, _xPadding: 10, _yPadding: 10, title: "Button" }); + } + if (doc.emptySearch === undefined) { + doc.emptySearch = Docs.Create.QueryDocument({ _width: 200, title: "empty search" }); + } if (doc.emptyDocHolder === undefined) { doc.emptyDocHolder = Docs.Create.DocumentDocument( ComputedField.MakeFunction("selectedDocs(this,this.excludeCollections,[_last_])?.[0]") as any, @@ -402,29 +418,30 @@ export class CurrentUserUtils { this.setupActiveMobileMenu(doc); } return [ - { title: "Drag a comparison box", label: "Comp", icon: "columns", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyComparison as Doc }, - { title: "Drag a collection", label: "Col", icon: "folder", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyCollection as Doc }, - { title: "Drag a web page", label: "Web", icon: "globe-asia", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyWebpage as Doc }, - { title: "Drag a cat image", label: "Img", icon: "cat", ignoreClick: true, drag: 'Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { _width: 250, _nativeWidth:250, title: "an image of a cat" })' }, - { title: "Drag a screenshot", label: "Grab", icon: "photo-video", ignoreClick: true, drag: 'Docs.Create.ScreenshotDocument("", { _width: 400, _height: 200, title: "screen snapshot" })' }, - { title: "Drag a webcam", label: "Cam", icon: "video", ignoreClick: true, drag: 'Docs.Create.WebCamDocument("", { _width: 400, _height: 400, title: "a test cam" })' }, - { title: "Drag a audio recorder", label: "Audio", icon: "microphone", ignoreClick: true, drag: `Docs.Create.AudioDocument("${nullAudio}", { _width: 200, title: "ready to record audio" })` }, - { title: "Drag a clickable button", label: "Btn", icon: "bolt", ignoreClick: true, drag: 'Docs.Create.ButtonDocument({ _width: 150, _height: 50, _xPadding:10, _yPadding: 10, title: "Button" })' }, - { title: "Drag a presentation view", label: "Prezi", icon: "tv", click: 'openOnRight(Doc.UserDoc().activePresentation = getCopy(this.dragFactory, true))', drag: `Doc.UserDoc().activePresentation = getCopy(this.dragFactory,true)`, dragFactory: doc.emptyPresentation as Doc }, - { title: "Drag a search box", label: "Query", icon: "search", ignoreClick: true, drag: 'Docs.Create.QueryDocument({ _width: 200, title: "an image of a cat" })' }, - { title: "Drag a scripting box", label: "Script", icon: "terminal", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyScript as Doc }, - { title: "Drag an import folder", label: "Load", icon: "cloud-upload-alt", ignoreClick: true, drag: 'Docs.Create.DirectoryImportDocument({ title: "Directory Import", _width: 400, _height: 400 })' }, - { title: "Drag a mobile view", label: "Phone", icon: "mobile", click: 'openOnRight(Doc.UserDoc().activeMobileMenu)', drag: 'this.dragFactory', dragFactory: doc.activeMobileMenu as Doc }, - { title: "Drag an instance of the device collection", label: "Buxton", icon: "globe-asia", ignoreClick: true, drag: 'Docs.Create.Buxton()' }, + { toolTip: "Drag a collection", title: "Col", icon: "folder", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyCollection as Doc }, + { toolTip: "Drag a web page", title: "Web", icon: "globe-asia", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyWebpage as Doc }, + { toolTip: "Drag a cat image", title: "Image", icon: "cat", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyImage as Doc }, + { toolTip: "Drag a comparison box", title: "Comp", icon: "columns", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyComparison as Doc }, + { toolTip: "Drag a screengrabber", title: "Grab", icon: "photo-video", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyScreenshot as Doc }, + // { title: "Drag a webcam", title: "Cam", icon: "video", ignoreClick: true, drag: 'Docs.Create.WebCamDocument("", { _width: 400, _height: 400, title: "a test cam" })' }, + { toolTip: "Drag a audio recorder", title: "Audio", icon: "microphone", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyAudio as Doc }, + { toolTip: "Drag a button", title: "Button", icon: "bolt", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyButton as Doc }, + + { toolTip: "Drag a presentation view", title: "Prezi", icon: "tv", click: 'openOnRight(Doc.UserDoc().activePresentation = getCopy(this.dragFactory, true))', drag: `Doc.UserDoc().activePresentation = getCopy(this.dragFactory, true)`, dragFactory: doc.emptyPresentation as Doc }, + { toolTip: "Drag a search box", title: "Query", icon: "search", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptySearch as Doc }, + { toolTip: "Drag a scripting box", title: "Script", icon: "terminal", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyScript as Doc }, + // { title: "Drag an import folder", title: "Load", icon: "cloud-upload-alt", ignoreClick: true, drag: 'Docs.Create.DirectoryImportDocument({ title: "Directory Import", _width: 400, _height: 400 })' }, + { toolTip: "Drag a mobile view", title: "Phone", icon: "mobile", click: 'openOnRight(Doc.UserDoc().activeMobileMenu)', drag: 'this.dragFactory', dragFactory: doc.activeMobileMenu as Doc }, + // { title: "Drag an instance of the device collection", title: "Buxton", icon: "globe-asia", ignoreClick: true, drag: 'Docs.Create.Buxton()' }, // { title: "use pen", icon: "pen-nib", click: 'activatePen(this.activeInkPen = sameDocs(this.activeInkPen, this) ? undefined : this)', backgroundColor: "blue", ischecked: `sameDocs(this.activeInkPen, this)`, activeInkPen: doc }, // { title: "use highlighter", icon: "highlighter", click: 'activateBrush(this.activeInkPen = sameDocs(this.activeInkPen, this) ? undefined : this,20,this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activeInkPen, this)`, activeInkPen: doc }, // { title: "use stamp", icon: "stamp", click: 'activateStamp(this.activeInkPen = sameDocs(this.activeInkPen, this) ? undefined : this)', backgroundColor: "orange", ischecked: `sameDocs(this.activeInkPen, this)`, activeInkPen: doc }, // { title: "use eraser", icon: "eraser", click: 'activateEraser(this.activeInkPen = sameDocs(this.activeInkPen, this) ? undefined : this);', ischecked: `sameDocs(this.activeInkPen, this)`, backgroundColor: "pink", activeInkPen: doc }, // { title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activeInkPen = this;', ischecked: `sameDocs(this.activeInkPen, this)`, backgroundColor: "white", activeInkPen: doc }, - { title: "Drag a document previewer", label: "Prev", icon: "expand", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory,true)', dragFactory: doc.emptyDocHolder as Doc }, - { title: "Toggle a Calculator REPL", label: "repl", icon: "calculator", click: 'addOverlayWindow("ScriptingRepl", { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" })' }, - { title: "Connect a Google Account", label: "Google Account", icon: "external-link-alt", click: 'GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(true)' }, - { title: "Connect a Hypothesis Account", label: "Hypothesis Account", icon: "heading", click: 'HypothesisAuthenticationManager.Instance.fetchAccessToken(true)' }, + { toolTip: "Drag a document previewer", title: "Prev", icon: "expand", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory,true)', dragFactory: doc.emptyDocHolder as Doc }, + { toolTip: "Toggle a Calculator REPL", title: "repl", icon: "calculator", click: 'addOverlayWindow("ScriptingRepl", { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" })' }, + { toolTip: "Connect a Google Account", title: "Google Account", icon: "external-link-alt", click: 'GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(true)' }, + { toolTip: "Connect a Hypothesis Account", title: "Hypothesis Account", icon: "heading", click: 'HypothesisAuthenticationManager.Instance.fetchAccessToken(true)' }, ]; } @@ -441,11 +458,11 @@ export class CurrentUserUtils { } } const buttons = CurrentUserUtils.creatorBtnDescriptors(doc).filter(d => !alreadyCreatedButtons?.includes(d.title)); - const creatorBtns = buttons.map(({ title, label, icon, ignoreClick, drag, click, ischecked, activeInkPen, backgroundColor, dragFactory }) => Docs.Create.FontIconDocument({ + const creatorBtns = buttons.map(({ title, toolTip, icon, ignoreClick, drag, click, ischecked, activeInkPen, backgroundColor, dragFactory }) => Docs.Create.FontIconDocument({ _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, icon, title, - label, + toolTip, ignoreClick, dropAction: "copy", onDragStart: drag ? ScriptField.MakeFunction(drag) : undefined, @@ -472,7 +489,6 @@ export class CurrentUserUtils { // Sets up mobile menu if it is undefined creates a new one, otherwise returns existing menu static setupActiveMobileMenu(doc: Doc) { if (doc.activeMobileMenu === undefined) { - console.log("undefined"); doc.activeMobileMenu = this.setupMobileMenu(); } return doc.activeMobileMenu as Doc; @@ -525,13 +541,13 @@ export class CurrentUserUtils { // Sets up the title of the button static mobileButtonText = (opts: DocumentOptions, buttonTitle: string) => Docs.Create.TextDocument(buttonTitle, { ...opts, - dropAction: undefined, title: buttonTitle, _fontSize: 37, _xMargin: 0, _yMargin: 0, ignoreClick: true, _chromeStatus: "disabled", backgroundColor: "rgba(0,0,0,0)" + dropAction: undefined, title: buttonTitle, _fontSize: "37pt", _xMargin: 0, _yMargin: 0, ignoreClick: true, _chromeStatus: "disabled", backgroundColor: "rgba(0,0,0,0)" }) as any as Doc // Sets up the description of the button static mobileButtonInfo = (opts: DocumentOptions, buttonInfo: string) => Docs.Create.TextDocument(buttonInfo, { ...opts, - dropAction: undefined, title: "info", _fontSize: 25, _xMargin: 0, _yMargin: 0, ignoreClick: true, _chromeStatus: "disabled", backgroundColor: "rgba(0,0,0,0)", _dimMagnitude: 2, + dropAction: undefined, title: "info", _fontSize: "25pt", _xMargin: 0, _yMargin: 0, ignoreClick: true, _chromeStatus: "disabled", backgroundColor: "rgba(0,0,0,0)", _dimMagnitude: 2, }) as any as Doc @@ -598,10 +614,10 @@ export class CurrentUserUtils { _width: 500, lockedPosition: true, _chromeStatus: "disabled", title: "tools stack", forceActive: true })) as any as Doc; doc["tabs-button-tools"] = new PrefetchProxy(Docs.Create.ButtonDocument({ - _width: 35, _height: 25, title: "Tools", _fontSize: 10, + _width: 35, _height: 25, title: "Tools", _fontSize: "10pt", letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)", sourcePanel: toolsStack, - onDragStart: ScriptField.MakeFunction('getAlias(this.dragFactory, true)'), + onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'), dragFactory: toolsStack, removeDropProperties: new List<string>(["lockedPosition"]), stayInCollection: true, @@ -663,15 +679,15 @@ export class CurrentUserUtils { lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true, targetDropAction: "same" })) as any as Doc; doc["tabs-button-library"] = new PrefetchProxy(Docs.Create.ButtonDocument({ - _width: 50, _height: 25, title: "Library", _fontSize: 10, targetDropAction: "same", + _width: 50, _height: 25, title: "Library", _fontSize: "10pt", targetDropAction: "same", letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)", sourcePanel: libraryStack, - onDragStart: ScriptField.MakeFunction('getAlias(this.dragFactory, true)'), + onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'), dragFactory: libraryStack, removeDropProperties: new List<string>(["lockedPosition"]), stayInCollection: true, targetContainer: new PrefetchProxy(sidebarContainer) as any as Doc, - onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel;") + onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel") })); } return doc["tabs-button-library"] as Doc; @@ -681,7 +697,7 @@ export class CurrentUserUtils { static setupSearchBtnPanel(doc: Doc, sidebarContainer: Doc) { if (doc["tabs-button-search"] === undefined) { doc["tabs-button-search"] = new PrefetchProxy(Docs.Create.ButtonDocument({ - _width: 50, _height: 25, title: "Search", _fontSize: 10, + _width: 50, _height: 25, title: "Search", _fontSize: "10pt", letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)", sourcePanel: new PrefetchProxy(Docs.Create.QueryDocument({ title: "search stack", })) as any as Doc, searchFileTypes: new List<string>([DocumentType.RTF, DocumentType.IMG, DocumentType.PDF, DocumentType.VID, DocumentType.WEB, DocumentType.SCRIPTING]), @@ -721,33 +737,25 @@ export class CurrentUserUtils { } static blist = (opts: DocumentOptions, docs: Doc[]) => new PrefetchProxy(Docs.Create.LinearDocument(docs, { - ...opts, - _gridGap: 5, _xMargin: 5, _yMargin: 5, _height: 42, _width: 100, boxShadow: "0 0", forceActive: true, + ...opts, _gridGap: 5, _xMargin: 5, _yMargin: 5, _height: 42, _width: 100, boxShadow: "0 0", forceActive: true, dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), backgroundColor: "black", treeViewPreventOpen: true, lockedPosition: true, _chromeStatus: "disabled", linearViewIsExpanded: true })) as any as Doc static ficon = (opts: DocumentOptions) => new PrefetchProxy(Docs.Create.FontIconDocument({ - ...opts, - dropAction: "alias", removeDropProperties: new List<string>(["dropAction"]), _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100 + ...opts, dropAction: "alias", removeDropProperties: new List<string>(["dropAction"]), _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100 })) as any as Doc /// sets up the default list of buttons to be shown in the expanding button menu at the bottom of the Dash window static setupDockedButtons(doc: Doc) { - if (doc["dockedBtn-pen"] === undefined) { - doc["dockedBtn-pen"] = CurrentUserUtils.ficon({ - onClick: ScriptField.MakeScript("activatePen(this.activeInkPen = sameDocs(this.activeInkPen, this) ? undefined : this)"), - author: "systemTemplates", title: "ink mode", icon: "pen-nib", ischecked: ComputedField.MakeFunction(`sameDocs(this.activeInkPen, this)`), activeInkPen: doc - }); - } if (doc["dockedBtn-undo"] === undefined) { - doc["dockedBtn-undo"] = CurrentUserUtils.ficon({ onClick: ScriptField.MakeScript("undo()"), title: "undo button", icon: "undo-alt" }); + doc["dockedBtn-undo"] = CurrentUserUtils.ficon({ onClick: ScriptField.MakeScript("undo()"), toolTip: "click to undo", title: "undo", icon: "undo-alt" }); } if (doc["dockedBtn-redo"] === undefined) { - doc["dockedBtn-redo"] = CurrentUserUtils.ficon({ onClick: ScriptField.MakeScript("redo()"), title: "redo button", icon: "redo-alt" }); + doc["dockedBtn-redo"] = CurrentUserUtils.ficon({ onClick: ScriptField.MakeScript("redo()"), toolTip: "click to redo", title: "redo", icon: "redo-alt" }); } if (doc.dockedBtns === undefined) { - doc.dockedBtns = CurrentUserUtils.blist({ title: "docked buttons", ignoreClick: true }, [doc["dockedBtn-undo"] as Doc, doc["dockedBtn-redo"] as Doc, doc["dockedBtn-pen"] as Doc]); + doc.dockedBtns = CurrentUserUtils.blist({ title: "docked buttons", ignoreClick: true }, [doc["dockedBtn-undo"] as Doc, doc["dockedBtn-redo"] as Doc]); } } // sets up the default set of documents to be shown in the Overlay layer @@ -777,17 +785,17 @@ export class CurrentUserUtils { static setupClickEditorTemplates(doc: Doc) { if (doc["clickFuncs-child"] === undefined) { + // to use this function, select it from the context menu of a collection. then edit the onChildClick script. Add two Doc variables: 'target' and 'thisContainer', then assign 'target' to some target collection. After that, clicking on any document in the initial collection will open it in the target const openInTarget = Docs.Create.ScriptingDocument(ScriptField.MakeScript( - "docCast(thisContainer.target).then((target) => {" + - " target && docCast(this.source).then((source) => { " + - " target.proto.data = new List([source || this]); " + - " }); " + - "})", - { target: Doc.name }), { title: "Click to open in target", _width: 300, _height: 200, targetScriptKey: "onChildClick" }); + "docCast(thisContainer.target).then((target) => target && (target.proto.data = new List([self]))) ", + { thisContainer: Doc.name }), { + title: "Click to open in target", _width: 300, _height: 200, + targetScriptKey: "onChildClick", + }); const openDetail = Docs.Create.ScriptingDocument(ScriptField.MakeScript( "openOnRight(self.doubleClickView)", - { target: Doc.name }), { title: "Double click to open doubleClickView", _width: 300, _height: 200, targetScriptKey: "onChildDoubleClick" }); + {}), { title: "Double click to open doubleClickView", _width: 300, _height: 200, targetScriptKey: "onChildDoubleClick" }); doc["clickFuncs-child"] = Docs.Create.TreeDocument([openInTarget, openDetail], { title: "on Child Click function templates" }); } @@ -799,14 +807,22 @@ export class CurrentUserUtils { title: "onClick", "onClick-rawScript": "console.log('click')", isTemplateDoc: true, isTemplateForField: "onClick", _width: 300, _height: 200 }, "onClick"); + const onChildClick = Docs.Create.ScriptingDocument(undefined, { + title: "onChildClick", "onChildClick-rawScript": "console.log('child click')", + isTemplateDoc: true, isTemplateForField: "onChildClick", _width: 300, _height: 200 + }, "onChildClick"); const onDoubleClick = Docs.Create.ScriptingDocument(undefined, { title: "onDoubleClick", "onDoubleClick-rawScript": "console.log('double click')", isTemplateDoc: true, isTemplateForField: "onDoubleClick", _width: 300, _height: 200 }, "onDoubleClick"); + const onChildDoubleClick = Docs.Create.ScriptingDocument(undefined, { + title: "onChildDoubleClick", "onChildDoubleClick-rawScript": "console.log('child double click')", + isTemplateDoc: true, isTemplateForField: "onChildDoubleClick", _width: 300, _height: 200 + }, "onChildDoubleClick"); const onCheckedClick = Docs.Create.ScriptingDocument(undefined, { title: "onCheckedClick", "onCheckedClick-rawScript": "console.log(heading + checked + containingTreeView)", "onCheckedClick-params": new List<string>(["heading", "checked", "containingTreeView"]), isTemplateDoc: true, isTemplateForField: "onCheckedClick", _width: 300, _height: 200 }, "onCheckedClick"); - doc.clickFuncs = Docs.Create.TreeDocument([onClick, onDoubleClick, onCheckedClick], { title: "onClick funcs" }); + doc.clickFuncs = Docs.Create.TreeDocument([onClick, onChildClick, onDoubleClick, onCheckedClick], { title: "onClick funcs" }); } PromiseValue(Cast(doc.clickFuncs, Doc)).then(func => func && PromiseValue(func.data).then(DocListCast)); @@ -820,11 +836,12 @@ export class CurrentUserUtils { doc.activeInkColor = StrCast(doc.activeInkColor, "rgb(0, 0, 0)"); doc.activeInkWidth = StrCast(doc.activeInkWidth, "1"); doc.activeInkBezier = StrCast(doc.activeInkBezier, "0"); - doc.activeFillColor = StrCast(doc.activeFillColor, "none"); - doc.activeArrowStart = StrCast(doc.activeArrowStart, "none"); - doc.activeArrowEnd = StrCast(doc.activeArrowEnd, "none"); + doc.activeFillColor = StrCast(doc.activeFillColor, ""); + doc.activeArrowStart = StrCast(doc.activeArrowStart, ""); + doc.activeArrowEnd = StrCast(doc.activeArrowEnd, ""); doc.activeDash = StrCast(doc.activeDash, "0"); - doc.fontSize = NumCast(doc.fontSize, 12); + doc.fontSize = StrCast(doc.fontSize, "12pt"); + doc.fontFamily = StrCast(doc.fontFamily, "Arial"); doc["constants-snapThreshold"] = NumCast(doc["constants-snapThreshold"], 10); // doc["constants-dragThreshold"] = NumCast(doc["constants-dragThreshold"], 4); // Utils.DRAG_THRESHOLD = NumCast(doc["constants-dragThreshold"]); @@ -873,3 +890,8 @@ export class CurrentUserUtils { Scripting.addGlobal(function createNewWorkspace() { return MainView.Instance.createNewWorkspace(); }, "creates a new workspace when called"); + +Scripting.addGlobal(function links(doc: any) { return new List(LinkManager.Instance.getAllRelatedLinks(doc)); }, + "returns all the links to the document or its annotations", "(doc: any)"); +Scripting.addGlobal(function directLinks(doc: any) { return new List(LinkManager.Instance.getAllDirectLinks(doc)); }, + "returns all the links directly to the document", "(doc: any)"); diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts index 28b1ca6cf..540540642 100644 --- a/src/client/util/DictationManager.ts +++ b/src/client/util/DictationManager.ts @@ -121,7 +121,7 @@ export namespace DictationManager { const listenImpl = (options?: Partial<ListeningOptions>) => { if (!recognizer) { - console.log(unsupported); + console.log("DictationManager:" + unsupported); return unsupported; } if (isListening) { diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 1fa5faeb3..51b50878d 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -9,6 +9,7 @@ import { LinkManager } from './LinkManager'; import { Scripting } from './Scripting'; import { SelectionManager } from './SelectionManager'; import { DocumentType } from '../documents/DocumentTypes'; +import { TraceMobx } from '../../fields/util'; export type CreateViewFunc = (doc: Doc, followLinkLocation: string, finished?: () => void) => void; @@ -104,8 +105,9 @@ export class DocumentManager { @computed public get LinkedDocumentViews() { + TraceMobx(); const pairs = DocumentManager.Instance.DocumentViews.reduce((pairs, dv) => { - const linksList = LinkManager.Instance.getAllRelatedLinks(dv.props.Document); + const linksList = DocListCast(dv.props.Document.links); pairs.push(...linksList.reduce((pairs, link) => { const linkToDoc = link && LinkManager.Instance.getOppositeAnchor(link, dv.props.Document); linkToDoc && DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 => { @@ -146,7 +148,7 @@ export class DocumentManager { }; const docView = getFirstDocView(targetDoc, originatingDoc); let annotatedDoc = await Cast(targetDoc.annotationOn, Doc); - if (annotatedDoc && !linkDoc?.isPushpin) { + if (annotatedDoc && !targetDoc?.isPushpin) { const first = getFirstDocView(annotatedDoc); if (first) { annotatedDoc = first.props.Document; @@ -156,7 +158,7 @@ export class DocumentManager { } } if (docView) { // we have a docView already and aren't forced to create a new one ... just focus on the document. TODO move into view if necessary otherwise just highlight? - if (linkDoc?.isPushpin) docView.props.Document.hidden = !docView.props.Document.hidden; + if (originatingDoc?.isPushpin) docView.props.Document.hidden = !docView.props.Document.hidden; else { docView.props.Document.hidden && (docView.props.Document.hidden = undefined); docView.props.focus(docView.props.Document, willZoom, undefined, focusAndFinish); diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 2ceafff30..837f0b1db 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -7,7 +7,7 @@ import { listSpec } from "../../fields/Schema"; import { SchemaHeaderField } from "../../fields/SchemaHeaderField"; import { ScriptField } from "../../fields/ScriptField"; import { Cast, NumCast, ScriptCast, StrCast } from "../../fields/Types"; -import { emptyFunction } from "../../Utils"; +import { emptyFunction, returnTrue } from "../../Utils"; import { Docs, DocUtils } from "../documents/Documents"; import * as globalCssVariables from "../views/globalCssVariables.scss"; import { UndoManager } from "./UndoManager"; @@ -83,6 +83,7 @@ export namespace DragManager { hideSource?: boolean; // hide source document during drag offsetX?: number; // offset of top left of source drag visual from cursor offsetY?: number; + noAutoscroll?: boolean; } // event called when the drag operation results in a drop action @@ -225,13 +226,17 @@ export namespace DragManager { // drag a button template and drop a new button export function StartButtonDrag(eles: HTMLElement[], script: string, title: string, vars: { [name: string]: Field }, params: string[], initialize: (button: Doc) => void, downX: number, downY: number, options?: DragOptions) { const finishDrag = (e: DragCompleteEvent) => { - const bd = Docs.Create.ButtonDocument({ _width: 150, _height: 50, title, onClick: ScriptField.MakeScript(script) }); + const bd = params.length > Object.keys(vars).length ? + Docs.Create.ButtonDocument({ toolTip: title, z: 1, _width: 150, _height: 50, title, onClick: ScriptField.MakeScript(script) }) : + Docs.Create.FontIconDocument({ toolTip: title, z: 1, _nativeWidth: 30, _nativeHeight: 30, _width: 30, _height: 30, title, onClick: ScriptField.MakeScript(script) }); params.map(p => Object.keys(vars).indexOf(p) !== -1 && (Doc.GetProto(bd)[p] = new PrefetchProxy(vars[p] as Doc))); // copy all "captured" arguments into document parameterfields initialize?.(bd); Doc.GetProto(bd)["onClick-paramFieldKeys"] = new List<string>(params); e.docDragData && (e.docDragData.droppedDocuments = [bd]); return e; }; + options = options ?? {}; + options.noAutoscroll = true; // these buttons are being dragged on the overlay layer, so scrollin the underlay is not appropriate StartDrag(eles, new DragManager.DocumentDragData([]), downX, downY, options, finishDrag); } @@ -323,7 +328,7 @@ export namespace DragManager { dragLabel = document.createElement("div"); dragLabel.className = "dragManager-dragLabel"; dragLabel.style.zIndex = "100001"; - dragLabel.style.fontSize = "10"; + dragLabel.style.fontSize = "10pt"; dragLabel.style.position = "absolute"; // dragLabel.innerText = "press 'a' to embed on drop"; // bcz: need to move this to a status bar dragDiv.appendChild(dragLabel); @@ -407,6 +412,7 @@ export namespace DragManager { const yFromTop = downY - elesCont.top; const xFromRight = elesCont.right - downX; const yFromBottom = elesCont.bottom - downY; + let scrollAwaiter: Opt<NodeJS.Timeout>; const moveHandler = (e: PointerEvent) => { e.preventDefault(); // required or dragging text menu link item ends up dragging the link button as native drag/drop if (dragData instanceof DocumentDragData) { @@ -427,6 +433,59 @@ export namespace DragManager { }, dragData.droppedDocuments); } + const target = document.elementFromPoint(e.x, e.y); + + if (target && !options?.noAutoscroll && !dragData.draggedDocuments?.some((d: any) => d._noAutoscroll)) { + const autoScrollHandler = () => { + target.dispatchEvent( + new CustomEvent<React.DragEvent>("dashDragAutoScroll", { + bubbles: true, + detail: { + shiftKey: e.shiftKey, + altKey: e.altKey, + metaKey: e.metaKey, + ctrlKey: e.ctrlKey, + clientX: e.clientX, + clientY: e.clientY, + dataTransfer: new DataTransfer, + button: e.button, + buttons: e.buttons, + getModifierState: e.getModifierState, + movementX: e.movementX, + movementY: e.movementY, + pageX: e.pageX, + pageY: e.pageY, + relatedTarget: e.relatedTarget, + screenX: e.screenX, + screenY: e.screenY, + detail: e.detail, + view: e.view ? e.view : new Window, + nativeEvent: new DragEvent("dashDragAutoScroll"), + currentTarget: target, + target: target, + bubbles: true, + cancelable: true, + defaultPrevented: true, + eventPhase: e.eventPhase, + isTrusted: true, + preventDefault: () => "not implemented for this event" ? false : false, + isDefaultPrevented: () => "not implemented for this event" ? false : false, + stopPropagation: () => "not implemented for this event" ? false : false, + isPropagationStopped: () => "not implemented for this event" ? false : false, + persist: emptyFunction, + timeStamp: e.timeStamp, + type: "dashDragAutoScroll" + } + }) + ); + + scrollAwaiter && clearTimeout(scrollAwaiter); + SnappingManager.GetIsDragging() && (scrollAwaiter = setTimeout(autoScrollHandler, 25)); + }; + scrollAwaiter && clearTimeout(scrollAwaiter); + scrollAwaiter = setTimeout(autoScrollHandler, 250); + } + const { thisX, thisY } = snapDrag(e, xFromLeft, yFromTop, xFromRight, yFromBottom); const moveX = thisX - lastX; diff --git a/src/client/util/GroupManager.scss b/src/client/util/GroupManager.scss index 544a79e98..51e4fa9e2 100644 --- a/src/client/util/GroupManager.scss +++ b/src/client/util/GroupManager.scss @@ -1,23 +1,47 @@ -@import "../views/globalCssVariables"; - .group-interface { - background-color: whitesmoke !important; - color: grey; - width: 450px; + width: 380px; height: 300px; .dialogue-box { - width: 450; - height: 300; + .group-create { + display: flex; + flex-direction: column; + height: 90%; + justify-content: space-between; + margin-left: 5px; + + input { + border-radius: 5px; + padding: 8px; + min-width: 100%; + border: 1px solid hsl(0, 0%, 80%); + outline: none; + height: 30; + + &:focus { + border: 2.5px solid #2684FF; + } + } + + p { + font-size: 20px; + text-align: left; + color: black; + } + + button { + align-self: flex-end; + } + } } + button { - background: $lighter-alt-accent; + align-self: center; outline: none; border-radius: 5px; border: 0px; - color: #fcfbf7; - text-transform: uppercase; + text-transform: none; letter-spacing: 2px; font-size: 75%; padding: 10px; @@ -36,12 +60,6 @@ border-radius: 10px; } - button { - width: 100%; - align-self: center; - background: $darker-alt-accent; - } - .delete-button { background: rgb(227, 86, 86); } @@ -55,33 +73,33 @@ } .group-heading { - letter-spacing: .5em; - } - - - .group-body { display: flex; - justify-content: space-between; - max-height: 80%; + align-items: center; + margin-bottom: 25px; - .group-create { - display: flex; - flex-direction: column; - flex-basis: 30%; - margin-left: 5px; + p { + font-size: 20px; + text-align: left; + margin-right: 15px; + color: black; + } + } - input { - border-radius: 5px; - border: none; - padding: 4px; - min-width: 100%; - margin: 4px 0 4px 0; - } + .main-container { + display: flex; + flex-direction: column; + .sort-groups { + text-align: left; + margin-left: 5; + cursor: pointer; } - .group-content { - padding-left: 1em; + .group-body { + justify-content: space-between; + height: 220; + background-color: #e8e8e8; + padding-right: 1em; justify-content: space-around; text-align: left; @@ -91,17 +109,18 @@ .group-row { display: flex; - position: relative; margin-bottom: 5px; - min-height: 40px; - border: 1px solid; - border-radius: 10px; + min-height: 30px; align-items: center; .group-name { - position: relative; max-width: 65%; - left: 10; + margin: 0 10; + color: black; + } + + .group-info { + cursor: pointer; } button { @@ -112,10 +131,6 @@ } } - ::placeholder { - color: $intermediate-color; - } - input { border-radius: 5px; border: none; @@ -126,11 +141,4 @@ } } - - h1 { - color: $dark-color; - text-transform: uppercase; - letter-spacing: 2px; - font-size: 120%; - } }
\ No newline at end of file diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx index 7c68fc2a0..2e5ecc543 100644 --- a/src/client/util/GroupManager.tsx +++ b/src/client/util/GroupManager.tsx @@ -3,7 +3,7 @@ import { observable, action, runInAction, computed } from "mobx"; import { SelectionManager } from "./SelectionManager"; import MainViewModal from "../views/MainViewModal"; import { observer } from "mobx-react"; -import { Doc, DocListCast, Opt } from "../../fields/Doc"; +import { Doc, DocListCast, Opt, DocListCastAsync } from "../../fields/Doc"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import * as fa from '@fortawesome/free-solid-svg-icons'; import { library } from "@fortawesome/fontawesome-svg-core"; @@ -12,10 +12,12 @@ import { Utils } from "../../Utils"; import * as RequestPromise from "request-promise"; import Select from 'react-select'; import "./GroupManager.scss"; -import { StrCast } from "../../fields/Types"; +import { StrCast, Cast } from "../../fields/Types"; import GroupMemberView from "./GroupMemberView"; +import { setGroups } from "../../fields/util"; +import { DocServer } from "../DocServer"; -library.add(fa.faWindowClose); +library.add(fa.faPlus, fa.faTimes, fa.faInfoCircle); export interface UserOptions { label: string; @@ -32,26 +34,44 @@ export default class GroupManager extends React.Component<{}> { @observable private users: string[] = []; // list of users populated from the database. @observable private selectedUsers: UserOptions[] | null = null; // list of users selected in the "Select users" dropdown. @observable currentGroup: Opt<Doc>; // the currently selected group. + @observable private createGroupModalOpen: boolean = false; private inputRef: React.RefObject<HTMLInputElement> = React.createRef(); // the ref for the input box. + private currentUserGroups: string[] = []; + @observable private buttonColour: "#979797" | "black" = "#979797"; + @observable private groupSort: "ascending" | "descending" | "none" = "none"; + constructor(props: Readonly<{}>) { super(props); GroupManager.Instance = this; } - // sets up the list of users componentDidMount() { - this.populateUsers().then(resolved => runInAction(() => this.users = resolved)); + this.populateUsers(); } /** - * Fetches the list of users stored on the database and @returns a list of the emails. + * Fetches the list of users stored on the database. */ populateUsers = async () => { - const userList: User[] = JSON.parse(await RequestPromise.get(Utils.prepend("/getUsers"))); - const currentUserIndex = userList.findIndex(user => user.email === Doc.CurrentUserEmail); - currentUserIndex !== -1 && userList.splice(currentUserIndex, 1); - return userList.map(user => user.email); + runInAction(() => this.users = []); + const userList = await RequestPromise.get(Utils.prepend("/getUsers")); + const raw = JSON.parse(userList) as User[]; + const evaluating = raw.map(async user => { + // const isCandidate = user.email !== Doc.CurrentUserEmail; + // if (isCandidate) { + const userDocument = await DocServer.GetRefField(user.userDocumentId); + if (userDocument instanceof Doc) { + const notificationDoc = await Cast(userDocument.rightSidebarCollection, Doc); + runInAction(() => { + if (notificationDoc instanceof Doc) { + this.users.push(user.email); + } + }); + } + // } + }); + return Promise.all(evaluating); } /** @@ -68,6 +88,15 @@ export default class GroupManager extends React.Component<{}> { open = () => { SelectionManager.DeselectAll(); this.isOpen = true; + this.populateUsers(); + DocListCastAsync(this.GroupManagerDoc?.data).then(groups => { + groups?.forEach(group => { + const members: string[] = JSON.parse(StrCast(group.members)); + if (members.includes(Doc.CurrentUserEmail)) this.currentUserGroups.push(StrCast(group.groupName)); + }); + + setGroups(this.currentUserGroups); + }); } /** @@ -77,6 +106,8 @@ export default class GroupManager extends React.Component<{}> { close = () => { this.isOpen = false; this.currentGroup = undefined; + // this.users = []; + this.createGroupModalOpen = false; } /** @@ -89,7 +120,8 @@ export default class GroupManager extends React.Component<{}> { /** * @returns a list of all group documents. */ - private getAllGroups(): Doc[] { + // private ? + getAllGroups(): Doc[] { const groupDoc = this.GroupManagerDoc; return groupDoc ? DocListCast(groupDoc.data) : []; } @@ -98,7 +130,8 @@ export default class GroupManager extends React.Component<{}> { * @returns a group document based on the group name. * @param groupName */ - private getGroup(groupName: string): Doc | undefined { + // private? + getGroup(groupName: string): Doc | undefined { const groupDoc = this.getAllGroups().find(group => group.groupName === groupName); return groupDoc; } @@ -123,6 +156,11 @@ export default class GroupManager extends React.Component<{}> { ); } + getGroupMembers(group: string | Doc): string[] { + if (group instanceof Doc) return JSON.parse(StrCast(group.members)) as string[]; + else return JSON.parse(StrCast(this.getGroup(group)!.members)) as string[]; + } + /** * @returns the members of the admin group. */ @@ -150,6 +188,10 @@ export default class GroupManager extends React.Component<{}> { groupDoc.groupName = groupName; groupDoc.owners = JSON.stringify([Doc.CurrentUserEmail]); groupDoc.members = JSON.stringify(memberEmails); + if (memberEmails.includes(Doc.CurrentUserEmail)) { + this.currentUserGroups.push(groupName); + setGroups(this.currentUserGroups); + } this.addGroup(groupDoc); } @@ -172,8 +214,16 @@ export default class GroupManager extends React.Component<{}> { deleteGroup(group: Doc): boolean { if (group) { if (this.GroupManagerDoc && this.hasEditAccess(group)) { + // TODO look at this later + // SharingManager.Instance.setInternalGroupSharing(group, "Not Shared"); Doc.RemoveDocFromList(this.GroupManagerDoc, "data", group); - SharingManager.Instance.setInternalGroupSharing(group, "Not Shared"); + SharingManager.Instance.removeGroup(group); + const members: string[] = JSON.parse(StrCast(group.members)); + if (members.includes(Doc.CurrentUserEmail)) { + const index = this.currentUserGroups.findIndex(groupName => groupName === group.groupName); + index !== -1 && this.currentUserGroups.splice(index, 1); + setGroups(this.currentUserGroups); + } if (group === this.currentGroup) { runInAction(() => this.currentGroup = undefined); } @@ -193,6 +243,7 @@ export default class GroupManager extends React.Component<{}> { const memberList: string[] = JSON.parse(StrCast(groupDoc.members)); !memberList.includes(email) && memberList.push(email); groupDoc.members = JSON.stringify(memberList); + SharingManager.Instance.shareWithAddedMember(groupDoc, email); } } @@ -205,8 +256,11 @@ export default class GroupManager extends React.Component<{}> { if (this.hasEditAccess(groupDoc)) { const memberList: string[] = JSON.parse(StrCast(groupDoc.members)); const index = memberList.indexOf(email); - index !== -1 && memberList.splice(index, 1); - groupDoc.members = JSON.stringify(memberList); + if (index !== -1) { + const user = memberList.splice(index, 1)[0]; + groupDoc.members = JSON.stringify(memberList); + SharingManager.Instance.removeMember(groupDoc, email); + } } } @@ -243,104 +297,123 @@ export default class GroupManager extends React.Component<{}> { this.createGroupDoc(this.inputRef.current.value, this.selectedUsers?.map(user => user.value)); this.selectedUsers = null; this.inputRef.current.value = ""; + this.buttonColour = "#979797"; } - /** - * A getter that @returns the interface rendered to view an individual group. - */ - private get editingInterface() { - const members: string[] = this.currentGroup ? JSON.parse(StrCast(this.currentGroup.members)) : []; - const options: UserOptions[] = this.currentGroup ? this.options.filter(option => !(JSON.parse(StrCast(this.currentGroup!.members)) as string[]).includes(option.value)) : []; - return (!this.currentGroup ? null : - <div className="editing-interface"> - <div className="editing-header"> - <b>{this.currentGroup.groupName}</b> - <div className={"close-button"} onClick={action(() => this.currentGroup = undefined)}> - <FontAwesomeIcon icon={fa.faWindowClose} size={"lg"} /> + private get groupCreationModal() { + const contents = ( + <div className="group-create"> + <div className="group-heading" style={{ marginBottom: 0 }}> + <p><b>New Group</b></p> + <div className={"close-button"} onClick={action(() => this.createGroupModalOpen = false)}> + <FontAwesomeIcon icon={fa.faTimes} color={"black"} size={"lg"} /> </div> - - {this.hasEditAccess(this.currentGroup) ? - <div className="group-buttons"> - <div className="add-member-dropdown"> - <Select - // isMulti={true} - isSearchable={true} - options={options} - onChange={selectedOption => this.addMemberToGroup(this.currentGroup!, (selectedOption as UserOptions).value)} - placeholder={"Add members"} - value={null} - closeMenuOnSelect={true} - /> - </div> - <button onClick={() => this.deleteGroup(this.currentGroup!)}>Delete group</button> - </div> : - null} - </div> - <div className="editing-contents"> - {members.map(member => ( - <div className="editing-row"> - <div className="user-email"> - {member} - </div> - {this.hasEditAccess(this.currentGroup!) ? <button onClick={() => this.removeMemberFromGroup(this.currentGroup!, member)}> Remove </button> : null} - </div> - ))} </div> + <input + className="group-input" + ref={this.inputRef} + onKeyDown={this.handleKeyDown} + type="text" + placeholder="Group name" + onChange={action(() => this.buttonColour = this.inputRef.current?.value ? "black" : "#979797")} /> + <Select + isMulti={true} + isSearchable={true} + options={this.options} + onChange={this.handleChange} + placeholder={"Select users"} + value={this.selectedUsers} + closeMenuOnSelect={false} + styles={{ + dropdownIndicator: (base, state) => ({ + ...base, + transition: '0.5s all ease', + transform: state.selectProps.menuIsOpen ? 'rotate(180deg)' : undefined + }), + multiValue: (base) => ({ + ...base, + maxWidth: "50%", + + '&:hover': { + maxWidth: "unset" + } + }) + }} + /> + <button onClick={this.createGroup} + style={{ background: this.buttonColour }} + disabled={this.buttonColour === "#979797"} + > + Create + </button> </div> ); + return ( + <MainViewModal + isDisplayed={this.createGroupModalOpen} + interactive={true} + contents={contents} + dialogueBoxStyle={{ width: "90%", height: "70%" }} + closeOnExternalClick={action(() => this.createGroupModalOpen = false)} + /> + ); } /** * A getter that @returns the main interface for the GroupManager. */ private get groupInterface() { + + const sortGroups = (d1: Doc, d2: Doc) => { + const g1 = StrCast(d1.groupName); + const g2 = StrCast(d2.groupName); + + return g1 < g2 ? -1 : g1 === g2 ? 0 : 1; + }; + + let groups = this.getAllGroups(); + groups = this.groupSort === "ascending" ? groups.sort(sortGroups) : this.groupSort === "descending" ? groups.sort(sortGroups).reverse() : groups; + return ( <div className="group-interface"> - {/* <MainViewModal - contents={this.editingInterface} - isDisplayed={this.currentGroup ? true : false} - interactive={true} - dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity} - overlayDisplayedOpacity={this.overlayOpacity} - /> */} + {this.groupCreationModal} {this.currentGroup ? <GroupMemberView group={this.currentGroup} - onCloseButtonClick={() => this.currentGroup = undefined} + onCloseButtonClick={action(() => this.currentGroup = undefined)} /> : null} <div className="group-heading"> - <h1>Groups</h1> + <p><b>Manage Groups</b></p> + <button onClick={action(() => this.createGroupModalOpen = true)}> + <FontAwesomeIcon icon={fa.faPlus} size={"sm"} /> Create Group + </button> <div className={"close-button"} onClick={this.close}> - <FontAwesomeIcon icon={fa.faWindowClose} size={"lg"} /> + <FontAwesomeIcon icon={fa.faTimes} color={"black"} size={"lg"} /> </div> </div> - <div className="group-body"> - <div className="group-create"> - <button onClick={this.createGroup}>Create group</button> - <input ref={this.inputRef} onKeyDown={this.handleKeyDown} type="text" placeholder="Group name" /> - <Select - isMulti={true} - isSearchable={true} - options={this.options} - onChange={this.handleChange} - placeholder={"Select users"} - value={this.selectedUsers} - closeMenuOnSelect={false} - /> + <div className="main-container"> + <div + className="sort-groups" + onClick={action(() => this.groupSort = this.groupSort === "ascending" ? "descending" : this.groupSort === "descending" ? "none" : "ascending")}> + Name {this.groupSort === "ascending" ? "↑" : this.groupSort === "descending" ? "↓" : ""} {/* → */} </div> - <div className="group-content"> - {this.getAllGroups().map(group => - <div className="group-row"> - <div className="group-name">{group.groupName}</div> - <button onClick={action(() => this.currentGroup = group)}> - {this.hasEditAccess(group) ? "Edit" : "View"} - </button> + <div className="group-body"> + {groups.map(group => + <div + className="group-row" + key={StrCast(group.groupName)} + > + <div className="group-name" >{group.groupName}</div> + <div className="group-info" onClick={action(() => this.currentGroup = group)}> + <FontAwesomeIcon icon={fa.faInfoCircle} color={"#e8e8e8"} size={"sm"} style={{ backgroundColor: "#1e89d7", borderRadius: "100%", border: "1px solid #1e89d7" }} /> + </div> </div> )} </div> </div> + </div> ); } @@ -351,8 +424,9 @@ export default class GroupManager extends React.Component<{}> { contents={this.groupInterface} isDisplayed={this.isOpen} interactive={true} - dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity} - overlayDisplayedOpacity={this.overlayOpacity} + dialogueBoxStyle={{ zIndex: 1002 }} + overlayStyle={{ zIndex: 1001 }} + closeOnExternalClick={this.close} /> ); } diff --git a/src/client/util/GroupMemberView.scss b/src/client/util/GroupMemberView.scss index 7833c485f..c609c5c7b 100644 --- a/src/client/util/GroupMemberView.scss +++ b/src/client/util/GroupMemberView.scss @@ -1,18 +1,17 @@ -@import "../views/globalCssVariables"; - .editing-interface { - background-color: whitesmoke !important; - color: grey; width: 100%; height: 100%; + hr { + margin-top: 20; + } + button { - background: $darker-alt-accent; outline: none; border-radius: 5px; border: 0px; color: #fcfbf7; - text-transform: uppercase; + text-transform: none; letter-spacing: 2px; font-size: 75%; padding: 10px; @@ -32,34 +31,69 @@ .editing-header { margin-bottom: 5; + .group-title { + font-weight: bold; + font-size: 15; + text-align: center; + border: none; + outline: none; + color: black; + margin-top: -5; + height: 20; + text-overflow: ellipsis; + + &:hover { + text-overflow: visible; + overflow-x: auto; + } + } + + .sort-emails { + float: left; + margin: -18 0 0 5; + cursor: pointer; + } + .group-buttons { display: flex; margin-top: 5; + margin-bottom: 25; .add-member-dropdown { - width: 100%; + width: 65%; margin: 0 5; + + input { + height: 30; + } } } } .editing-contents { overflow-y: auto; - // max-height: 67%; - height: 67%; + height: 65%; width: 100%; + color: black; + margin-top: -15px; .editing-row { display: flex; align-items: center; - // border: 1px solid; - // border-radius: 10px; + margin-bottom: 10px; + position: relative; .user-email { - // position: relative; min-width: 65%; word-break: break-all; padding: 0 5; + text-align: left; + } + + .remove-button { + position: absolute; + right: 10; + cursor: pointer; } } } diff --git a/src/client/util/GroupMemberView.tsx b/src/client/util/GroupMemberView.tsx index b2d75158e..f20670c4e 100644 --- a/src/client/util/GroupMemberView.tsx +++ b/src/client/util/GroupMemberView.tsx @@ -4,14 +4,14 @@ import { observer } from "mobx-react"; import GroupManager, { UserOptions } from "./GroupManager"; import { library } from "@fortawesome/fontawesome-svg-core"; import { StrCast } from "../../fields/Types"; -import { action } from "mobx"; +import { action, observable } from "mobx"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import * as fa from '@fortawesome/free-solid-svg-icons'; import Select from "react-select"; -import { Doc, Opt } from "../../fields/Doc"; +import { Doc } from "../../fields/Doc"; import "./GroupMemberView.scss"; -library.add(fa.faWindowClose); +library.add(fa.faTimes, fa.faTrashAlt); interface GroupMemberViewProps { group: Doc; @@ -21,17 +21,26 @@ interface GroupMemberViewProps { @observer export default class GroupMemberView extends React.Component<GroupMemberViewProps> { + @observable private memberSort: "ascending" | "descending" | "none" = "none"; + private get editingInterface() { - const members: string[] = this.props.group ? JSON.parse(StrCast(this.props.group.members)) : []; + let members: string[] = this.props.group ? JSON.parse(StrCast(this.props.group.members)) : []; + members = this.memberSort === "ascending" ? members.sort() : this.memberSort === "descending" ? members.sort().reverse() : members; + const options: UserOptions[] = this.props.group ? GroupManager.Instance.options.filter(option => !(JSON.parse(StrCast(this.props.group.members)) as string[]).includes(option.value)) : []; + return (!this.props.group ? null : <div className="editing-interface"> <div className="editing-header"> - <b>{this.props.group.groupName}</b> + <input + className="group-title" + value={StrCast(this.props.group.groupName)} + onChange={e => this.props.group.groupName = e.currentTarget.value} + > + </input> <div className={"memberView-closeButton"} onClick={action(this.props.onCloseButtonClick)}> - <FontAwesomeIcon icon={fa.faWindowClose} size={"lg"} /> + <FontAwesomeIcon icon={fa.faTimes} color={"black"} size={"lg"} /> </div> - {GroupManager.Instance.hasEditAccess(this.props.group) ? <div className="group-buttons"> <div className="add-member-dropdown"> @@ -42,19 +51,39 @@ export default class GroupMemberView extends React.Component<GroupMemberViewProp placeholder={"Add members"} value={null} closeMenuOnSelect={true} + styles={{ + dropdownIndicator: (base, state) => ({ + ...base, + transition: '0.5s all ease', + transform: state.selectProps.menuIsOpen ? 'rotate(180deg)' : undefined + }) + }} /> </div> <button onClick={() => GroupManager.Instance.deleteGroup(this.props.group)}>Delete group</button> </div> : null} + <div + className="sort-emails" + onClick={action(() => this.memberSort = this.memberSort === "ascending" ? "descending" : this.memberSort === "descending" ? "none" : "ascending")}> + Emails {this.memberSort === "ascending" ? "↑" : this.memberSort === "descending" ? "↓" : ""} {/* → */} + </div> </div> + <hr /> <div className="editing-contents"> {members.map(member => ( - <div className="editing-row"> + <div + className="editing-row" + key={member} + > <div className="user-email"> {member} </div> - {GroupManager.Instance.hasEditAccess(this.props.group) ? <button onClick={() => GroupManager.Instance.removeMemberFromGroup(this.props.group, member)}> Remove </button> : null} + {GroupManager.Instance.hasEditAccess(this.props.group) ? + <div className={"remove-button"} onClick={() => GroupManager.Instance.removeMemberFromGroup(this.props.group, member)}> + <FontAwesomeIcon icon={fa.faTrashAlt} size={"sm"} /> + </div> + : null} </div> ))} </div> @@ -68,6 +97,8 @@ export default class GroupMemberView extends React.Component<GroupMemberViewProp isDisplayed={true} interactive={true} contents={this.editingInterface} + dialogueBoxStyle={{ width: 400, height: 250 }} + closeOnExternalClick={this.props.onCloseButtonClick} />; } diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx index 02b444cd3..8b3614ea7 100644 --- a/src/client/util/InteractionUtils.tsx +++ b/src/client/util/InteractionUtils.tsx @@ -94,6 +94,7 @@ export namespace InteractionUtils { export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number, color: string, width: number, strokeWidth: number, bezier: string, fill: string, arrowStart: string, arrowEnd: string, dash: string, scalex: number, scaley: number, shape: string, pevents: string, drawHalo: boolean, nodefs: boolean) { + let pts: { X: number; Y: number; }[] = []; if (shape) { //if any of the shape are true pts = makePolygon(shape, points); @@ -111,7 +112,11 @@ export namespace InteractionUtils { } } } else { - pts = points; + pts = points.slice(); + // bcz: Ugh... this is ugly, but shapes apprently have an extra point added that is = (p[0].x,p[0].y+1) as some sort of flag. need to remove it here. + if (pts.length > 2 && pts[pts.length - 2].X === pts[0].X && pts[pts.length - 2].Y === pts[0].Y) { + pts.pop(); + } } const strpts = pts.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${(pt.X - left - width / 2) * scalex + width / 2}, @@ -119,15 +124,15 @@ export namespace InteractionUtils { const dashArray = String(Number(width) * Number(dash)); const defGuid = Utils.GenerateGuid(); const arrowDim = Math.max(0.5, 8 / Math.log(Math.max(2, strokeWidth))); - return (<svg fill={fill === "none" ? color : fill}> {/* setting the svg fill sets the arrowhead fill */} + return (<svg fill={color}> {/* setting the svg fill sets the arrowStart fill */} {nodefs ? (null) : <defs> {arrowStart !== "dot" && arrowEnd !== "dot" ? (null) : <marker id={`dot${defGuid}`} orient="auto" overflow="visible"> <circle r={1} fill="context-stroke" /> </marker>} - {arrowStart !== "arrowHead" && arrowEnd !== "arrowHead" ? (null) : <marker id={`arrowHead${defGuid}`} orient="auto" overflow="visible" refX="1.6" refY="0" markerWidth="10" markerHeight="7"> + {arrowStart !== "arrow" && arrowEnd !== "arrow" ? (null) : <marker id={`arrowStart${defGuid}`} orient="auto" overflow="visible" refX="1.6" refY="0" markerWidth="10" markerHeight="7"> <polygon points={`${arrowDim} ${-Math.max(1, arrowDim / 2)}, ${arrowDim} ${Math.max(1, arrowDim / 2)}, -1 0`} /> </marker>} - {arrowStart !== "arrowEnd" && arrowEnd !== "arrowEnd" ? (null) : <marker id={`arrowEnd${defGuid}`} orient="auto" overflow="visible" refX="1.6" refY="0" markerWidth="10" markerHeight="7"> + {arrowStart !== "arrow" && arrowEnd !== "arrow" ? (null) : <marker id={`arrowEnd${defGuid}`} orient="auto" overflow="visible" refX="1.6" refY="0" markerWidth="10" markerHeight="7"> <polygon points={`${2 - arrowDim} ${-Math.max(1, arrowDim / 2)}, ${2 - arrowDim} ${Math.max(1, arrowDim / 2)}, 3 0`} /> </marker>} </defs>} @@ -136,7 +141,7 @@ export namespace InteractionUtils { points={strpts} style={{ filter: drawHalo ? "url(#inkSelectionHalo)" : undefined, - fill, + fill: fill ? fill : "transparent", opacity: strokeWidth !== width ? 0.5 : undefined, pointerEvents: pevents as any, stroke: color ?? "rgb(0, 0, 0)", @@ -145,8 +150,8 @@ export namespace InteractionUtils { strokeLinecap: "round", strokeDasharray: dashArray }} - markerStart={`url(#${arrowStart + defGuid})`} - markerEnd={`url(#${arrowEnd + defGuid})`} + markerStart={`url(#${arrowStart + "Start" + defGuid})`} + markerEnd={`url(#${arrowEnd + "End" + defGuid})`} /> </svg>); @@ -155,8 +160,7 @@ export namespace InteractionUtils { // export function makeArrow() { // return ( // InkOptionsMenu.Instance.getColors().map(color => { - // const id1 = "arrowHeadTest" + color; - // console.log(color); + // const id1 = "arrowStartTest" + color; // <marker id={id1} orient="auto" overflow="visible" refX="0" refY="1" markerWidth="10" markerHeight="7"> // <polygon points="0 0, 3 1, 0 2" fill={"#" + color} /> // </marker>; @@ -378,7 +382,6 @@ export namespace InteractionUtils { // let dist12 = TwoPointEuclidist(pt1, pt2); // let dist23 = TwoPointEuclidist(pt2, pt3); // let dist13 = TwoPointEuclidist(pt1, pt3); - // console.log(`distances: ${dist12}, ${dist23}, ${dist13}`); // let dist12close = dist12 < leniency; // let dist23close = dist23 < leniency; // let dist13close = dist13 < leniency; diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index 6da581f35..223f0e7ef 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -2,7 +2,6 @@ import { Doc, DocListCast, Opt } from "../../fields/Doc"; import { List } from "../../fields/List"; import { listSpec } from "../../fields/Schema"; import { Cast, StrCast } from "../../fields/Types"; -import { Scripting } from "./Scripting"; /* * link doc: @@ -24,12 +23,12 @@ export class LinkManager { private static _instance: LinkManager; - public static currentLink: Opt<Doc>; public static get Instance(): LinkManager { return this._instance || (this._instance = new this()); } + private constructor() { } @@ -61,12 +60,17 @@ export class LinkManager { } // finds all links that contain the given anchor - public getAllRelatedLinks(anchor: Doc): Doc[] { + public getAllDirectLinks(anchor: Doc): Doc[] { const related = LinkManager.Instance.getAllLinks().filter(link => { const protomatch1 = Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, null)); const protomatch2 = Doc.AreProtosEqual(anchor, Cast(link.anchor2, Doc, null)); return protomatch1 || protomatch2 || Doc.AreProtosEqual(link, anchor); }); + return related; + } + // finds all links that contain the given anchor + public getAllRelatedLinks(anchor: Doc): Doc[] { + const related = LinkManager.Instance.getAllDirectLinks(anchor); DocListCast(anchor[Doc.LayoutFieldKey(anchor) + "-annotations"]).map(anno => { related.push(...LinkManager.Instance.getAllRelatedLinks(anno)); }); @@ -203,7 +207,4 @@ export class LinkManager { if (Doc.AreProtosEqual(anchor, a2)) return a1; if (Doc.AreProtosEqual(anchor, linkDoc)) return linkDoc; } -} - -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 +}
\ No newline at end of file diff --git a/src/client/util/ScriptManager.ts b/src/client/util/ScriptManager.ts index 785e63d9a..94806a7ba 100644 --- a/src/client/util/ScriptManager.ts +++ b/src/client/util/ScriptManager.ts @@ -32,24 +32,17 @@ export class ScriptManager { } 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)); } @@ -70,7 +63,6 @@ export class ScriptManager { 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(); @@ -82,8 +74,6 @@ export class ScriptManager { const f = new Function(paramNames, StrCast(scriptDoc.script)); - console.log(scriptDoc.script); - Object.defineProperty(f, 'name', { value: StrCast(scriptDoc.name), writable: false }); let parameters = "("; diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index e6cf50de3..f1e6155d2 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -10,8 +10,6 @@ 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; @@ -95,10 +93,10 @@ export namespace Scripting { export function removeGlobal(name: string) { if (getGlobals().includes(name)) { delete _scriptingGlobals[name]; - if (_scriptingDescriptions[name]){ + if (_scriptingDescriptions[name]) { delete _scriptingDescriptions[name]; } - if (_scriptingParams[name]){ + if (_scriptingParams[name]) { delete _scriptingParams[name]; } return true; @@ -123,11 +121,11 @@ export namespace Scripting { return _scriptingGlobals; } - export function getDescriptions(){ + export function getDescriptions() { return _scriptingDescriptions; } - export function getParameters(){ + export function getParameters() { return _scriptingParams; } } diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts index 1ac68480e..0a01d8ac7 100644 --- a/src/client/util/SearchUtil.ts +++ b/src/client/util/SearchUtil.ts @@ -128,7 +128,6 @@ export namespace SearchUtil { }); const result: IdSearchResult = JSON.parse(response); const { ids, numFound, highlighting } = result; - //console.log(ids.length); const docMap = await DocServer.GetRefFields(ids); const docs: Doc[] = []; for (const id of ids) { diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index 024532f90..20d881961 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -14,6 +14,7 @@ export namespace SelectionManager { SelectedDocuments: ObservableMap<DocumentView, boolean> = new ObservableMap(); @action SelectDoc(docView: DocumentView, ctrlPressed: boolean): void { + // if doc is not in SelectedDocuments, add it if (!manager.SelectedDocuments.get(docView)) { if (!ctrlPressed) { @@ -21,7 +22,6 @@ export namespace SelectionManager { } manager.SelectedDocuments.set(docView, true); - // console.log(manager.SelectedDocuments); docView.props.whenActiveChanged(true); } else if (!ctrlPressed && Array.from(manager.SelectedDocuments.entries()).length > 1) { Array.from(manager.SelectedDocuments.keys()).map(dv => dv !== docView && dv.props.whenActiveChanged(false)); diff --git a/src/client/util/SettingsManager.scss b/src/client/util/SettingsManager.scss index 13c65042c..c1627e69f 100644 --- a/src/client/util/SettingsManager.scss +++ b/src/client/util/SettingsManager.scss @@ -52,11 +52,12 @@ .settings-body { display: flex; justify-content: space-between; + margin-top: -10; .settings-type { display: flex; flex-direction: column; - flex-basis: 30%; + flex-basis: 45%; } @@ -105,6 +106,7 @@ text-transform: uppercase; letter-spacing: 2px; font-size: 120%; + margin-top: 0; } .container { diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index 9888cce48..90d59aa51 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -11,8 +11,12 @@ import { Networking } from "../Network"; import { CurrentUserUtils } from "./CurrentUserUtils"; import { Utils } from "../../Utils"; import { Doc } from "../../fields/Doc"; +import GroupManager from "./GroupManager"; +import HypothesisAuthenticationManager from "../apis/HypothesisAuthenticationManager"; +import GoogleAuthenticationManager from "../apis/GoogleAuthenticationManager"; +import { togglePlaygroundMode } from "../../fields/util"; -library.add(fa.faWindowClose); +library.add(fa.faTimes); @observer export default class SettingsManager extends React.Component<{}> { @@ -23,6 +27,7 @@ export default class SettingsManager extends React.Component<{}> { @observable private settingsContent = "password"; @observable private errorText = ""; @observable private successText = ""; + @observable private playgroundMode = false; private curr_password_ref = React.createRef<HTMLInputElement>(); private new_password_ref = React.createRef<HTMLInputElement>(); private new_confirm_ref = React.createRef<HTMLInputElement>(); @@ -83,6 +88,20 @@ export default class SettingsManager extends React.Component<{}> { noviceToggle = (event: any) => { Doc.UserDoc().noviceMode = !Doc.UserDoc().noviceMode; } + @action + googleAuthorize = (event: any) => { + GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(true); + } + @action + hypothesisAuthorize = (event: any) => { + HypothesisAuthenticationManager.Instance.fetchAccessToken(true); + } + + @action + togglePlaygroundMode = () => { + togglePlaygroundMode(); + this.playgroundMode = !this.playgroundMode; + } private get settingsInterface() { return ( @@ -90,13 +109,17 @@ export default class SettingsManager extends React.Component<{}> { <div className="settings-heading"> <h1>settings</h1> <div className={"close-button"} onClick={this.close}> - <FontAwesomeIcon icon={fa.faWindowClose} size={"lg"} /> + <FontAwesomeIcon icon={fa.faTimes} color="black" size={"lg"} /> </div> </div> <div className="settings-body"> <div className="settings-type"> <button onClick={this.onClick} value="password">reset password</button> - <button onClick={this.noviceToggle} value="data">{`toggle ${Doc.UserDoc().noviceMode ? "developer" : "novice"} mode`}</button> + <button onClick={this.noviceToggle} value="data">{`Set ${Doc.UserDoc().noviceMode ? "developer" : "novice"} mode`}</button> + <button onClick={this.togglePlaygroundMode}>{`${this.playgroundMode ? "Disable" : "Enable"} playground mode`}</button> + <button onClick={this.googleAuthorize} value="data">{`Link to Google`}</button> + <button onClick={this.hypothesisAuthorize} value="data">{`Link to Hypothes.is`}</button> + <button onClick={() => GroupManager.Instance.open()}>Manage groups</button> <button onClick={() => window.location.assign(Utils.prepend("/logout"))}> {CurrentUserUtils.GuestWorkspace ? "Exit" : "Log Out"} </button> @@ -132,8 +155,7 @@ export default class SettingsManager extends React.Component<{}> { contents={this.settingsInterface} isDisplayed={this.isOpen} interactive={true} - dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity} - overlayDisplayedOpacity={this.overlayOpacity} + closeOnExternalClick={this.close} /> ); } diff --git a/src/client/util/SharingManager.scss b/src/client/util/SharingManager.scss index fcbc05f8a..130785672 100644 --- a/src/client/util/SharingManager.scss +++ b/src/client/util/SharingManager.scss @@ -1,29 +1,112 @@ -@import "../views/globalCssVariables"; - .sharing-interface { - display: flex; - flex-direction: column; - width: 730px; - - .dialogue-box { - width: 450; - height: 300; - } + width: 600px; + height: 360px; .overlay { transform: translate(-20px, -20px); } + select { + text-align: justify; + text-align-last: end + } + .sharing-contents { display: flex; + flex-direction: column; + + .close-button { + position: absolute; + right: 1em; + top: 1em; + cursor: pointer; + z-index: 999; + } + + .share-setup { + display: flex; + margin-bottom: 20px; + align-items: center; + height: 36; + + .user-search { + width: 90%; + + input { + height: 30; + } + } + + .permissions-select { + z-index: 1; + margin-left: -100; + border: none; + outline: none; + text-align: justify; // for Edge + text-align-last: end; + } + + .share-button { + height: 105%; + margin-left: 2%; + background-color: #979797; + } + } + + .main-container { + display: flex; + margin-top: -10px; + + .individual-container, + .group-container { + width: 50%; + display: flex; + flex-direction: column; + + .user-sort { + text-align: left; + margin-left: 10; + cursor: pointer; + } + + .share-title { + margin-top: 20px; + margin-bottom: 20px; + } + + .groups-list, + .users-list { + font-style: italic; + background: #e8e8e8; + padding-left: 10px; + padding-right: 10px; + overflow-y: scroll; + overflow-x: hidden; + text-align: left; + display: flex; + align-content: center; + align-items: center; + text-align: center; + justify-content: center; + color: black; + height: 250px; + margin: 0 2; + + + .none { + font-style: italic; + + } + } + } + } button { - background: $darker-alt-accent; outline: none; border-radius: 5px; border: 0px; color: #fcfbf7; - text-transform: uppercase; + text-transform: none; letter-spacing: 2px; font-size: 75%; padding: 0 10; @@ -31,37 +114,6 @@ transition: transform 0.2s; height: 25; } - - .individual-container, - .group-container { - width: 50%; - - .share-groups, - .share-individual { - margin-top: 20px; - margin-bottom: 20px; - } - - .groups-list, - .users-list { - font-style: italic; - background: white; - border: 1px solid black; - padding-left: 10px; - padding-right: 10px; - overflow-y: scroll; - overflow-x: hidden; - text-align: left; - display: flex; - align-content: center; - align-items: center; - text-align: center; - justify-content: center; - color: red; - height: 150px; - margin: 0 2; - } - } } .focus-span { @@ -69,11 +121,10 @@ } p { - font-size: 15px; + font-size: 20px; text-align: left; - font-style: italic; - padding: 0; margin: 0 0 20px 0; + color: black; } .hr-substitute { @@ -108,30 +159,41 @@ -moz-user-select: none; -ms-user-select: none; user-select: none; - width: 700px; - min-width: 700px; - max-width: 700px; + width: 100%; text-align: left; font-style: normal; font-size: 14; font-weight: normal; padding: 0; - align-items: baseline; + align-items: center; + + .group-info { + cursor: pointer; + } + + &:hover .padding { + white-space: unset; + } .padding { padding: 0 10px 0 0; color: black; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + max-width: 40%; } .permissions-dropdown { - outline: none; + border: none; height: 25; + background-color: #e8e8e8; } .edit-actions { display: flex; position: absolute; - right: 51.5%; + right: -10; } } @@ -171,18 +233,4 @@ padding-top: 12px; } } - - .close-button { - border-radius: 5px; - margin-top: 20px; - padding: 10px 0; - background: aliceblue; - transition: 0.5s ease all; - border: 1px solid; - border-color: aliceblue; - } - - .close-button:hover { - border-color: black; - } }
\ No newline at end of file diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index 127ee33ce..9c857a7c0 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -1,15 +1,13 @@ import { observable, runInAction, action } from "mobx"; import * as React from "react"; import MainViewModal from "../views/MainViewModal"; -import { Doc, Opt, DocCastAsync } from "../../fields/Doc"; +import { Doc, Opt, DocListCastAsync } from "../../fields/Doc"; import { DocServer } from "../DocServer"; import { Cast, StrCast } from "../../fields/Types"; import * as RequestPromise from "request-promise"; import { Utils } from "../../Utils"; import "./SharingManager.scss"; -import { Id } from "../../fields/FieldSymbols"; import { observer } from "mobx-react"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { library } from '@fortawesome/fontawesome-svg-core'; import * as fa from '@fortawesome/free-solid-svg-icons'; import { DocumentView } from "../views/nodes/DocumentView"; @@ -17,10 +15,14 @@ import { SelectionManager } from "./SelectionManager"; import { DocumentManager } from "./DocumentManager"; import { CollectionView } from "../views/collections/CollectionView"; import { DictationOverlay } from "../views/DictationOverlay"; -import GroupManager from "./GroupManager"; +import GroupManager, { UserOptions } from "./GroupManager"; import GroupMemberView from "./GroupMemberView"; +import Select from "react-select"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { List } from "../../fields/List"; +import { distributeAcls } from "../../fields/util"; -library.add(fa.faCopy); +library.add(fa.faCopy, fa.faTimes); export interface User { email: string; @@ -28,35 +30,23 @@ export interface User { } export enum SharingPermissions { - None = "Not Shared", - View = "Can View", + Edit = "Can Edit", Add = "Can Add", - Edit = "Can Edit" + View = "Can View", + None = "Not Shared" } -const ColorMapping = new Map<string, string>([ - [SharingPermissions.None, "red"], - [SharingPermissions.View, "maroon"], - [SharingPermissions.Add, "blue"], - [SharingPermissions.Edit, "green"] -]); - -const HierarchyMapping = new Map<string, string>([ - [SharingPermissions.None, "0"], - [SharingPermissions.View, "1"], - [SharingPermissions.Add, "2"], - [SharingPermissions.Edit, "3"], - - ["0", SharingPermissions.None], - ["1", SharingPermissions.View], - ["2", SharingPermissions.Add], - ["3", SharingPermissions.Edit] +interface GroupOptions { + label: string; + options: UserOptions[]; +} -]); +// const SharingKey = "sharingPermissions"; +// const PublicKey = "publicLinkPermissions"; +// const DefaultColor = "black"; -const SharingKey = "sharingPermissions"; -const PublicKey = "publicLinkPermissions"; -const DefaultColor = "black"; +const groupType = "!groupType/"; +const indType = "!indType/"; interface ValidatedUser { user: User; @@ -70,17 +60,20 @@ export default class SharingManager extends React.Component<{}> { public static Instance: SharingManager; @observable private isOpen = false; @observable private users: ValidatedUser[] = []; - @observable private groups: Doc[] = []; @observable private targetDoc: Doc | undefined; @observable private targetDocView: DocumentView | undefined; - @observable private copied = false; + // @observable private copied = false; @observable private dialogueBoxOpacity = 1; @observable private overlayOpacity = 0.4; - @observable private groupToView: Opt<Doc>; + @observable private selectedUsers: UserOptions[] | null = null; + @observable private permissions: SharingPermissions = SharingPermissions.Edit; + @observable private individualSort: "ascending" | "descending" | "none" = "none"; + @observable private groupSort: "ascending" | "descending" | "none" = "none"; - private get linkVisible() { - return this.sharingDoc ? this.sharingDoc[PublicKey] !== SharingPermissions.None : false; - } + + // private get linkVisible() { + // return this.sharingDoc ? this.sharingDoc[PublicKey] !== SharingPermissions.None : false; + // } public open = (target: DocumentView) => { SelectionManager.DeselectAll(); @@ -89,32 +82,21 @@ export default class SharingManager extends React.Component<{}> { this.targetDoc = target.props.Document; DictationOverlay.Instance.hasActiveModal = true; this.isOpen = true; - if (!this.sharingDoc) { - this.sharingDoc = new Doc; - } + this.permissions = SharingPermissions.Edit; })); - runInAction(() => this.groups = GroupManager.Instance.getAllGroupsCopy()); } public close = action(() => { this.isOpen = false; this.users = []; setTimeout(action(() => { - this.copied = false; + // this.copied = false; DictationOverlay.Instance.hasActiveModal = false; this.targetDoc = undefined; }), 500); }); - private get sharingDoc() { - return this.targetDoc ? Cast(this.targetDoc[SharingKey], Doc) as Doc : undefined; - } - - private set sharingDoc(value: Doc | undefined) { - this.targetDoc && (this.targetDoc[SharingKey] = value); - } - constructor(props: {}) { super(props); SharingManager.Instance = this; @@ -142,93 +124,113 @@ export default class SharingManager extends React.Component<{}> { setInternalGroupSharing = (group: Doc, permission: string) => { const members: string[] = JSON.parse(StrCast(group.members)); - const users: ValidatedUser[] = this.users.filter(user => members.includes(user.user.email)); + const users: ValidatedUser[] = this.users.filter(({ user: { email } }) => members.includes(email)); - const sharingDoc = this.sharingDoc!; - if (permission === SharingPermissions.None) { - const metadata = sharingDoc[StrCast(group.groupName)]; - if (metadata) sharingDoc[StrCast(group.groupName)] = undefined; - } - else { - sharingDoc[StrCast(group.groupName)] = permission; - } + const target = this.targetDoc!; + const ACL = `ACL-${StrCast(group.groupName)}`; + // fix this - not needed (here and setinternalsharing and removegroup) + // target[ACL] = permission; + // Doc.GetProto(target)[ACL] = permission; + + distributeAcls(ACL, permission as SharingPermissions, this.targetDoc!); + + group.docsShared ? DocListCastAsync(group.docsShared).then(resolved => Doc.IndexOf(target, resolved!) === -1 && (group.docsShared as List<Doc>).push(target)) : group.docsShared = new List<Doc>([target]); - users.forEach(user => { - this.setInternalSharing(user, permission, group); + users.forEach(({ notificationDoc }) => { + DocListCastAsync(notificationDoc[storage]).then(resolved => { + if (permission !== SharingPermissions.None) Doc.IndexOf(target, resolved!) === -1 && Doc.AddDocToList(notificationDoc, storage, target); + else Doc.IndexOf(target, resolved!) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, target); + }); }); } - setInternalSharing = async (recipient: ValidatedUser, state: string, group: Opt<Doc>) => { - const { user, notificationDoc } = recipient; - const target = this.targetDoc!; - const manager = this.sharingDoc!; - const key = user.userDocumentId; - - let metadata = await DocCastAsync(manager[key]); - const permissions: { [key: string]: number } = metadata?.permissions ? JSON.parse(StrCast(metadata.permissions)) : {}; - permissions[StrCast(group ? group.groupName : Doc.CurrentUserEmail)] = parseInt(HierarchyMapping.get(state)!); - const max = Math.max(...Object.values(permissions)); - - // let max = 0; - // const keys: string[] = []; - // for (const [key, value] of Object.entries(permissions)) { - // if (value === max && max !== 0) { - // keys.push(key); - // } - // else if (value > max) { - // keys.splice(0, keys.length); - // keys.push(key); - // max = value; - // } - // } - - switch (max) { - case 0: - if (metadata) { - const sharedAlias = (await DocCastAsync(metadata.sharedAlias))!; - Doc.RemoveDocFromList(notificationDoc, storage, sharedAlias); - manager[key] = undefined; - } - break; - - case 1: case 2: case 3: - if (!metadata) { - metadata = new Doc; - const sharedAlias = Doc.MakeAlias(target); - Doc.AddDocToList(notificationDoc, storage, sharedAlias); - metadata.sharedAlias = sharedAlias; - manager[key] = metadata; - } - metadata.permissions = JSON.stringify(permissions); - // metadata.usersShared = JSON.stringify(keys); - break; - } + shareWithAddedMember = (group: Doc, emailId: string) => { + const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!; - if (metadata) metadata.maxPermission = HierarchyMapping.get(`${max}`); + if (group.docsShared) { + DocListCastAsync(group.docsShared).then(docsShared => { + docsShared?.forEach(doc => { + DocListCastAsync(user.notificationDoc[storage]).then(resolved => Doc.IndexOf(doc, resolved!) === -1 && Doc.AddDocToList(user.notificationDoc, storage, doc)); + }); + }); + } } - private setExternalSharing = (state: string) => { - const sharingDoc = this.sharingDoc; - if (!sharingDoc) { - return; + removeMember = (group: Doc, emailId: string) => { + const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!; + + if (group.docsShared) { + DocListCastAsync(group.docsShared).then(docsShared => { + docsShared?.forEach(doc => { + DocListCastAsync(user.notificationDoc[storage]).then(resolved => Doc.IndexOf(doc, resolved!) !== -1 && Doc.RemoveDocFromList(user.notificationDoc, storage, doc)); + }); + }); } - sharingDoc[PublicKey] = state; } - private get sharingUrl() { - if (!this.targetDoc) { - return undefined; + removeGroup = (group: Doc) => { + if (group.docsShared) { + DocListCastAsync(group.docsShared).then(resolved => { + resolved?.forEach(doc => { + const ACL = `ACL-${StrCast(group.groupName)}`; + // doc[ACL] = doc[DataSym][ACL] = "Not Shared"; + + distributeAcls(ACL, SharingPermissions.None, doc); + + const members: string[] = JSON.parse(StrCast(group.members)); + const users: ValidatedUser[] = this.users.filter(({ user: { email } }) => members.includes(email)); + + users.forEach(({ notificationDoc }) => Doc.RemoveDocFromList(notificationDoc, storage, doc)); + }); + + }); } - const baseUrl = Utils.prepend("/doc/" + this.targetDoc[Id]); - return `${baseUrl}?sharing=true`; } - copy = action(() => { - if (this.sharingUrl) { - Utils.CopyText(this.sharingUrl); - this.copied = true; + // @action + setInternalSharing = (recipient: ValidatedUser, permission: string) => { + const { user, notificationDoc } = recipient; + const target = this.targetDoc!; + const key = user.email.replace('.', '_'); + const ACL = `ACL-${key}`; + + distributeAcls(ACL, permission as SharingPermissions, this.targetDoc!); + + if (permission !== SharingPermissions.None) { + DocListCastAsync(notificationDoc[storage]).then(resolved => { + Doc.IndexOf(target, resolved!) === -1 && Doc.AddDocToList(notificationDoc, storage, target); + }); } - }); + else { + DocListCastAsync(notificationDoc[storage]).then(resolved => { + Doc.IndexOf(target, resolved!) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, target); + }); + } + } + + + // private setExternalSharing = (permission: string) => { + // const sharingDoc = this.sharingDoc; + // if (!sharingDoc) { + // return; + // } + // sharingDoc[PublicKey] = permission; + // } + + // private get sharingUrl() { + // if (!this.targetDoc) { + // return undefined; + // } + // const baseUrl = Utils.prepend("/doc/" + this.targetDoc[Id]); + // return `${baseUrl}?sharing=true`; + // } + + // copy = action(() => { + // if (this.sharingUrl) { + // Utils.CopyText(this.sharingUrl); + // this.copied = true; + // } + // }); private get sharingOptions() { return Object.values(SharingPermissions).map(permission => { @@ -270,33 +272,141 @@ export default class SharingManager extends React.Component<{}> { ); } - private computePermissions = (userKey: string) => { - const sharingDoc = this.sharingDoc; - if (!sharingDoc) { - return SharingPermissions.None; - } - const metadata = sharingDoc[userKey] as Doc | string; - if (!metadata) { - return SharingPermissions.None; - } - return StrCast(metadata instanceof Doc ? metadata.maxPermission : metadata, SharingPermissions.None); + @action + handleUsersChange = (selectedOptions: any) => { + this.selectedUsers = selectedOptions as UserOptions[]; + } + + @action + handlePermissionsChange = (event: React.ChangeEvent<HTMLSelectElement>) => { + this.permissions = event.currentTarget.value as SharingPermissions; + } + + @action + share = () => { + this.selectedUsers?.forEach(user => { + if (user.value.includes(indType)) { + this.setInternalSharing(this.users.find(u => u.user.email === user.label)!, this.permissions); + } + else { + this.setInternalGroupSharing(GroupManager.Instance.getGroup(user.label)!, this.permissions); + } + }); + this.selectedUsers = null; + } + + sortUsers = (u1: ValidatedUser, u2: ValidatedUser) => { + const { email: e1 } = u1.user; + const { email: e2 } = u2.user; + return e1 < e2 ? -1 : e1 === e2 ? 0 : 1; + } + + sortGroups = (group1: Doc, group2: Doc) => { + const g1 = StrCast(group1.groupName); + const g2 = StrCast(group2.groupName); + return g1 < g2 ? -1 : g1 === g2 ? 0 : 1; } private get sharingInterface() { - const existOtherUsers = this.users.length > 0; - const existGroups = this.groups.length > 0; + const groupList = GroupManager.Instance?.getAllGroups() || []; + + const sortedUsers = this.users.sort(this.sortUsers) + .map(({ user: { email } }) => ({ label: email, value: indType + email })); + const sortedGroups = groupList.sort(this.sortGroups) + .map(({ groupName }) => ({ label: StrCast(groupName), value: groupType + StrCast(groupName) })); + + const options: GroupOptions[] = GroupManager.Instance ? + [ + { + label: 'Individuals', + options: sortedUsers + }, + { + label: 'Groups', + options: sortedGroups + } + ] + : []; + + const users = this.individualSort === "ascending" ? this.users.sort(this.sortUsers) : this.individualSort === "descending" ? this.users.sort(this.sortUsers).reverse() : this.users; + const groups = this.groupSort === "ascending" ? groupList.sort(this.sortGroups) : this.groupSort === "descending" ? groupList.sort(this.sortGroups).reverse() : groupList; + + const userListContents: (JSX.Element | null)[] = users.map(({ user, notificationDoc }) => { + const userKey = user.email.replace('.', '_'); + const permissions = StrCast(this.targetDoc?.[`ACL-${userKey}`], SharingPermissions.None); + + return permissions === SharingPermissions.None || user.email === this.targetDoc?.author ? null : ( + <div + key={userKey} + className={"container"} + > + <span className={"padding"}>{user.email}</span> + <div className="edit-actions"> + <select + className={"permissions-dropdown"} + value={permissions} + onChange={e => this.setInternalSharing({ user, notificationDoc }, e.currentTarget.value)} + > + {this.sharingOptions} + </select> + </div> + </div> + ); + }); - // const manager = this.sharingDoc!; + userListContents.unshift( + ( + <div + key={"owner"} + className={"container"} + > + <span className={"padding"}>{this.targetDoc?.author}</span> + <div className="edit-actions"> + <div className={"permissions-dropdown"}> + Owner + </div> + </div> + </div> + ) + ); + + const groupListContents = groups.map(group => { + const permissions = StrCast(this.targetDoc?.[`ACL-${StrCast(group.groupName)}`], SharingPermissions.None); + + return permissions === SharingPermissions.None ? null : ( + <div + key={StrCast(group.groupName)} + className={"container"} + > + <div className={"padding"}>{group.groupName}</div> + <div className="group-info" onClick={action(() => GroupManager.Instance.currentGroup = group)}> + <FontAwesomeIcon icon={fa.faInfoCircle} color={"#e8e8e8"} size={"sm"} style={{ backgroundColor: "#1e89d7", borderRadius: "100%", border: "1px solid #1e89d7" }} /> + </div> + <div className="edit-actions"> + <select + className={"permissions-dropdown"} + value={permissions} + onChange={e => this.setInternalGroupSharing(group, e.currentTarget.value)} + > + {this.sharingOptions} + </select> + </div> + </div> + ); + }); + + const displayUserList = !userListContents?.every(user => user === null); + const displayGroupList = !groupListContents?.every(group => group === null); return ( <div className={"sharing-interface"}> - {this.groupToView ? + {GroupManager.Instance?.currentGroup ? <GroupMemberView - group={this.groupToView} - onCloseButtonClick={action(() => this.groupToView = undefined)} + group={GroupManager.Instance.currentGroup} + onCloseButtonClick={action(() => GroupManager.Instance.currentGroup = undefined)} /> : null} - <p className={"share-link"}>Manage the public link to {this.focusOn("this document...")}</p> + {/* <p className={"share-link"}>Manage the public link to {this.focusOn("this document...")}</p> {!this.linkVisible ? (null) : <div className={"link-container"}> <div className={"link-box"} onClick={this.copy}>{this.sharingUrl}</div> @@ -325,86 +435,81 @@ export default class SharingManager extends React.Component<{}> { {this.sharingOptions} </select> </div> - <div className={"hr-substitute"} /> + <div className={"hr-substitute"} /> */} <div className="sharing-contents"> - <div className={"individual-container"}> - <p className={"share-individual"}>Privately share {this.focusOn("this document")} with an individual...</p> - <div className={"users-list"} style={{ display: existOtherUsers ? "block" : "flex", minHeight: existOtherUsers ? undefined : 150 }}>{/*200*/} - {!existOtherUsers ? "There are no other users in your database." : - this.users.map(({ user, notificationDoc }) => { // can't use async here - const userKey = user.userDocumentId; - const permissions = this.computePermissions(userKey); - const color = ColorMapping.get(permissions); - - // console.log(manager); - // const metadata = manager[userKey] as Doc; - // const usersShared = StrCast(metadata?.usersShared, ""); - // console.log(usersShared) - - - return ( + <p className={"share-title"}><b>Share </b>{this.focusOn(StrCast(this.targetDoc?.title, "this document"))}</p> + <div className={"close-button"} onClick={this.close}> + <FontAwesomeIcon icon={fa.faTimes} color={"black"} size={"lg"} /> + </div> + {this.targetDoc?.author !== Doc.CurrentUserEmail ? null + : + <div className="share-setup"> + <Select + className={"user-search"} + placeholder={"Enter user or group name..."} + isMulti + closeMenuOnSelect={false} + options={options} + onChange={this.handleUsersChange} + value={this.selectedUsers} + /> + <select className="permissions-select" onChange={this.handlePermissionsChange}> + {this.sharingOptions} + </select> + <button className="share-button" onClick={this.share}> + Share + </button> + </div> + } + <div className="main-container"> + <div className={"individual-container"}> + <div + className="user-sort" + onClick={action(() => this.individualSort = this.individualSort === "ascending" ? "descending" : this.individualSort === "descending" ? "none" : "ascending")}> + Individuals {this.individualSort === "ascending" ? "↑" : this.individualSort === "descending" ? "↓" : ""} {/* → */} + </div> + <div className={"users-list"} style={{ display: !displayUserList ? "flex" : "block" }}>{/*200*/} + { + !displayUserList ? <div - key={userKey} - className={"container"} + className={"none"} > - <span className={"padding"}>{user.email}</span> - {/* <div className={"shared-by"}>{usersShared}</div> */} - <div className="edit-actions"> - <select - className={"permissions-dropdown"} - value={permissions} - style={{ color, borderColor: color }} - onChange={e => this.setInternalSharing({ user, notificationDoc }, e.currentTarget.value, undefined)} - > - {this.sharingOptions} - </select> - </div> + There are no users this document has been shared with. </div> - ); - }) - } + : + userListContents + } + </div> </div> - </div> - <div className={"group-container"}> - <p className={"share-groups"}>Privately share {this.focusOn("this document")} with a group...</p> - <div className={"groups-list"} style={{ display: existGroups ? "block" : "flex", minHeight: existOtherUsers ? undefined : 150 }}>{/*200*/} - {!existGroups ? "There are no groups in your database." : - this.groups.map(group => { - const permissions = this.computePermissions(StrCast(group.groupName)); - const color = ColorMapping.get(permissions); - return ( + <div className={"group-container"}> + <div + className="user-sort" + onClick={action(() => this.groupSort = this.groupSort === "ascending" ? "descending" : this.groupSort === "descending" ? "none" : "ascending")}> + Groups {this.groupSort === "ascending" ? "↑" : this.groupSort === "descending" ? "↓" : ""} {/* → */} + + </div> + <div className={"groups-list"} style={{ display: !displayGroupList ? "flex" : "block" }}>{/*200*/} + { + !displayGroupList ? <div - key={StrCast(group.groupName)} - className={"container"} + className={"none"} > - <span className={"padding"}>{group.groupName}</span> - <div className="edit-actions"> - <select - className={"permissions-dropdown"} - value={permissions} - style={{ color, borderColor: color }} - onChange={e => this.setInternalGroupSharing(group, e.currentTarget.value)} - > - {this.sharingOptions} - </select> - <button onClick={action(() => this.groupToView = group)}>Edit</button> + There are no groups this document has been shared with. </div> - </div> - ); - }) - - } + : + groupListContents + } + </div> </div> </div> + </div> - <div className={"close-button"} onClick={this.close}>Done</div> </div> ); } render() { - // console.log(this.sharingDoc); return ( <MainViewModal contents={this.sharingInterface} @@ -412,6 +517,7 @@ export default class SharingManager extends React.Component<{}> { interactive={true} dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity} overlayDisplayedOpacity={this.overlayOpacity} + closeOnExternalClick={this.close} /> ); } diff --git a/src/client/views/AntimodeMenu.tsx b/src/client/views/AntimodeMenu.tsx index cb293dee4..68ccefcb5 100644 --- a/src/client/views/AntimodeMenu.tsx +++ b/src/client/views/AntimodeMenu.tsx @@ -134,13 +134,35 @@ export default abstract class AntimodeMenu extends React.Component { protected getElement(buttons: JSX.Element[]) { return ( <div className="antimodeMenu-cont" onPointerLeave={this.pointerLeave} onPointerEnter={this.pointerEntered} ref={this._mainCont} onContextMenu={this.handleContextMenu} - style={{ left: this._left, top: this._top, opacity: this._opacity, transitionProperty: this._transitionProperty, transitionDuration: this._transitionDuration, transitionDelay: this._transitionDelay }}> + style={{ + left: this._left, top: this._top, opacity: this._opacity, transitionProperty: this._transitionProperty, transitionDuration: this._transitionDuration, transitionDelay: this._transitionDelay, + position: this.Pinned ? "unset" : undefined + }}> <div className="antimodeMenu-dragger" onPointerDown={this.dragStart} style={{ width: "20px" }} /> {buttons} </div> ); } + protected getElementVert(buttons: JSX.Element[]) { + return ( + <div className="antimodeMenu-cont" onPointerLeave={this.pointerLeave} onPointerEnter={this.pointerEntered} ref={this._mainCont} onContextMenu={this.handleContextMenu} + style={{ + left: this.Pinned ? undefined : this._left, + top: this.Pinned ? 0 : this._top, + right: this.Pinned ? 0 : undefined, + height: "inherit", + width: 200, + opacity: this._opacity, transitionProperty: this._transitionProperty, transitionDuration: this._transitionDuration, transitionDelay: this._transitionDelay, + position: this.Pinned ? "absolute" : undefined + }}> + {buttons} + </div> + ); + } + + + protected getElementWithRows(rows: JSX.Element[], numRows: number, hasDragger: boolean = true) { return ( <div className="antimodeMenu-cont with-rows" onPointerLeave={this.pointerLeave} onPointerEnter={this.pointerEntered} ref={this._mainCont} onContextMenu={this.handleContextMenu} diff --git a/src/client/views/ContextMenu.scss b/src/client/views/ContextMenu.scss index 1bf242d93..86e0a568a 100644 --- a/src/client/views/ContextMenu.scss +++ b/src/client/views/ContextMenu.scss @@ -42,7 +42,7 @@ .contextMenu-item { // width: 11vw; //10vw - height: 30px; //2vh + height: 25px; //2vh background: whitesmoke; display: flex; //comment out to allow search icon to be inline with search text justify-content: left; @@ -70,9 +70,6 @@ text-align: center; font-size: 20px; margin-left: 5px; - margin-top: 5px; - margin-bottom: 5px; - height: 20px; } } .contextMenu-description { @@ -90,14 +87,11 @@ border-style: none; // padding: 10px 0px 10px 0px; white-space: nowrap; - font-size: 13px; + font-size: 10px; color: grey; - letter-spacing: 2px; + letter-spacing: 1px; text-transform: uppercase; padding-right: 30px; - margin-top: 5px; - height: 20px; - margin-bottom: 5px; } .contextMenu-item:hover { @@ -138,6 +132,10 @@ padding-left: 5px; } +.contextMenu-inlineMenu { + border-top: solid 1px; +} + .contextMenu-item:hover { transition: all 0.1s ease; background: $lighter-alt-accent; diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx index 99840047f..86d1f22e1 100644 --- a/src/client/views/ContextMenuItem.tsx +++ b/src/client/views/ContextMenuItem.tsx @@ -19,6 +19,7 @@ export interface OriginalMenuProps { export interface SubmenuProps { description: string; subitems: ContextMenuProps[]; + noexpand?: boolean; icon: IconProp; //maybe should be optional (icon?) closeMenu?: () => void; } @@ -96,6 +97,11 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select <div className="contextMenu-subMenu-cont" style={{ marginLeft: "25%", left: "0px", marginTop }}> {this._items.map(prop => <ContextMenuItem {...prop} key={prop.description} closeMenu={this.props.closeMenu} />)} </div>; + if (!("noexpand" in this.props)) { + return <div className="contextMenu-inlineMenu"> + {this._items.map(prop => <ContextMenuItem {...prop} key={prop.description} closeMenu={this.props.closeMenu} />)} + </div>; + } return ( <div className={"contextMenu-item" + (this.props.selected ? " contextMenu-itemSelected" : "")} style={{ alignItems: where }} onMouseLeave={this.onPointerLeave} onMouseEnter={this.onPointerEnter}> diff --git a/src/client/views/DictationOverlay.tsx b/src/client/views/DictationOverlay.tsx index 65770c0bb..9ed14509f 100644 --- a/src/client/views/DictationOverlay.tsx +++ b/src/client/views/DictationOverlay.tsx @@ -66,6 +66,7 @@ export class DictationOverlay extends React.Component { interactive={false} dialogueBoxStyle={dialogueBoxStyle} overlayStyle={overlayStyle} + closeOnExternalClick={this.initiateDictationFade} />); } }
\ No newline at end of file diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index 9b9a28f0f..95c1bcda8 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -1,4 +1,4 @@ -import { Doc, Opt, DataSym, DocListCast, AclSym, AclReadonly, AclAddonly } from '../../fields/Doc'; +import { Doc, Opt, DataSym, AclReadonly, AclAddonly, AclPrivate, AclEdit, AclSym, DocListCastAsync, DocListCast } from '../../fields/Doc'; import { Touchable } from './Touchable'; import { computed, action, observable } from 'mobx'; import { Cast, BoolCast, ScriptCast } from '../../fields/Types'; @@ -7,6 +7,8 @@ import { InteractionUtils } from '../util/InteractionUtils'; import { List } from '../../fields/List'; import { DateField } from '../../fields/DateField'; import { ScriptField } from '../../fields/ScriptField'; +import { GetEffectiveAcl, getPlaygroundMode } from '../../fields/util'; +import { SharingPermissions } from '../util/SharingManager'; /// DocComponent returns a generic React base class used by views that don't have 'fieldKey' props (e.g.,CollectionFreeFormDocumentView, DocumentView) @@ -91,6 +93,13 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T // key where data is stored @computed get fieldKey() { return this.props.fieldKey; } + private AclMap = new Map<symbol, string>([ + [AclPrivate, SharingPermissions.None], + [AclReadonly, SharingPermissions.View], + [AclAddonly, SharingPermissions.Add], + [AclEdit, SharingPermissions.Edit] + ]); + lookupField = (field: string) => ScriptCast((this.layoutDoc as any).lookupField)?.script.run({ self: this.layoutDoc, data: this.rootDoc, field: field }).result; styleFromLayoutString = (scale: number) => { @@ -117,11 +126,13 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T docs.map(doc => doc.annotationOn = undefined); const targetDataDoc = this.dataDoc; const value = DocListCast(targetDataDoc[this.annotationKey]); - const result = value.filter(v => !docs.includes(v)); - if (result.length !== value.length) { - targetDataDoc[this.annotationKey] = new List<Doc>(result); + const toRemove = value.filter(v => docs.includes(v)); + // can't assign new List<Doc>(result) to this because you can't assign new values in addonly + if (toRemove.length !== 0) { + toRemove.forEach(doc => Doc.RemoveDocFromList(targetDataDoc, this.annotationKey, doc)); return true; } + return false; } // if the moved document is already in this overlay collection nothing needs to be done. @@ -137,15 +148,30 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T const targetDataDoc = this.props.Document[DataSym]; const docList = DocListCast(targetDataDoc[this.annotationKey]); const added = docs.filter(d => !docList.includes(d)); + const effectiveAcl = GetEffectiveAcl(this.dataDoc); + if (added.length) { - if (this.dataDoc[AclSym] === AclReadonly) { + if (effectiveAcl === AclReadonly && !getPlaygroundMode()) { return false; - } else if (this.dataDoc[AclSym] === AclAddonly) { - added.map(doc => Doc.AddDocToList(targetDataDoc, this.annotationKey, doc)); - } else { - added.map(doc => doc.context = this.props.Document); - targetDataDoc[this.annotationKey] = new List<Doc>([...docList, ...added]); - targetDataDoc[this.annotationKey + "-lastModified"] = new DateField(new Date(Date.now())); + } + else { + if (this.props.Document[AclSym]) { + added.forEach(d => { + const dataDoc = d[DataSym]; + dataDoc[AclSym] = d[AclSym] = this.props.Document[AclSym]; + for (const [key, value] of Object.entries(this.props.Document[AclSym])) { + dataDoc[key] = d[key] = this.AclMap.get(value); + } + }); + } + if (effectiveAcl === AclAddonly) { + added.map(doc => Doc.AddDocToList(targetDataDoc, this.annotationKey, doc)); + } + else { + added.map(doc => doc.context = this.props.Document); + targetDataDoc[this.annotationKey] = new List<Doc>([...docList, ...added]); + targetDataDoc[this.annotationKey + "-lastModified"] = new DateField(new Date(Date.now())); + } } } return true; diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index 1667b2f65..c99034d81 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -11,7 +11,6 @@ import GoogleAuthenticationManager from '../apis/GoogleAuthenticationManager'; import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils'; import { Docs, DocUtils } from '../documents/Documents'; import { DragManager } from '../util/DragManager'; -import { UndoManager } from "../util/UndoManager"; import { CollectionDockingView, DockedFrameRenderer } from './collections/CollectionDockingView'; import { ParentDocSelector } from './collections/ParentDocumentSelector'; import './collections/ParentDocumentSelector.scss'; @@ -23,6 +22,7 @@ import { TemplateMenu } from "./TemplateMenu"; import { Template, Templates } from "./Templates"; import React = require("react"); import { DocumentLinksButton } from './nodes/DocumentLinksButton'; +import { Tooltip } from '@material-ui/core'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -51,7 +51,6 @@ enum UtilityButtonState { @observer export class DocumentButtonBar extends React.Component<{ views: () => (DocumentView | undefined)[], stack?: any }, {}> { - private _linkButton = React.createRef<HTMLDivElement>(); private _dragRef = React.createRef<HTMLDivElement>(); private _pullAnimating = false; private _pushAnimating = false; @@ -118,18 +117,18 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV const targetDoc = this.view0?.props.Document; const published = targetDoc && Doc.GetProto(targetDoc)[GoogleRef] !== undefined; const animation = this.isAnimatingPulse ? "shadow-pulse 1s linear infinite" : "none"; - return !targetDoc ? (null) : <div - title={`${published ? "Push" : "Publish"} to Google Docs`} - className="documentButtonBar-linker" - style={{ animation }} - onClick={async () => { - await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(); - !published && runInAction(() => this.isAnimatingPulse = true); - DocumentButtonBar.hasPushedHack = false; - targetDoc[Pushes] = NumCast(targetDoc[Pushes]) + 1; - }}> - <FontAwesomeIcon className="documentdecorations-icon" icon={published ? (this.pushIcon as any) : cloud} size={published ? "sm" : "xs"} /> - </div>; + return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{`${published ? "Push" : "Publish"} to Google Docs`}</div></>}> + <div + className="documentButtonBar-linker" + style={{ animation }} + onClick={async () => { + await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(); + !published && runInAction(() => this.isAnimatingPulse = true); + DocumentButtonBar.hasPushedHack = false; + targetDoc[Pushes] = NumCast(targetDoc[Pushes]) + 1; + }}> + <FontAwesomeIcon className="documentdecorations-icon" icon={published ? (this.pushIcon as any) : cloud} size={published ? "sm" : "xs"} /> + </div></Tooltip>; } @computed @@ -137,82 +136,87 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV const targetDoc = this.view0?.props.Document; const dataDoc = targetDoc && Doc.GetProto(targetDoc); const animation = this.isAnimatingFetch ? "spin 0.5s linear infinite" : "none"; - return !targetDoc || !dataDoc || !dataDoc[GoogleRef] ? (null) : <div className="documentButtonBar-linker" - title={(() => { - switch (this.openHover) { - default: - case UtilityButtonState.Default: return `${!dataDoc.unchanged ? "Pull from" : "Fetch"} Google Docs`; - case UtilityButtonState.OpenRight: return "Open in Right Split"; - case UtilityButtonState.OpenExternally: return "Open in new Browser Tab"; - } - })()} - style={{ backgroundColor: this.pullColor }} - onPointerEnter={action(e => { - if (e.altKey) { - this.openHover = UtilityButtonState.OpenExternally; - } else if (e.shiftKey) { - this.openHover = UtilityButtonState.OpenRight; - } - })} - onPointerLeave={action(() => this.openHover = UtilityButtonState.Default)} - onClick={async e => { - const googleDocUrl = `https://docs.google.com/document/d/${dataDoc[GoogleRef]}/edit`; - if (e.shiftKey) { - e.preventDefault(); - let googleDoc = await Cast(dataDoc.googleDoc, Doc); - if (!googleDoc) { - const options = { _width: 600, _nativeWidth: 960, _nativeHeight: 800, isAnnotating: false, UseCors: false }; - googleDoc = Docs.Create.WebDocument(googleDocUrl, options); - dataDoc.googleDoc = googleDoc; + + const title = (() => { + switch (this.openHover) { + default: + case UtilityButtonState.Default: return `${!dataDoc?.unchanged ? "Pull from" : "Fetch"} Google Docs`; + case UtilityButtonState.OpenRight: return "Open in Right Split"; + case UtilityButtonState.OpenExternally: return "Open in new Browser Tab"; + } + })(); + + return !targetDoc || !dataDoc || !dataDoc[GoogleRef] ? (null) : <Tooltip + title={<><div className="dash-tooltip">{title}</div></>}> + <div className="documentButtonBar-linker" + style={{ backgroundColor: this.pullColor }} + onPointerEnter={action(e => { + if (e.altKey) { + this.openHover = UtilityButtonState.OpenExternally; + } else if (e.shiftKey) { + this.openHover = UtilityButtonState.OpenRight; } - CollectionDockingView.AddRightSplit(googleDoc); - } else if (e.altKey) { - e.preventDefault(); - window.open(googleDocUrl); - } else { - this.clearPullColor(); - DocumentButtonBar.hasPulledHack = false; - targetDoc[Pulls] = NumCast(targetDoc[Pulls]) + 1; - dataDoc.unchanged && runInAction(() => this.isAnimatingFetch = true); - } - }}> - <FontAwesomeIcon className="documentdecorations-icon" size="sm" - style={{ WebkitAnimation: animation, MozAnimation: animation }} - icon={(() => { - switch (this.openHover) { - default: - case UtilityButtonState.Default: return dataDoc.unchanged === false ? (this.pullIcon as any) : fetch; - case UtilityButtonState.OpenRight: return "arrow-alt-circle-right"; - case UtilityButtonState.OpenExternally: return "share"; + })} + onPointerLeave={action(() => this.openHover = UtilityButtonState.Default)} + onClick={async e => { + const googleDocUrl = `https://docs.google.com/document/d/${dataDoc[GoogleRef]}/edit`; + if (e.shiftKey) { + e.preventDefault(); + let googleDoc = await Cast(dataDoc.googleDoc, Doc); + if (!googleDoc) { + const options = { _width: 600, _nativeWidth: 960, _nativeHeight: 800, isAnnotating: false, UseCors: false }; + googleDoc = Docs.Create.WebDocument(googleDocUrl, options); + dataDoc.googleDoc = googleDoc; + } + CollectionDockingView.AddRightSplit(googleDoc); + } else if (e.altKey) { + e.preventDefault(); + window.open(googleDocUrl); + } else { + this.clearPullColor(); + DocumentButtonBar.hasPulledHack = false; + targetDoc[Pulls] = NumCast(targetDoc[Pulls]) + 1; + dataDoc.unchanged && runInAction(() => this.isAnimatingFetch = true); } - })()} - /> - </div>; + }}> + <FontAwesomeIcon className="documentdecorations-icon" size="sm" + style={{ WebkitAnimation: animation, MozAnimation: animation }} + icon={(() => { + switch (this.openHover) { + default: + case UtilityButtonState.Default: return dataDoc.unchanged === false ? (this.pullIcon as any) : fetch; + case UtilityButtonState.OpenRight: return "arrow-alt-circle-right"; + case UtilityButtonState.OpenExternally: return "share"; + } + })()} + /> + </div></Tooltip>; } @computed get pinButton() { const targetDoc = this.view0?.props.Document; const isPinned = targetDoc && Doc.isDocPinned(targetDoc); - return !targetDoc ? (null) : <div className="documentButtonBar-linker" - title={Doc.isDocPinned(targetDoc) ? "Unpin from presentation" : "Pin to presentation"} - style={{ backgroundColor: isPinned ? "black" : "white", color: isPinned ? "white" : "black" }} - onClick={e => DockedFrameRenderer.PinDoc(targetDoc, isPinned)}> - <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="map-pin" - /> - </div>; + return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{Doc.isDocPinned(targetDoc) ? "Unpin from presentation" : "Pin to presentation"}</div></>}> + <div className="documentButtonBar-linker" + style={{ backgroundColor: isPinned ? "black" : "white", color: isPinned ? "white" : "black" }} + onClick={e => DockedFrameRenderer.PinDoc(targetDoc, isPinned)}> + <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="map-pin" + /> + </div></Tooltip>; } @computed get metadataButton() { const view0 = this.view0; - return !view0 ? (null) : <div title="Show metadata panel" className="documentButtonBar-linkFlyout"> - <Flyout anchorPoint={anchorPoints.LEFT_TOP} - content={<MetadataEntryMenu docs={() => this.props.views().filter(dv => dv).map(dv => dv!.props.Document)} suggestWithFunction /> /* tfs: @bcz This might need to be the data document? */}> - <div className={"documentButtonBar-linkButton-" + "empty"} onPointerDown={e => e.stopPropagation()} > - {<FontAwesomeIcon className="documentdecorations-icon" icon="tag" size="sm" />} - </div> - </Flyout> - </div>; + return !view0 ? (null) : <Tooltip title={<><div className="dash-tooltip">Show metadata panel</div></>}> + <div className="documentButtonBar-linkFlyout"> + <Flyout anchorPoint={anchorPoints.LEFT_TOP} + content={<MetadataEntryMenu docs={this.props.views().filter(dv => dv).map(dv => dv!.props.Document)} suggestWithFunction /> /* tfs: @bcz This might need to be the data document? */}> + <div className={"documentButtonBar-linkButton-" + "empty"} onPointerDown={e => e.stopPropagation()} > + {<FontAwesomeIcon className="documentdecorations-icon" icon="tag" size="sm" />} + </div> + </Flyout> + </div></Tooltip>; } @computed @@ -253,14 +257,15 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV Array.from(Object.values(Templates.TemplateList)).map(template => templates.set(template, views.reduce((checked, doc) => checked || doc?.props.Document["_show" + template.Name] ? true : false, false as boolean))); return !view0 ? (null) : - <div title="Tap: Customize layout. Drag: Create alias" className="documentButtonBar-linkFlyout" ref={this._dragRef}> - <Flyout anchorPoint={anchorPoints.LEFT_TOP} onOpen={action(() => this._aliasDown = true)} onClose={action(() => this._aliasDown = false)} - content={!this._aliasDown ? (null) : <TemplateMenu docViews={views.filter(v => v).map(v => v as DocumentView)} templates={templates} />}> - <div className={"documentButtonBar-linkButton-empty"} ref={this._dragRef} onPointerDown={this.onAliasButtonDown} > - {<FontAwesomeIcon className="documentdecorations-icon" icon="edit" size="sm" />} - </div> - </Flyout> - </div>; + <Tooltip title={<><div className="dash-tooltip">Tap: Customize layout. Drag: Create alias</div></>}> + <div className="documentButtonBar-linkFlyout" ref={this._dragRef}> + <Flyout anchorPoint={anchorPoints.LEFT_TOP} onOpen={action(() => this._aliasDown = true)} onClose={action(() => this._aliasDown = false)} + content={!this._aliasDown ? (null) : <TemplateMenu docViews={views.filter(v => v).map(v => v as DocumentView)} templates={templates} />}> + <div className={"documentButtonBar-linkButton-empty"} ref={this._dragRef} onPointerDown={this.onAliasButtonDown} > + {<FontAwesomeIcon className="documentdecorations-icon" icon="edit" size="sm" />} + </div> + </Flyout> + </div></Tooltip>; } render() { @@ -271,8 +276,11 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV const considerPush = isText && this.considerGoogleDocsPush; return <div className="documentButtonBar"> <div className="documentButtonBar-button"> - <DocumentLinksButton View={this.view0} AlwaysOn={true} InMenu={true} /> + <DocumentLinksButton View={this.view0} AlwaysOn={true} InMenu={true} StartLink={true} /> </div> + {DocumentLinksButton.StartLink ? <div className="documentButtonBar-button"> + <DocumentLinksButton View={this.view0} AlwaysOn={true} InMenu={true} StartLink={false} /> + </div> : null} <div className="documentButtonBar-button"> {this.templateButton} </div> diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index a45ef8862..fec4ad9e0 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -17,14 +17,12 @@ import { DocumentButtonBar } from './DocumentButtonBar'; import './DocumentDecorations.scss'; import { DocumentView } from "./nodes/DocumentView"; import React = require("react"); -import { Id, Copy, Update } from '../../fields/FieldSymbols'; import e = require('express'); import { CollectionDockingView } from './collections/CollectionDockingView'; import { SnappingManager } from '../util/SnappingManager'; import { HtmlField } from '../../fields/HtmlField'; import { InkData, InkField, InkTool } from "../../fields/InkField"; -import { update } from 'serializr'; -import { Transform } from "../util/Transform"; +import { Tooltip } from '@material-ui/core'; library.add(faCaretUp); library.add(faObjectGroup); @@ -83,6 +81,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> return SelectionManager.SelectedDocuments().reduce((bounds, documentView) => { if (documentView.props.renderDepth === 0 || documentView.props.treeViewDoc || + !documentView.ContentDiv || Doc.AreProtosEqual(documentView.props.Document, Doc.UserDoc())) { return bounds; } @@ -90,7 +89,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> var [sptX, sptY] = transform.transformPoint(0, 0); let [bptX, bptY] = transform.transformPoint(documentView.props.PanelWidth(), documentView.props.PanelHeight()); if (documentView.props.Document.type === DocumentType.LINK) { - const docuBox = documentView.ContentDiv!.getElementsByClassName("linkAnchorBox-cont"); + const docuBox = documentView.ContentDiv.getElementsByClassName("linkAnchorBox-cont"); if (docuBox.length) { const rect = docuBox[0].getBoundingClientRect(); sptX = rect.left; @@ -152,8 +151,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> if (e.button === 0 && !e.altKey && !e.ctrlKey) { let child = SelectionManager.SelectedDocuments()[0].ContentDiv!.children[0]; while (child.children.length) { - const next = Array.from(child.children).find(c => !c.className.includes("collectionViewChrome")); - if (next?.className.includes("documentView-node")) break; + const next = Array.from(child.children).find(c => typeof (c.className) !== "string"); + if (typeof (next?.className) === "string" && next?.className.includes("documentView-node")) break; if (next) child = next; else break; } @@ -293,13 +292,13 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> SelectionManager.SelectedDocuments().forEach(action((element: DocumentView) => { const doc = Document(element.rootDoc); if (doc.type === DocumentType.INK && doc.x && doc.y && doc._width && doc._height && doc.data) { - const ink = Cast(doc.data, InkField)?.inkData; - if (ink) { - + doc.rotation = Number(doc.rotation) + Number(angle); + const inks = Cast(doc.data, InkField)?.inkData; + if (inks) { const newPoints: { X: number, Y: number }[] = []; - for (var i = 0; i < ink.length; i++) { - const newX = Math.cos(angle) * (ink[i].X - this._centerPoints[index].X) - Math.sin(angle) * (ink[i].Y - this._centerPoints[index].Y) + this._centerPoints[index].X; - const newY = Math.sin(angle) * (ink[i].X - this._centerPoints[index].X) + Math.cos(angle) * (ink[i].Y - this._centerPoints[index].Y) + this._centerPoints[index].Y; + for (const ink of inks) { + const newX = Math.cos(angle) * (ink.X - this._centerPoints[index].X) - Math.sin(angle) * (ink.Y - this._centerPoints[index].Y) + this._centerPoints[index].X; + const newY = Math.sin(angle) * (ink.X - this._centerPoints[index].X) + Math.cos(angle) * (ink.Y - this._centerPoints[index].Y) + this._centerPoints[index].Y; newPoints.push({ X: newX, Y: newY }); } doc.data = new InkField(newPoints); @@ -433,6 +432,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> let nheight = doc._nativeHeight || 0; const width = (doc._width || 0); let height = (doc._height || (nheight / nwidth * width)); + height = !height || isNaN(height) ? 20 : height; const scale = element.props.ScreenToLocalTransform().Scale * element.props.ContentScaling(); if (nwidth && nheight) { if (nwidth / nheight !== width / height) { @@ -545,13 +545,15 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> } const minimal = bounds.r - bounds.x < 100 ? true : false; const maximizeIcon = minimal ? ( - <div className="documentDecorations-contextMenu" title="Show context menu" onPointerDown={this.onSettingsDown}> - <FontAwesomeIcon size="lg" icon="cog" /> - </div>) : ( - <div className="documentDecorations-minimizeButton" title="Iconify" onClick={this.onCloseClick}> - {/* Currently, this is set to be enabled if there is no ink selected. It might be interesting to think about minimizing ink if it's useful? -syip2*/} - <FontAwesomeIcon className="documentdecorations-times" icon={faTimes} size="lg" /> - </div>); + <Tooltip title={<><div className="dash-tooltip">Show context menu</div></>} placement="top"> + <div className="documentDecorations-contextMenu" onPointerDown={this.onSettingsDown}> + <FontAwesomeIcon size="lg" icon="cog" /> + </div></Tooltip>) : ( + <Tooltip title={<><div className="dash-tooltip">Iconify</div></>} placement="top"> + <div className="documentDecorations-minimizeButton" onClick={this.onCloseClick}> + {/* Currently, this is set to be enabled if there is no ink selected. It might be interesting to think about minimizing ink if it's useful? -syip2*/} + <FontAwesomeIcon className="documentdecorations-times" icon={faTimes} size="lg" /> + </div></Tooltip>); const titleArea = this._edtingTitle ? <> @@ -571,11 +573,11 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> </div>} </> : <> - {minimal ? (null) : <div className="documentDecorations-contextMenu" key="menu" title="Show context menu" onPointerDown={this.onSettingsDown}> + {minimal ? (null) : <Tooltip title={<><div className="dash-tooltip">Show context menu</div></>} placement="top"><div className="documentDecorations-contextMenu" key="menu" onPointerDown={this.onSettingsDown}> <FontAwesomeIcon size="lg" icon="cog" /> - </div>} + </div></Tooltip>} <div className="documentDecorations-title" key="title" onPointerDown={this.onTitleDown} > - <span style={{ width: "100%", display: "inline-block" }}>{`${this.selectionTitle}`}</span> + <span style={{ width: "100%", display: "inline-block", cursor: "move" }}>{`${this.selectionTitle}`}</span> </div> </>; @@ -610,12 +612,13 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> {maximizeIcon} {titleArea} {SelectionManager.SelectedDocuments().length !== 1 || seldoc.Document.type === DocumentType.INK ? (null) : - <div className="documentDecorations-iconifyButton" title={`${seldoc.finalLayoutKey.includes("icon") ? "De" : ""}Iconify Document`} onPointerDown={this.onIconifyDown}> - {"_"} - </div>} - <div className="documentDecorations-closeButton" title="Open Document in Tab" onPointerDown={this.onMaximizeDown}> + <Tooltip title={<><div className="dash-tooltip">{`${seldoc.finalLayoutKey.includes("icon") ? "De" : ""}Iconify Document`}</div></>} placement="top"> + <div className="documentDecorations-iconifyButton" onPointerDown={this.onIconifyDown}> + {"_"} + </div></Tooltip>} + <Tooltip title={<><div className="dash-tooltip">Open Document In Tab</div></>} placement="top"><div className="documentDecorations-closeButton" onPointerDown={this.onMaximizeDown}> {SelectionManager.SelectedDocuments().length === 1 ? DocumentDecorations.DocumentIcon(StrCast(seldoc.props.Document.layout, "...")) : "..."} - </div> + </div></Tooltip> <div id="documentDecorations-rotation" className="documentDecorations-rotation" onPointerDown={this.onRotateDown}> ⟲ </div> <div id="documentDecorations-topLeftResizer" className="documentDecorations-resizer" @@ -636,10 +639,11 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> <div id="documentDecorations-bottomRightResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> {seldoc.props.renderDepth <= 1 || !seldoc.props.ContainingCollectionView ? (null) : - <div id="documentDecorations-levelSelector" className="documentDecorations-selector" - title="tap to select containing document" onPointerDown={this.onSelectorUp} onContextMenu={e => e.preventDefault()}> - <FontAwesomeIcon className="documentdecorations-times" icon={faArrowAltCircleUp} size="lg" /> - </div>} + <Tooltip title={<><div className="dash-tooltip">tap to select containing document</div></>} placement="top"> + <div id="documentDecorations-levelSelector" className="documentDecorations-selector" + onPointerDown={this.onSelectorUp} onContextMenu={e => e.preventDefault()}> + <FontAwesomeIcon className="documentdecorations-times" icon={faArrowAltCircleUp} size="lg" /> + </div></Tooltip>} <div id="documentDecorations-borderRadius" className="documentDecorations-radius" onPointerDown={this.onRadiusDown} onContextMenu={(e) => e.preventDefault()}></div> diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index 25a87ab56..ad61d3f91 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -73,7 +73,6 @@ export class EditableView extends React.Component<EditableProps> { // // this is done because when autosuggest is turned on, the suggestions are passed in as a prop, // // so when the suggestions are passed in, and no editing prop is passed in, it used to set it // // to false. this will no longer do so -syip - // console.log("props editing = " + nextProps.editing); // if (nextProps.editing && nextProps.editing !== this._editing) { // this._editing = nextProps.editing; // EditableView.loadId = ""; diff --git a/src/client/views/GestureOverlay.scss b/src/client/views/GestureOverlay.scss index f61f4a05e..c9d78890e 100644 --- a/src/client/views/GestureOverlay.scss +++ b/src/client/views/GestureOverlay.scss @@ -1,7 +1,7 @@ .gestureOverlay-cont { width: 100vw; height: 100vh; - position: fixed; + position: relative; top: 0; left: 0; touch-action: none; diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index cdc468066..2e588ceb5 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -7,9 +7,8 @@ import { Cast, FieldValue, NumCast } from "../../fields/Types"; import MobileInkOverlay from "../../mobile/MobileInkOverlay"; import { GestureUtils } from "../../pen-gestures/GestureUtils"; import { MobileInkOverlayContent } from "../../server/Message"; -import { emptyFunction, emptyPath, returnEmptyString, returnFalse, returnOne, returnTrue, returnZero, returnEmptyFilter } from "../../Utils"; +import { emptyFunction, emptyPath, returnEmptyString, returnFalse, returnOne, returnTrue, returnZero, returnEmptyFilter, setupMoveUpEvents } from "../../Utils"; import { CognitiveServices } from "../cognitive_services/CognitiveServices"; -import { DocServer } from "../DocServer"; import { DocUtils } from "../documents/Documents"; import { CurrentUserUtils } from "../util/CurrentUserUtils"; import { InteractionUtils } from "../util/InteractionUtils"; @@ -23,7 +22,7 @@ import { RadialMenu } from "./nodes/RadialMenu"; import HorizontalPalette from "./Palette"; import { Touchable } from "./Touchable"; import TouchScrollableMenu, { TouchScrollableMenuItem } from "./TouchScrollableMenu"; -import HeightLabel from "./collections/collectionMulticolumn/MultirowHeightLabel"; +import { CollectionFreeFormViewChrome } from "./collections/CollectionMenu"; @observer export default class GestureOverlay extends Touchable { @@ -55,6 +54,7 @@ export default class GestureOverlay extends Touchable { @observable private showMobileInkOverlay: boolean = false; + private _overlayRef = React.createRef<HTMLDivElement>(); private _d1: Doc | undefined; private _inkToTextDoc: Doc | undefined; private _thumbDoc: Doc | undefined; @@ -160,7 +160,6 @@ export default class GestureOverlay extends Touchable { if (nts.nt.length === 1) { // -- radial menu code -- this._holdTimer = setTimeout(() => { - console.log("hold"); const target = document.elementFromPoint(te.changedTouches?.item(0).clientX, te.changedTouches?.item(0).clientY); const pt: any = te.touches[te.touches?.length - 1]; if (nts.nt.length === 1 && pt.radiusX > 1 && pt.radiusY > 1) { @@ -497,39 +496,26 @@ export default class GestureOverlay extends Touchable { onPointerDown = (e: React.PointerEvent) => { if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (Doc.GetSelectedTool() === InkTool.Highlighter || Doc.GetSelectedTool() === InkTool.Pen)) { this._points.push({ X: e.clientX, Y: e.clientY }); - e.stopPropagation(); - e.preventDefault(); - - document.removeEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp); - document.addEventListener("pointermove", this.onPointerMove); - document.addEventListener("pointerup", this.onPointerUp); + setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, emptyFunction); } } @action onPointerMove = (e: PointerEvent) => { - if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (Doc.GetSelectedTool() === InkTool.Highlighter || Doc.GetSelectedTool() === InkTool.Pen)) { - this._points.push({ X: e.clientX, Y: e.clientY }); - e.stopPropagation(); - e.preventDefault(); - - - if (this._points.length > 1) { - const B = this.svgBounds; - const initialPoint = this._points[0.]; - const xInGlass = initialPoint.X > (this._thumbX ?? Number.MAX_SAFE_INTEGER) && initialPoint.X < (this._thumbX ?? Number.MAX_SAFE_INTEGER) + this.height; - const yInGlass = initialPoint.Y > (this._thumbY ?? Number.MAX_SAFE_INTEGER) - this.height && initialPoint.Y < (this._thumbY ?? Number.MAX_SAFE_INTEGER); - if (this.Tool !== ToolglassTools.None && xInGlass && yInGlass) { - switch (this.Tool) { - case ToolglassTools.RadialMenu: - document.removeEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp); - //this.handle1PointerHoldStart(e); - } + this._points.push({ X: e.clientX, Y: e.clientY }); + + if (this._points.length > 1) { + const B = this.svgBounds; + const initialPoint = this._points[0.]; + const xInGlass = initialPoint.X > (this._thumbX ?? Number.MAX_SAFE_INTEGER) && initialPoint.X < (this._thumbX ?? Number.MAX_SAFE_INTEGER) + this.height; + const yInGlass = initialPoint.Y > (this._thumbY ?? Number.MAX_SAFE_INTEGER) - this.height && initialPoint.Y < (this._thumbY ?? Number.MAX_SAFE_INTEGER); + if (this.Tool !== ToolglassTools.None && xInGlass && yInGlass) { + switch (this.Tool) { + case ToolglassTools.RadialMenu: return true; } } } + return false; } handleLineGesture = (): boolean => { @@ -588,8 +574,6 @@ export default class GestureOverlay extends Touchable { if (this.Tool !== ToolglassTools.None && xInGlass && yInGlass) { switch (this.Tool) { case ToolglassTools.InkToText: - document.removeEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp); this._strokes.push(new Array(...this._points)); this._points = []; CognitiveServices.Inking.Appliers.InterpretStrokes(this._strokes).then((results) => { @@ -618,12 +602,12 @@ export default class GestureOverlay extends Touchable { break; } } - //if any of the shape is activated in the InkOptionsMenu + //if any of the shape is activated in the CollectionFreeFormViewChrome else if (this.InkShape) { this.makePolygon(this.InkShape, false); this.dispatchGesture(GestureUtils.Gestures.Stroke); this._points = []; - if (this.InkShape !== "noRec") { + if (!CollectionFreeFormViewChrome.Instance._keepMode) { this.InkShape = ""; } } @@ -633,54 +617,35 @@ export default class GestureOverlay extends Touchable { let actionPerformed = false; if (result && result.Score > 0.7) { switch (result.Name) { - case GestureUtils.Gestures.Box: - this.dispatchGesture(GestureUtils.Gestures.Box); - actionPerformed = true; - break; - case GestureUtils.Gestures.StartBracket: - this.dispatchGesture(GestureUtils.Gestures.StartBracket); - actionPerformed = true; - break; - case GestureUtils.Gestures.EndBracket: - this.dispatchGesture("endbracket"); - actionPerformed = true; - break; - case GestureUtils.Gestures.Line: - actionPerformed = this.handleLineGesture(); - break; - case GestureUtils.Gestures.Triangle: - this.makePolygon("triangle", true); - break; - case GestureUtils.Gestures.Circle: - this.makePolygon("circle", true); - break; - case GestureUtils.Gestures.Rectangle: - this.makePolygon("rectangle", true); - break; - case GestureUtils.Gestures.Scribble: - console.log("scribble"); - break; - } - if (actionPerformed) { - this._points = []; + case GestureUtils.Gestures.Box: actionPerformed = this.dispatchGesture(GestureUtils.Gestures.Box); break; + case GestureUtils.Gestures.StartBracket: actionPerformed = this.dispatchGesture(GestureUtils.Gestures.StartBracket); break; + case GestureUtils.Gestures.EndBracket: actionPerformed = this.dispatchGesture("endbracket"); break; + case GestureUtils.Gestures.Line: actionPerformed = this.handleLineGesture(); break; + case GestureUtils.Gestures.Triangle: actionPerformed = this.makePolygon("triangle", true); break; + case GestureUtils.Gestures.Circle: actionPerformed = this.makePolygon("circle", true); break; + case GestureUtils.Gestures.Rectangle: actionPerformed = this.makePolygon("rectangle", true); break; + case GestureUtils.Gestures.Scribble: console.log("scribble"); break; } } // if no gesture (or if the gesture was unsuccessful), "dry" the stroke into an ink document if (!actionPerformed) { this.dispatchGesture(GestureUtils.Gestures.Stroke); - this._points = []; } + this._points = []; } } else { this._points = []; } - SetActiveArrowStart("none"); - GestureOverlay.Instance.SavedArrowStart = ActiveArrowStart(); - SetActiveArrowEnd("none"); - GestureOverlay.Instance.SavedArrowEnd = ActiveArrowEnd(); - document.removeEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp); + //get out of ink mode after each stroke= + if (!CollectionFreeFormViewChrome.Instance._keepMode) { + Doc.SetSelectedTool(InkTool.None); + CollectionFreeFormViewChrome.Instance._selected = CollectionFreeFormViewChrome.Instance._shapesNum; + SetActiveArrowStart("none"); + GestureOverlay.Instance.SavedArrowStart = ActiveArrowStart(); + SetActiveArrowEnd("none"); + GestureOverlay.Instance.SavedArrowEnd = ActiveArrowEnd(); + } } makePolygon = (shape: string, gesture: boolean) => { @@ -691,7 +656,7 @@ export default class GestureOverlay extends Touchable { var bottom = Math.max(...ys); var top = Math.min(...ys); if (shape === "noRec") { - return; + return false; } if (!gesture) { //if shape options is activated in inkOptionMenu @@ -772,11 +737,12 @@ export default class GestureOverlay extends Touchable { this._points.push({ X: x2, Y: y2 }); // this._points.push({ X: x1, Y: y1 - 1 }); } + return true; } dispatchGesture = (gesture: "box" | "line" | "startbracket" | "endbracket" | "stroke" | "scribble" | "text", stroke?: InkData, data?: any) => { const target = document.elementFromPoint((stroke ?? this._points)[0].X, (stroke ?? this._points)[0].Y); - target?.dispatchEvent( + return target?.dispatchEvent( new CustomEvent<GestureUtils.GestureEvent>("dashOnGesture", { bubbles: true, @@ -788,7 +754,7 @@ export default class GestureOverlay extends Touchable { } } ) - ); + ) || false; } getBounds = (stroke: InkData) => { @@ -807,10 +773,11 @@ export default class GestureOverlay extends Touchable { @computed get elements() { const width = Number(ActiveInkWidth()); + const rect = this._overlayRef.current?.getBoundingClientRect(); const B = this.svgBounds; B.left = B.left - width / 2; B.right = B.right + width / 2; - B.top = B.top - width / 2; + B.top = B.top - width / 2 - (rect?.y || 0); B.bottom = B.bottom + width / 2; B.width += width; B.height += width; @@ -827,7 +794,7 @@ export default class GestureOverlay extends Touchable { }), 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(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), 1, 1, this.InkShape, "none", false, false)} + {InteractionUtils.CreatePolyline(this._points.map(p => ({ X: p.X, Y: p.Y - (rect?.y || 0) })), B.left, B.top, ActiveInkColor(), width, width, ActiveInkBezierApprox(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), 1, 1, this.InkShape, "none", false, false)} </svg>] ]; } @@ -876,7 +843,8 @@ export default class GestureOverlay extends Touchable { render() { return ( - <div className="gestureOverlay-cont" onPointerDown={this.onPointerDown} onTouchStart={this.onReactTouchStart}> + <div className="gestureOverlay-cont" ref={this._overlayRef} + onPointerDown={this.onPointerDown} onTouchStart={this.onReactTouchStart}> {this.showMobileInkOverlay ? <MobileInkOverlay /> : <></>} {this.elements} diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index a3a023164..086085db5 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -22,6 +22,8 @@ import { DocumentView } from "./nodes/DocumentView"; import { DocumentLinksButton } from "./nodes/DocumentLinksButton"; import PDFMenu from "./pdf/PDFMenu"; import { ContextMenu } from "./ContextMenu"; +import GroupManager from "../util/GroupManager"; +import { CollectionFreeFormViewChrome } from "./collections/CollectionMenu"; const modifiers = ["control", "meta", "shift", "alt"]; type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo | Promise<KeyControlInfo>; @@ -107,6 +109,8 @@ export default class KeyManager { GoogleAuthenticationManager.Instance.cancel(); HypothesisAuthenticationManager.Instance.cancel(); SharingManager.Instance.close(); + GroupManager.Instance.close(); + CollectionFreeFormViewChrome.Instance.clearKeep(); break; case "delete": case "backspace": @@ -305,13 +309,13 @@ export default class KeyManager { const targetDataDoc = Doc.GetProto(first.props.Document); const fieldKey = Doc.LayoutFieldKey(first.props.Document); const docList = DocListCast(targetDataDoc[fieldKey]); - docids.map((did, i) => i && DocServer.GetRefField(did).then(doc => { + docids.map((did, i) => i && DocServer.GetRefField(did).then(async doc => { count++; if (doc instanceof Doc) { list.push(doc); } if (count === docids.length) { - const added = list.filter(d => !docList.includes(d)).map(d => clone ? Doc.MakeClone(d) : d); + const added = await Promise.all(list.filter(d => !docList.includes(d)).map(async d => clone ? (await Doc.MakeClone(d)).clone : d)); if (added.length) { added.map(doc => doc.context = targetDataDoc); undoBatch(() => { diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 7d6e7e5dd..e26ad47f9 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -15,6 +15,8 @@ import { FieldView, FieldViewProps } from "./nodes/FieldView"; import React = require("react"); import { Scripting } from "../util/Scripting"; import { Doc } from "../../fields/Doc"; +import FormatShapePane from "./collections/collectionFreeForm/FormatShapePane"; +import { action } from "mobx"; library.add(faPaintBrush); @@ -38,10 +40,17 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume this.props.Document.isInkMask = true; } + @action + private formatShape = () => { + FormatShapePane.Instance.Pinned = true; + } + + public static MaskDim = 50000; render() { TraceMobx(); const data: InkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? []; - const strokeWidth = Number(StrCast(this.layoutDoc.strokeWidth, ActiveInkWidth())); + // const strokeWidth = Number(StrCast(this.layoutDoc.strokeWidth, ActiveInkWidth())); + const strokeWidth = Number(this.layoutDoc.strokeWidth); const xs = data.map(p => p.X); const ys = data.map(p => p.Y); const left = Math.min(...xs) - strokeWidth / 2; @@ -52,14 +61,14 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume const height = bottom - top; const scaleX = (this.props.PanelWidth() - strokeWidth) / (width - strokeWidth); const scaleY = (this.props.PanelHeight() - strokeWidth) / (height - strokeWidth); - const strokeColor = StrCast(this.layoutDoc.color, ActiveInkColor()); + const strokeColor = StrCast(this.layoutDoc.color, ""); const points = InteractionUtils.CreatePolyline(data, left, top, strokeColor, strokeWidth, strokeWidth, - StrCast(this.layoutDoc.strokeBezier, ActiveInkBezierApprox()), StrCast(this.layoutDoc.fillColor, ActiveFillColor()), - StrCast(this.layoutDoc.arrowStart, ActiveArrowStart()), StrCast(this.layoutDoc.arrowEnd, ActiveArrowEnd()), - StrCast(this.layoutDoc.dash, ActiveDash()), scaleX, scaleY, "", "none", this.props.isSelected() && strokeWidth <= 5, false); + StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "transparent"), + StrCast(this.layoutDoc.strokeStartMarker), StrCast(this.layoutDoc.strokeEndMarker), + StrCast(this.layoutDoc.strokeDash), scaleX, scaleY, "", "none", this.props.isSelected() && strokeWidth <= 5, false); const hpoints = InteractionUtils.CreatePolyline(data, left, top, this.props.isSelected() && strokeWidth > 5 ? strokeColor : "transparent", strokeWidth, (strokeWidth + 15), - StrCast(this.layoutDoc.strokeBezier, ActiveInkBezierApprox()), StrCast(this.layoutDoc.fillColor, ActiveFillColor()), + StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "transparent"), "none", "none", "0", scaleX, scaleY, "", this.props.active() ? "visiblepainted" : "none", false, true); return ( <svg className="inkingStroke" @@ -67,13 +76,17 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume height={height} style={{ pointerEvents: this.props.Document.isInkMask ? "all" : "none", - transform: this.props.Document.isInkMask ? "translate(2500px, 2500px)" : undefined, + transform: this.props.Document.isInkMask ? `translate(${InkingStroke.MaskDim / 2}px, ${InkingStroke.MaskDim / 2}px)` : undefined, mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? "multiply" : "unset", overflow: "visible", }} onContextMenu={() => { - ContextMenu.Instance?.addItem({ description: "Analyze Stroke", event: this.analyzeStrokes, icon: "paint-brush" }); - ContextMenu.Instance?.addItem({ description: "Make Mask", event: this.makeMask, icon: "paint-brush" }); + const cm = ContextMenu.Instance; + if (cm) { + !Doc.UserDoc().noviceMode && cm.addItem({ description: "Recognize Writing", event: this.analyzeStrokes, icon: "paint-brush" }); + cm.addItem({ description: "Make Mask", event: this.makeMask, icon: "paint-brush" }); + cm.addItem({ description: "Format Shape...", event: this.formatShape, icon: "paint-brush" }); + } }} ><defs> </defs> @@ -94,9 +107,9 @@ export function SetActiveArrowEnd(value: string) { ActiveInkPen() && (ActiveInkP export function SetActiveDash(dash: string): void { !isNaN(parseInt(dash)) && ActiveInkPen() && (ActiveInkPen().activeDash = dash); } export function ActiveInkPen(): Doc { return Cast(Doc.UserDoc().activeInkPen, Doc, null); } export function ActiveInkColor(): string { return StrCast(ActiveInkPen()?.activeInkColor, "black"); } -export function ActiveFillColor(): string { return StrCast(ActiveInkPen()?.activeFillColor, "none"); } -export function ActiveArrowStart(): string { return StrCast(ActiveInkPen()?.activeArrowStart, "none"); } -export function ActiveArrowEnd(): string { return StrCast(ActiveInkPen()?.activeArrowEnd, "none"); } +export function ActiveFillColor(): string { return StrCast(ActiveInkPen()?.activeFillColor, ""); } +export function ActiveArrowStart(): string { return StrCast(ActiveInkPen()?.activeArrowStart, ""); } +export function ActiveArrowEnd(): string { return StrCast(ActiveInkPen()?.activeArrowEnd, ""); } export function ActiveDash(): string { return StrCast(ActiveInkPen()?.activeDash, "0"); } export function ActiveInkWidth(): string { return StrCast(ActiveInkPen()?.activeInkWidth, "1"); } export function ActiveInkBezierApprox(): string { return StrCast(ActiveInkPen()?.activeInkBezier); } diff --git a/src/client/views/KeyphraseQueryView.tsx b/src/client/views/KeyphraseQueryView.tsx index 1dc156968..13d52db88 100644 --- a/src/client/views/KeyphraseQueryView.tsx +++ b/src/client/views/KeyphraseQueryView.tsx @@ -11,7 +11,6 @@ export interface KP_Props { export class KeyphraseQueryView extends React.Component<KP_Props>{ constructor(props: KP_Props) { super(props); - console.log("FIRST KEY PHRASE: ", props.keyphrases[0]); } render() { diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss index 5b142ffda..e1ddbc533 100644 --- a/src/client/views/MainView.scss +++ b/src/client/views/MainView.scss @@ -1,6 +1,10 @@ @import "globalCssVariables"; @import "nodeModuleOverrides"; +.dash-tooltip { + font-size: 11px; + padding: 2px; +} .mainView-tabButtons { position: relative; diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 81195b550..225fb2e8e 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -1,4 +1,5 @@ import { library } from '@fortawesome/fontawesome-svg-core'; + import { faTasks, faEdit, faTrashAlt, faPalette, faAngleRight, faBell, faTrash, faCamera, faExpand, faCaretDown, faCaretLeft, faCaretRight, faCaretSquareDown, faCaretSquareRight, faArrowsAltH, faPlus, faMinus, faTerminal, faToggleOn, faFile as fileSolid, faExternalLinkAlt, faLocationArrow, faSearch, faFileDownload, faStop, faCalculator, faWindowMaximize, faAddressCard, @@ -6,7 +7,8 @@ import { faCommentAlt, faCompressArrowsAlt, faCut, faEllipsisV, faEraser, faExclamation, faFileAlt, faFileAudio, faFilePdf, faFilm, faFilter, faFont, faGlobeAsia, faHighlighter, faLongArrowAltRight, faMicrophone, faMousePointer, faMusic, faObjectGroup, faPause, faPen, faPenNib, faPhone, faPlay, faPortrait, faRedoAlt, faStamp, faStickyNote, faTimesCircle, faThumbtack, faTree, faTv, faUndoAlt, faVideo, faAsterisk, faBrain, faImage, faPaintBrush, faTimes, faEye, faArrowsAlt, faQuoteLeft, faSortAmountDown, faAlignLeft, faAlignCenter, faAlignRight, - faHeading + faHeading, faRulerCombined, faFillDrip, faLink, faUnlink, faBold, faItalic, faChevronLeft, faUnderline, faStrikethrough, faSuperscript, faSubscript, faIndent, faEyeDropper, + faPaintRoller, faBars, faBrush, faShapes, faEllipsisH, faHandPaper, faMap } from '@fortawesome/free-solid-svg-icons'; import { ANTIMODEMENU_HEIGHT } from './globalCssVariables.scss'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; @@ -36,7 +38,6 @@ import SharingManager from '../util/SharingManager'; import { Transform } from '../util/Transform'; import { CollectionDockingView } from './collections/CollectionDockingView'; import MarqueeOptionsMenu from './collections/collectionFreeForm/MarqueeOptionsMenu'; -import InkOptionsMenu from './collections/collectionFreeForm/InkOptionsMenu'; import { CollectionLinearView } from './collections/CollectionLinearView'; import { CollectionView, CollectionViewType } from './collections/CollectionView'; import { ContextMenu } from './ContextMenu'; @@ -60,10 +61,11 @@ import { DocumentManager } from '../util/DocumentManager'; import { DocumentLinksButton } from './nodes/DocumentLinksButton'; import { LinkMenu } from './linking/LinkMenu'; import { LinkDocPreview } from './nodes/LinkDocPreview'; -import { Fade } from '@material-ui/core'; import { LinkCreatedBox } from './nodes/LinkCreatedBox'; import { LinkDescriptionPopup } from './nodes/LinkDescriptionPopup'; +import FormatShapePane from "./collections/collectionFreeForm/FormatShapePane"; import HypothesisAuthenticationManager from '../apis/HypothesisAuthenticationManager'; +import CollectionMenu from './collections/CollectionMenu'; @observer export class MainView extends React.Component { @@ -88,7 +90,7 @@ export class MainView extends React.Component { public isPointerDown = false; componentDidMount() { - DocServer.setPlaygroundFields(["dataTransition", "_viewTransition", "_panX", "_panY", "_viewScale", "_viewType"]); // can play with these fields on someone else's + DocServer.setPlaygroundFields(["dataTransition", "_viewTransition", "_panX", "_panY", "_viewScale", "_viewType", "_chromeStatus"]); // can play with these fields on someone else's const tag = document.createElement('script'); @@ -147,7 +149,8 @@ export class MainView extends React.Component { faCommentAlt, faCompressArrowsAlt, faCut, faEllipsisV, faEraser, faExclamation, faFileAlt, faFileAudio, faFilePdf, faFilm, faFilter, faFont, faGlobeAsia, faHighlighter, faLongArrowAltRight, faMicrophone, faMousePointer, faMusic, faObjectGroup, faPause, faPen, faPenNib, faPhone, faPlay, faPortrait, faRedoAlt, faStamp, faStickyNote, faTrashAlt, faAngleRight, faBell, faThumbtack, faTree, faTv, faUndoAlt, faVideo, faAsterisk, faBrain, faImage, faPaintBrush, faTimes, faEye, faArrowsAlt, faQuoteLeft, faSortAmountDown, faAlignLeft, faAlignCenter, faAlignRight, - faHeading); + faHeading, faRulerCombined, faFillDrip, faLink, faUnlink, faBold, faItalic, faChevronLeft, faUnderline, faStrikethrough, faSuperscript, faSubscript, faIndent, faEyeDropper, + faPaintRoller, faBars, faBrush, faShapes, faEllipsisH, faHandPaper, faMap); this.initEventListeners(); this.initAuthenticationRouters(); } @@ -388,7 +391,7 @@ export class MainView extends React.Component { doc.dockingConfig ? this.openWorkspace(doc) : CollectionDockingView.AddRightSplit(doc, libraryPath); } - sidebarScreenToLocal = () => new Transform(0, RichTextMenu.Instance.Pinned ? -35 : 0, 1); + sidebarScreenToLocal = () => new Transform(0, (RichTextMenu.Instance.Pinned ? -35 : 0) + (CollectionMenu.Instance.Pinned ? -35 : 0), 1); mainContainerXf = () => this.sidebarScreenToLocal().translate(0, -this._buttonBarHeight); @computed get flyout() { @@ -454,9 +457,6 @@ export class MainView extends React.Component { <button className="mainView-settings" key="settings" onClick={() => SettingsManager.Instance.open()}> <FontAwesomeIcon icon="cog" size="lg" /> </button> - <button className="mainView-settings" key="groups" onClick={() => GroupManager.Instance.open()}> - <FontAwesomeIcon icon="columns" size="lg" /> - </button> </div> </div> {this.docButtons} @@ -465,10 +465,14 @@ export class MainView extends React.Component { @computed get mainContent() { const sidebar = this.userDoc?.["tabs-panelContainer"]; + const n = (RichTextMenu.Instance?.Pinned ? 1 : 0) + (CollectionMenu.Instance?.Pinned ? 1 : 0); + const height = `calc(100% - ${n * Number(ANTIMODEMENU_HEIGHT.replace("px", ""))}px)`; return !this.userDoc || !(sidebar instanceof Doc) ? (null) : ( <div className="mainView-mainContent" style={{ color: this.darkScheme ? "rgb(205,205,205)" : "black", - height: RichTextMenu.Instance?.Pinned ? `calc(100% - ${ANTIMODEMENU_HEIGHT})` : "100%" + //change to times 2 for both pinned + height, + width: (FormatShapePane.Instance?.Pinned) ? `calc(100% - 200px)` : "100%" }} > <div style={{ display: "contents", flexDirection: "row", position: "relative" }}> <div className="mainView-flyoutContainer" onPointerLeave={this.pointerLeaveDragger} style={{ width: this.flyoutWidth }}> @@ -607,22 +611,25 @@ export class MainView extends React.Component { <GoogleAuthenticationManager /> <HypothesisAuthenticationManager /> <DocumentDecorations /> - <GestureOverlay> - <RichTextMenu key="rich" /> - {this.mainContent} - </GestureOverlay> - <PreviewCursor /> - <LinkCreatedBox /> + <CollectionMenu /> + <FormatShapePane /> + <RichTextMenu key="rich" /> {LinkDescriptionPopup.descriptionPopup ? <LinkDescriptionPopup /> : null} {DocumentLinksButton.EditLink ? <LinkMenu location={DocumentLinksButton.EditLinkLoc} docView={DocumentLinksButton.EditLink} addDocTab={DocumentLinksButton.EditLink.props.addDocTab} changeFlyout={emptyFunction} /> : (null)} {LinkDocPreview.LinkInfo ? <LinkDocPreview location={LinkDocPreview.LinkInfo.Location} backgroundColor={this.defaultBackgroundColors} linkDoc={LinkDocPreview.LinkInfo.linkDoc} linkSrc={LinkDocPreview.LinkInfo.linkSrc} href={LinkDocPreview.LinkInfo.href} addDocTab={LinkDocPreview.LinkInfo.addDocTab} /> : (null)} + <GestureOverlay > + {this.mainContent} + </GestureOverlay> + <PreviewCursor /> + <LinkCreatedBox /> <ContextMenu /> + <FormatShapePane /> <RadialMenu /> <PDFMenu /> <MarqueeOptionsMenu /> - <InkOptionsMenu /> + <OverlayView /> <TimelineMenu /> {this.snapLines} diff --git a/src/client/views/MainViewModal.scss b/src/client/views/MainViewModal.scss index f5a9ee76c..812fe540b 100644 --- a/src/client/views/MainViewModal.scss +++ b/src/client/views/MainViewModal.scss @@ -6,9 +6,10 @@ align-self: center; align-content: center; padding: 20px; - background: gainsboro; + // background: gainsboro; + background: white; border-radius: 10px; - border: 3px solid black; + border: 0.5px solid black; box-shadow: #00000044 5px 5px 10px; transform: translate(-50%, -50%); top: 50%; diff --git a/src/client/views/MainViewModal.tsx b/src/client/views/MainViewModal.tsx index a7bd5882d..249715511 100644 --- a/src/client/views/MainViewModal.tsx +++ b/src/client/views/MainViewModal.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import "./MainViewModal.scss"; +import { observer } from 'mobx-react'; export interface MainViewOverlayProps { isDisplayed: boolean; @@ -9,8 +10,10 @@ export interface MainViewOverlayProps { overlayStyle?: React.CSSProperties; dialogueBoxDisplayedOpacity?: number; overlayDisplayedOpacity?: number; + closeOnExternalClick?: () => void; } +@observer export default class MainViewModal extends React.Component<MainViewOverlayProps> { render() { @@ -18,11 +21,10 @@ export default class MainViewModal extends React.Component<MainViewOverlayProps> const dialogueOpacity = p.dialogueBoxDisplayedOpacity || 1; const overlayOpacity = p.overlayDisplayedOpacity || 0.4; return !p.isDisplayed ? (null) : ( - <div style={{ pointerEvents: p.isDisplayed ? p.interactive ? "all" : "none" : "none" }}> + <div style={{ pointerEvents: p.isDisplayed && p.interactive ? "all" : "none" }}> <div className={"dialogue-box"} style={{ - backgroundColor: "gainsboro", borderColor: "black", ...(p.dialogueBoxStyle || {}), opacity: p.isDisplayed ? dialogueOpacity : 0 @@ -30,6 +32,7 @@ export default class MainViewModal extends React.Component<MainViewOverlayProps> >{p.contents}</div> <div className={"overlay"} + onClick={this.props?.closeOnExternalClick} style={{ backgroundColor: "gainsboro", ...(p.overlayStyle || {}), diff --git a/src/client/views/MetadataEntryMenu.tsx b/src/client/views/MetadataEntryMenu.tsx index b0752ffb2..82ec5a5b3 100644 --- a/src/client/views/MetadataEntryMenu.tsx +++ b/src/client/views/MetadataEntryMenu.tsx @@ -3,14 +3,14 @@ import "./MetadataEntryMenu.scss"; import { observer } from 'mobx-react'; import { observable, action, runInAction, trace, computed, IReactionDisposer, reaction } from 'mobx'; import { KeyValueBox } from './nodes/KeyValueBox'; -import { Doc, Field, DocListCastAsync } from '../../fields/Doc'; +import { Doc, Field, DocListCastAsync, DocListCast } from '../../fields/Doc'; import * as Autosuggest from 'react-autosuggest'; -import { undoBatch } from '../util/UndoManager'; +import { undoBatch, UndoManager } from '../util/UndoManager'; import { emptyFunction, emptyPath } from '../../Utils'; export type DocLike = Doc | Doc[] | Promise<Doc> | Promise<Doc[]>; export interface MetadataEntryProps { - docs: DocLike | (() => DocLike); + docs: Doc[]; onError?: () => boolean; suggestWithFunction?: boolean; } @@ -38,27 +38,14 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{ let field: Field | undefined | null = null; let onProto: boolean = false; let value: string | undefined = undefined; - let docs = this.props.docs; - if (typeof docs === "function") { - if (this.props.suggestWithFunction) { - docs = docs(); - } else { - return; - } - } - docs = await docs; - if (docs instanceof Doc) { - await docs[this._currentKey]; - value = Field.toKeyValueString(docs, this._currentKey); - } else { - for (const doc of docs) { - const v = await doc[this._currentKey]; - onProto = onProto || !Object.keys(doc).includes(this._currentKey); - if (field === null) { - field = v; - } else if (v !== field) { - value = "multiple values"; - } + const docs = this.props.docs; + for (const doc of docs) { + const v = await doc[this._currentKey]; + onProto = onProto || !Object.keys(doc).includes(this._currentKey); + if (field === null) { + field = v; + } else if (v !== field) { + value = "multiple values"; } } if (value === undefined) { @@ -86,27 +73,16 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{ const script = KeyValueBox.CompileKVPScript(this._currentValue); if (!script) return; - let doc = this.props.docs; - if (typeof doc === "function") { - doc = doc(); - } - doc = await doc; - - let success: boolean; - if (doc instanceof Doc) { - success = KeyValueBox.ApplyKVPScript(doc, this._currentKey, script); - } else { - let childSuccess = true; - if (this._addChildren) { - for (const document of doc) { - const collectionChildren = await DocListCastAsync(document.data); - if (collectionChildren) { - childSuccess = collectionChildren.every(c => KeyValueBox.ApplyKVPScript(c, this._currentKey, script)); - } + let childSuccess = true; + if (this._addChildren) { + for (const document of this.props.docs) { + const collectionChildren = DocListCast(document.data); + if (collectionChildren) { + childSuccess = collectionChildren.every(c => KeyValueBox.ApplyKVPScript(c, this._currentKey, script)); } } - success = doc.every(d => KeyValueBox.ApplyKVPScript(d, this._currentKey, script)) && childSuccess; } + const success = this.props.docs.every(d => KeyValueBox.ApplyKVPScript(d, this._currentKey, script)) && childSuccess; if (!success) { if (this.props.onError) { if (this.props.onError()) { @@ -132,24 +108,12 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{ } } - getKeySuggestions = async (value: string): Promise<string[]> => { + getKeySuggestions = (value: string) => { value = value.toLowerCase(); - let docs = this.props.docs; - if (typeof docs === "function") { - if (this.props.suggestWithFunction) { - docs = docs(); - } else { - return []; - } - } - docs = await docs; - if (docs instanceof Doc) { - return Object.keys(docs).filter(key => key.toLowerCase().startsWith(value)); - } else { - const keys = new Set<string>(); - docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key))); - return Array.from(keys).filter(key => key.toLowerCase().startsWith(value)); - } + const docs = this.props.docs; + const keys = new Set<string>(); + docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key))); + return Array.from(keys).filter(key => key.toLowerCase().startsWith(value)); } getSuggestionValue = (suggestion: string) => suggestion; @@ -157,9 +121,8 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{ return (null); } componentDidMount() { - this._suggestionDispser = reaction(() => this._currentKey, - () => this.getKeySuggestions(this._currentKey).then(action((s: string[]) => this._allSuggestions = s)), + () => this._allSuggestions = this.getKeySuggestions(this._currentKey), { fireImmediately: true }); } componentWillUnmount() { @@ -171,19 +134,8 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{ } private get considerChildOptions() { - let docSource = this.props.docs; - if (typeof docSource === "function") { - docSource = docSource(); - } - docSource = docSource as Doc[] | Doc; - if (docSource instanceof Doc) { - if (docSource._viewType === undefined) { - return (null); - } - } else if (Array.isArray(docSource)) { - if (!docSource.every(doc => doc._viewType !== undefined)) { - return null; - } + if (!this.props.docs.every(doc => doc._viewType !== undefined)) { + return null; } return ( <div style={{ display: "flex" }}> diff --git a/src/client/views/OverlayView.scss b/src/client/views/OverlayView.scss index 26c2e0e1e..09a349012 100644 --- a/src/client/views/OverlayView.scss +++ b/src/client/views/OverlayView.scss @@ -3,6 +3,8 @@ overflow: hidden; display: flex; flex-direction: column; + top: 0; + left: 0; } .overlayWindow-outerDiv, diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx index 37d8dd23b..5c3a8185c 100644 --- a/src/client/views/OverlayView.tsx +++ b/src/client/views/OverlayView.tsx @@ -124,7 +124,9 @@ export class OverlayView extends React.Component { ele = <div key={Utils.GenerateGuid()} className="overlayView-wrapperDiv" style={{ transform: `translate(${options.x}px, ${options.y}px)`, width: options.width, - height: options.height + height: options.height, + top: 0, + left: 0 }}>{ele}</div>; this._elements.push(ele); return remove; diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx index 6583589f3..b4116e980 100644 --- a/src/client/views/PreviewCursor.tsx +++ b/src/client/views/PreviewCursor.tsx @@ -69,11 +69,11 @@ export class PreviewCursor extends React.Component<{}> { const list: Doc[] = []; let first: Doc | undefined; - docids.map((did, i) => i && DocServer.GetRefField(did).then(doc => { + docids.map((did, i) => i && DocServer.GetRefField(did).then(async doc => { count++; if (doc instanceof Doc) { i === 1 && (first = doc); - const alias = clone ? Doc.MakeClone(doc) : doc; + const alias = clone ? (await Doc.MakeClone(doc)).clone : doc; const deltaX = NumCast(doc.x) - NumCast(first!.x) - ptx; const deltaY = NumCast(doc.y) - NumCast(first!.y) - pty; alias.x = newPoint[0] + deltaX; diff --git a/src/client/views/ScriptBox.tsx b/src/client/views/ScriptBox.tsx index 888f84dfa..2c185be86 100644 --- a/src/client/views/ScriptBox.tsx +++ b/src/client/views/ScriptBox.tsx @@ -39,7 +39,7 @@ export class ScriptBox extends React.Component<ScriptBoxProps> { @action onError = (error: string) => { - console.log(error); + console.log("ScriptBox: " + error); } overlayDisposer?: () => void; diff --git a/src/client/views/SearchDocBox.tsx b/src/client/views/SearchDocBox.tsx index e038d8213..084f952a3 100644 --- a/src/client/views/SearchDocBox.tsx +++ b/src/client/views/SearchDocBox.tsx @@ -60,8 +60,6 @@ export class SearchDocBox extends React.Component<FieldViewProps> { componentDidMount() { runInAction(() => { - console.log("didit" - ); this.query = StrCast(this.props.Document.searchText); this.content = (Docs.Create.TreeDocument(DocListCast(Doc.GetProto(this.props.Document).data), { _width: 200, _height: 400, _chromeStatus: "disabled", title: `Search Docs:` + this.query })); @@ -83,12 +81,9 @@ export class SearchDocBox extends React.Component<FieldViewProps> { if (newKey.length > 1) { const newdocs = await this.getAllResults(this.query); const things = newdocs.docs; - console.log(things); - console.log(this.content); runInAction(() => { this.content = Docs.Create.TreeDocument(things, { _width: 200, _height: 400, _chromeStatus: "disabled", title: `Search Docs:` + this.query }); }); - console.log(this.content); } @@ -150,12 +145,9 @@ export class SearchDocBox extends React.Component<FieldViewProps> { } enter = async (e: React.KeyboardEvent) => { - console.log(e.key); if (e.key === "Enter") { const newdocs = await this.getAllResults(this.query); - console.log(newdocs.docs); this.content = Docs.Create.TreeDocument(newdocs.docs, { _width: 200, _height: 400, _chromeStatus: "disabled", title: `Search Docs: "Results"` }); - } } @@ -256,7 +248,6 @@ export class SearchDocBox extends React.Component<FieldViewProps> { startDragCollection = async () => { const res = await this.getAllResults(FilterBox.Instance.getFinalQuery(this._searchString)); const filtered = FilterBox.Instance.filterDocsByType(res.docs); - // console.log(this._results) const docs = filtered.map(doc => { const isProto = Doc.GetT(doc, "isPrototype", "boolean", true); if (isProto) { diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx index 916e631d0..9fb8a227e 100644 --- a/src/client/views/TemplateMenu.tsx +++ b/src/client/views/TemplateMenu.tsx @@ -108,8 +108,9 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { return100 = () => 100; @computed get scriptField() { - return ScriptField.MakeScript("docs.map(d => switchView(d, this))", { this: Doc.name, heading: "string", checked: "string", containingTreeView: Doc.name, firstDoc: Doc.name }, + const script = ScriptField.MakeScript("docs.map(d => switchView(d, this))", { this: Doc.name, heading: "string", checked: "string", containingTreeView: Doc.name, firstDoc: Doc.name }, { docs: new List<Doc>(this.props.docViews.map(dv => dv.props.Document)) }); + return script ? () => script : undefined; } templateIsUsed = (selDoc: Doc, templateDoc: Doc) => { const template = StrCast(templateDoc.dragFactory ? Cast(templateDoc.dragFactory, Doc, null)?.title : templateDoc.title); @@ -142,8 +143,8 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { ContainingCollectionView={undefined} docFilters={returnEmptyFilter} rootSelected={returnFalse} - onCheckedClick={this.scriptField!} - onChildClick={this.scriptField!} + onCheckedClick={this.scriptField} + onChildClick={this.scriptField} LibraryPath={emptyPath} dropAction={undefined} active={returnTrue} diff --git a/src/client/views/Touchable.tsx b/src/client/views/Touchable.tsx index 5e48d5ffb..c4cae7e8d 100644 --- a/src/client/views/Touchable.tsx +++ b/src/client/views/Touchable.tsx @@ -54,7 +54,6 @@ export abstract class Touchable<T = {}> extends React.Component<T> { } }); - // console.log(ptsToDelete.length); ptsToDelete.forEach(pt => this.prevPoints.delete(pt)); if (this.prevPoints.size) { @@ -86,7 +85,6 @@ export abstract class Touchable<T = {}> extends React.Component<T> { // if we're not actually moving a lot, don't consider it as dragging yet if (!InteractionUtils.IsDragging(this.prevPoints, myTouches, 5) && !this._touchDrag) return; this._touchDrag = true; - // console.log(myTouches.length); switch (myTouches.length) { case 1: this.handle1PointerMove(te, me); @@ -107,7 +105,6 @@ export abstract class Touchable<T = {}> extends React.Component<T> { @action protected onTouchEnd = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => { - // console.log(InteractionUtils.GetMyTargetTouches(e, this.prevPoints).length + " up"); // remove all the touches associated with the event const te = me.touchEvent; for (const pt of me.changedTouches) { diff --git a/src/client/views/animationtimeline/Keyframe.tsx b/src/client/views/animationtimeline/Keyframe.tsx index 3a7182a94..1b81c544a 100644 --- a/src/client/views/animationtimeline/Keyframe.tsx +++ b/src/client/views/animationtimeline/Keyframe.tsx @@ -527,10 +527,6 @@ export class Keyframe extends React.Component<IProps> { */ //154, 206, 223 render() { - trace(); - console.log(this.props.RegionData.position); - console.log(this.regiondata.position); - console.log(this.pixelPosition); return ( <div className="bar" ref={this._bar} style={{ transform: `translate(${this.pixelPosition}px)`, diff --git a/src/client/views/animationtimeline/Timeline.tsx b/src/client/views/animationtimeline/Timeline.tsx index f54bd3aff..df828897e 100644 --- a/src/client/views/animationtimeline/Timeline.tsx +++ b/src/client/views/animationtimeline/Timeline.tsx @@ -385,10 +385,10 @@ export class Timeline extends React.Component<FieldViewProps> { const ttime = `Total: ${this.toReadTime(this._time)}`; return ( <div style={{ flexDirection: "column" }}> - <div className="animation-text" style={{ fontSize: "10px", width: "100%", display: !this.props.Document.isATOn ? "block" : "none" }}> + <div className="animation-text" style={{ fontSize: "10pt", width: "100%", display: !this.props.Document.isATOn ? "block" : "none" }}> {ctime} </div> - <div className="animation-text" style={{ fontSize: "10px", width: "100%", display: !this.props.Document.isATOn ? "block" : "none" }}> + <div className="animation-text" style={{ fontSize: "10pt", width: "100%", display: !this.props.Document.isATOn ? "block" : "none" }}> {ttime} </div> </div> @@ -466,7 +466,6 @@ export class Timeline extends React.Component<FieldViewProps> { //TODO: remove undefineds and duplicates } }); - // console.log(longestTime); return longestTime; } diff --git a/src/client/views/animationtimeline/Track.tsx b/src/client/views/animationtimeline/Track.tsx index 4987006e7..25c2e68e7 100644 --- a/src/client/views/animationtimeline/Track.tsx +++ b/src/client/views/animationtimeline/Track.tsx @@ -134,7 +134,6 @@ export class Track extends React.Component<IProps> { autoCreateKeyframe = () => { const objects = this.objectWhitelist.map(key => this.props.node[key]); intercept(this.props.node, change => { - console.log(change); return change; }); return reaction(() => { @@ -174,7 +173,6 @@ export class Track extends React.Component<IProps> { if (regiondata) { this.props.node.hidden = false; // if (!this._autoKfReaction) { - // // console.log("creating another reaction"); // // this._autoKfReaction = this.autoCreateKeyframe(); // } this.timeChange(); @@ -204,7 +202,6 @@ export class Track extends React.Component<IProps> { } }); } else { - console.log("reverting state"); //this.revertState(); } }); @@ -229,7 +226,6 @@ export class Track extends React.Component<IProps> { const rightkf: (Doc | undefined) = await KeyframeFunc.calcMinRight(regiondata, this.time); //right keyframe, if it exists const currentkf: (Doc | undefined) = await this.calcCurrent(regiondata); //if the scrubber is on top of the keyframe if (currentkf) { - console.log("is current"); await this.applyKeys(currentkf); this.saveStateKf = currentkf; this.saveStateRegion = regiondata; diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx index 1344b70f4..0f3b6f212 100644 --- a/src/client/views/collections/CollectionCarousel3DView.tsx +++ b/src/client/views/collections/CollectionCarousel3DView.tsx @@ -38,15 +38,15 @@ export class CollectionCarousel3DView extends CollectionSubView(Carousel3DDocume panelWidth = () => this.props.PanelWidth() / 3; panelHeight = () => this.props.PanelHeight() * 0.6; + onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick); @computed get content() { const currentIndex = NumCast(this.layoutDoc._itemIndex); const displayDoc = (childPair: { layout: Doc, data: Doc }) => { + const script = ScriptField.MakeScript("child._showCaption = 'caption'", { child: Doc.name }, { child: childPair.layout }); + const onChildClick = script && (() => script); return <ContentFittingDocumentView {...this.props} - onDoubleClick={ScriptCast(this.layoutDoc.onChildDoubleClick)} - onClick={ScriptField.MakeScript( - "child._showCaption = 'caption'", - { child: Doc.name }, - { child: childPair.layout })} + onDoubleClick={this.onChildDoubleClick} + onClick={onChildClick} renderDepth={this.props.renderDepth + 1} LayoutTemplate={this.props.ChildLayoutTemplate} LayoutTemplateString={this.props.ChildLayoutString} @@ -86,7 +86,6 @@ export class CollectionCarousel3DView extends CollectionSubView(Carousel3DDocume interval?: number; startAutoScroll = (direction: number) => { this.interval = window.setInterval(() => { - console.log(this.interval, this.scrollSpeed); this.changeSlide(direction); }, this.scrollSpeed); } @@ -108,29 +107,16 @@ export class CollectionCarousel3DView extends CollectionSubView(Carousel3DDocume }, 1500); } - onContextMenu = (e: React.MouseEvent): void => { - // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout - if (!e.isPropagationStopped()) { - ContextMenu.Instance.addItem({ - description: "Make Hero Image", event: () => { - const index = NumCast(this.layoutDoc._itemIndex); - (this.dataDoc || Doc.GetProto(this.props.Document)).hero = ObjectField.MakeCopy(this.childLayoutPairs[index].layout.data as ObjectField); - }, icon: "plus" - }); - } - } _downX = 0; _downY = 0; onPointerDown = (e: React.PointerEvent) => { this._downX = e.clientX; this._downY = e.clientY; - console.log("CAROUSEL down"); document.addEventListener("pointerup", this.onpointerup); } private _lastTap: number = 0; private _doubleTap = false; onpointerup = (e: PointerEvent) => { - console.log("CAROUSEL up"); this._doubleTap = (Date.now() - this._lastTap < 300 && e.button === 0 && Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2); this._lastTap = Date.now(); } @@ -184,7 +170,7 @@ export class CollectionCarousel3DView extends CollectionSubView(Carousel3DDocume const index = NumCast(this.layoutDoc._itemIndex); const translateX = (1 - index) / this.childLayoutPairs.length * 100; - return <div className="collectionCarousel3DView-outer" onClick={this.onClick} onPointerDown={this.onPointerDown} ref={this.createDashEventsTarget} onContextMenu={this.onContextMenu}> + return <div className="collectionCarousel3DView-outer" onClick={this.onClick} onPointerDown={this.onPointerDown} ref={this.createDashEventsTarget}> <div className="carousel-wrapper" style={{ transform: `translateX(calc(${translateX}%` }}> {this.content} </div> diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx index bd0e4fc9a..27aea4b99 100644 --- a/src/client/views/collections/CollectionCarouselView.tsx +++ b/src/client/views/collections/CollectionCarouselView.tsx @@ -14,6 +14,7 @@ import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; import { ContextMenu } from '../ContextMenu'; import { ObjectField } from '../../../fields/ObjectField'; import { returnFalse } from '../../../Utils'; +import { ScriptField } from '../../../fields/ScriptField'; type CarouselDocument = makeInterface<[typeof documentSchema, typeof collectionSchema]>; const CarouselDocument = makeInterface(documentSchema, collectionSchema); @@ -40,14 +41,16 @@ export class CollectionCarouselView extends CollectionSubView(CarouselDocument) this.layoutDoc._itemIndex = (NumCast(this.layoutDoc._itemIndex) - 1 + this.childLayoutPairs.length) % this.childLayoutPairs.length; } panelHeight = () => this.props.PanelHeight() - 50; + onContentDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick); + onContentClick = () => ScriptCast(this.layoutDoc.onChildClick); @computed get content() { const index = NumCast(this.layoutDoc._itemIndex); return !(this.childLayoutPairs?.[index]?.layout instanceof Doc) ? (null) : <> <div className="collectionCarouselView-image" key="image"> <ContentFittingDocumentView {...this.props} - onDoubleClick={ScriptCast(this.layoutDoc.onChildDoubleClick)} - onClick={ScriptCast(this.layoutDoc.onChildClick)} + onDoubleClick={this.onContentDoubleClick} + onClick={this.onContentClick} renderDepth={this.props.renderDepth + 1} LayoutTemplate={this.props.ChildLayoutTemplate} LayoutTemplateString={this.props.ChildLayoutString} @@ -83,30 +86,16 @@ export class CollectionCarouselView extends CollectionSubView(CarouselDocument) </>; } - - onContextMenu = (e: React.MouseEvent): void => { - // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout - if (!e.isPropagationStopped()) { - ContextMenu.Instance?.addItem({ - description: "Make Hero Image", event: () => { - const index = NumCast(this.layoutDoc._itemIndex); - (this.dataDoc || Doc.GetProto(this.props.Document)).hero = ObjectField.MakeCopy(this.childLayoutPairs[index].layout.data as ObjectField); - }, icon: "plus" - }); - } - } _downX = 0; _downY = 0; onPointerDown = (e: React.PointerEvent) => { this._downX = e.clientX; this._downY = e.clientY; - console.log("CAROUSEL down"); document.addEventListener("pointerup", this.onpointerup); } private _lastTap: number = 0; private _doubleTap = false; onpointerup = (e: PointerEvent) => { - console.log("CAROUSEL up"); this._doubleTap = (Date.now() - this._lastTap < 300 && e.button === 0 && Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2); this._lastTap = Date.now(); } @@ -119,7 +108,7 @@ export class CollectionCarouselView extends CollectionSubView(CarouselDocument) } render() { - return <div className="collectionCarouselView-outer" onClick={this.onClick} onPointerDown={this.onPointerDown} ref={this.createDashEventsTarget} onContextMenu={this.onContextMenu}> + return <div className="collectionCarouselView-outer" onClick={this.onClick} onPointerDown={this.onPointerDown} ref={this.createDashEventsTarget}> {this.content} {this.props.Document._chromeStatus !== "replaced" ? this.buttons : (null)} </div>; diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss index 18d642510..1895c06a1 100644 --- a/src/client/views/collections/CollectionDockingView.scss +++ b/src/client/views/collections/CollectionDockingView.scss @@ -1,5 +1,23 @@ @import "../../views/globalCssVariables.scss"; +.miniMap { + position: absolute; + overflow: hidden; + right: 10; + bottom: 10; + border: solid 1px; + box-shadow: black 0.4vw 0.4vw 0.8vw; + + .miniOverlay { + width: 100%; + height: 100%; + position: absolute; + .miniThumb { + background: #25252525; + position: absolute; + } + } +} .lm_title { margin-top: 3px; border-radius: 5px; diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 79c577b6d..53b2d5254 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -11,7 +11,7 @@ import { Id } from '../../../fields/FieldSymbols'; import { FieldId } from "../../../fields/RefField"; import { Cast, NumCast, StrCast } from "../../../fields/Types"; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, returnOne, returnTrue, Utils, returnZero, returnEmptyFilter, setupMoveUpEvents, returnFalse } from "../../../Utils"; +import { emptyFunction, returnOne, returnTrue, Utils, returnZero, returnEmptyFilter, setupMoveUpEvents, returnFalse, emptyPath, aggregateBounds } from "../../../Utils"; import { DocServer } from "../../DocServer"; import { Docs } from '../../documents/Documents'; import { DocumentManager } from '../../util/DocumentManager'; @@ -28,6 +28,9 @@ import { DockingViewButtonSelector } from './ParentDocumentSelector'; import React = require("react"); import { CollectionViewType } from './CollectionView'; import { SnappingManager } from '../../util/SnappingManager'; +import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; +import { listSpec } from '../../../fields/Schema'; +import { clamp } from 'lodash'; const _global = (window /* browser */ || global /* node */) as any; @observer @@ -390,6 +393,12 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp reactionDisposer?: Lambda; componentDidMount: () => void = () => { if (this._containerRef.current) { + const observer = new _global.ResizeObserver(action((entries: any) => { + for (const entry of entries) { + this.onResize(null as any); + } + })); + observer.observe(this._containerRef.current); this.reactionDisposer = reaction( () => this.props.Document.dockingConfig, () => { @@ -430,7 +439,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp const cur = this._containerRef.current; // bcz: since GoldenLayout isn't a React component itself, we need to notify it to resize when its document container's size has changed - this._goldenLayout && this._goldenLayout.updateSize(cur!.getBoundingClientRect().width, cur!.getBoundingClientRect().height); + this._goldenLayout?.updateSize(cur!.getBoundingClientRect().width, cur!.getBoundingClientRect().height); } @action @@ -646,16 +655,8 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp if (this.props.renderDepth > 0) { return <div style={{ width: "100%", height: "100%" }}>Nested workspaces can't be rendered</div>; } - return ( - <Measure offset onResize={this.onResize}> - {({ measureRef }) => - <div ref={measureRef}> - <div className="collectiondockingview-container" id="menuContainer" - onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} ref={this._containerRef} /> - </div> - } - </Measure> - ); + return <div className="collectiondockingview-container" id="menuContainer" + onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} ref={this._containerRef} />; } } @@ -664,7 +665,6 @@ interface DockedFrameProps { documentId: FieldId; glContainer: any; libraryPath: (FieldId[]); - backgroundColor?: (doc: Doc) => string | undefined; //collectionDockingView: CollectionDockingView } @observer @@ -818,35 +818,109 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { } } + @computed get renderContentBounds() { + const bounds = this._document ? Cast(this._document._renderContentBounds, listSpec("number"), [0, 0, this.returnMiniSize(), this.returnMiniSize()]) : [0, 0, 0, 0]; + const xbounds = bounds[2] - bounds[0]; + const ybounds = bounds[3] - bounds[1]; + const dim = Math.max(xbounds, ybounds); + return { l: bounds[0] + xbounds / 2 - dim / 2, t: bounds[1] + ybounds / 2 - dim / 2, cx: bounds[0] + xbounds / 2, cy: bounds[1] + ybounds / 2, dim }; + } + @computed get miniLeft() { return 50 + (NumCast(this._document?._panX) - this.renderContentBounds.cx) / this.renderContentBounds.dim * 100 - this.miniWidth / 2; } + @computed get miniTop() { return 50 + (NumCast(this._document?._panY) - this.renderContentBounds.cy) / this.renderContentBounds.dim * 100 - this.miniHeight / 2; } + @computed get miniWidth() { return this.panelWidth() / NumCast(this._document?._viewScale, 1) / this.renderContentBounds.dim * 100; } + @computed get miniHeight() { return this.panelHeight() / NumCast(this._document?._viewScale, 1) / this.renderContentBounds.dim * 100; } + childLayoutTemplate = () => Cast(this._document?.childLayoutTemplate, Doc, null); + returnMiniSize = () => NumCast(this._document?._miniMapSize, 150); + miniDown = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => { + this._document!._panX = clamp(NumCast(this._document!._panX) + delta[0] / this.returnMiniSize() * this.renderContentBounds.dim, this.renderContentBounds.l, this.renderContentBounds.l + this.renderContentBounds.dim); + this._document!._panY = clamp(NumCast(this._document!._panY) + delta[1] / this.returnMiniSize() * this.renderContentBounds.dim, this.renderContentBounds.t, this.renderContentBounds.t + this.renderContentBounds.dim); + return false; + }), emptyFunction, emptyFunction); + } + renderMiniMap() { + return <div className="miniMap" style={{ + width: this.returnMiniSize(), height: this.returnMiniSize(), background: StrCast(this._document!._backgroundColor, + StrCast(this._document!.backgroundColor, CollectionDockingView.Instance.props.backgroundColor?.(this._document!))), + }}> + <CollectionFreeFormView + Document={this._document!} + LibraryPath={emptyPath} + CollectionView={undefined} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} + ChildLayoutTemplate={this.childLayoutTemplate} // bcz: Ugh .. should probably be rendering a CollectionView or the minimap should be part of the collectionFreeFormView to avoid havin to set stuff like this. + noOverlay={true} // don't render overlay Docs since they won't scale + active={returnTrue} + select={emptyFunction} + dropAction={undefined} + isSelected={returnFalse} + dontRegisterView={true} + annotationsKey={""} + fieldKey={Doc.LayoutFieldKey(this._document!)} + bringToFront={emptyFunction} + rootSelected={returnTrue} + addDocument={returnFalse} + moveDocument={returnFalse} + removeDocument={returnFalse} + ContentScaling={returnOne} + PanelWidth={this.returnMiniSize} + PanelHeight={this.returnMiniSize} + NativeHeight={returnZero} + NativeWidth={returnZero} + ScreenToLocalTransform={this.ScreenToLocalTransform} + renderDepth={0} + whenActiveChanged={emptyFunction} + focus={emptyFunction} + backgroundColor={CollectionDockingView.Instance.props.backgroundColor} + addDocTab={this.addDocTab} + pinToPres={DockedFrameRenderer.PinDoc} + docFilters={returnEmptyFilter} + fitToBox={true} + /> + <div className="miniOverlay" onPointerDown={this.miniDown} > + <div className="miniThumb" style={{ + width: `${this.miniWidth}%`, + height: `${this.miniHeight}%`, + left: `${this.miniLeft}%`, + top: `${this.miniTop}%`, + }} + /> + </div> + </div>; + } @computed get docView() { TraceMobx(); if (!this._document) return (null); const document = this._document; - const resolvedDataDoc = !Doc.AreProtosEqual(this._document[DataSym], this._document) ? this._document[DataSym] : undefined;// document.layout instanceof Doc ? document : this._dataDoc; - return <DocumentView key={document[Id]} - LibraryPath={this._libraryPath} - Document={document} - DataDoc={resolvedDataDoc} - bringToFront={emptyFunction} - rootSelected={returnTrue} - addDocument={undefined} - removeDocument={undefined} - ContentScaling={this.contentScaling} - PanelWidth={this.panelWidth} - PanelHeight={this.panelHeight} - NativeHeight={this.nativeHeight} - NativeWidth={this.nativeWidth} - ScreenToLocalTransform={this.ScreenToLocalTransform} - renderDepth={0} - parentActive={returnTrue} - whenActiveChanged={emptyFunction} - focus={emptyFunction} - backgroundColor={CollectionDockingView.Instance.props.backgroundColor} - addDocTab={this.addDocTab} - pinToPres={DockedFrameRenderer.PinDoc} - docFilters={returnEmptyFilter} - ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} />; + const resolvedDataDoc = !Doc.AreProtosEqual(this._document[DataSym], this._document) ? this._document[DataSym] : undefined; + return <> + <DocumentView key={document[Id]} + LibraryPath={this._libraryPath} + Document={document} + DataDoc={resolvedDataDoc} + bringToFront={emptyFunction} + rootSelected={returnTrue} + addDocument={undefined} + removeDocument={undefined} + ContentScaling={this.contentScaling} + PanelWidth={this.panelWidth} + PanelHeight={this.panelHeight} + NativeHeight={this.nativeHeight} + NativeWidth={this.nativeWidth} + ScreenToLocalTransform={this.ScreenToLocalTransform} + renderDepth={0} + parentActive={returnTrue} + whenActiveChanged={emptyFunction} + focus={emptyFunction} + backgroundColor={CollectionDockingView.Instance.props.backgroundColor} + addDocTab={this.addDocTab} + pinToPres={DockedFrameRenderer.PinDoc} + docFilters={returnEmptyFilter} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} /> + {document._viewType === CollectionViewType.Freeform && !this._document?.hideMinimap ? this.renderMiniMap() : (null)} + </>; } render() { diff --git a/src/client/views/collections/CollectionLinearView.tsx b/src/client/views/collections/CollectionLinearView.tsx index 0bbe97ec9..cd6e60de6 100644 --- a/src/client/views/collections/CollectionLinearView.tsx +++ b/src/client/views/collections/CollectionLinearView.tsx @@ -16,6 +16,7 @@ import { Id } from '../../../fields/FieldSymbols'; import { DocumentLinksButton } from '../nodes/DocumentLinksButton'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { LinkDescriptionPopup } from '../nodes/LinkDescriptionPopup'; +import { Tooltip } from '@material-ui/core'; type LinearDocument = makeInterface<[typeof documentSchema,]>; @@ -110,17 +111,22 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { const flexDir: any = StrCast(this.Document.flexDirection); const backgroundColor = StrCast(this.props.Document.backgroundColor, "black"); const color = StrCast(this.props.Document.color, "white"); + + const menuOpener = <label htmlFor={`${guid}`} style={{ + background: backgroundColor === color ? "black" : backgroundColor, + // width: "18px", height: "18px", fontSize: "12.5px", + // transition: this.props.Document.linearViewIsExpanded ? "transform 0.2s" : "transform 0.5s", + // transform: this.props.Document.linearViewIsExpanded ? "" : "rotate(45deg)" + }} + onPointerDown={e => e.stopPropagation()} > + <p>+</p> + </label>; + return <div className="collectionLinearView-outer"> <div className="collectionLinearView" ref={this.createDashEventsTarget} > - <label htmlFor={`${guid}`} title="Close Menu" style={{ - background: backgroundColor === color ? "black" : backgroundColor, - // width: "18px", height: "18px", fontSize: "12.5px", - // transition: this.props.Document.linearViewIsExpanded ? "transform 0.2s" : "transform 0.5s", - // transform: this.props.Document.linearViewIsExpanded ? "" : "rotate(45deg)" - }} - onPointerDown={e => e.stopPropagation()} > - <p>+</p> - </label> + <Tooltip title={<><div className="dash-tooltip">{BoolCast(this.props.Document.linearViewIsExpanded) ? "Close menu" : "Open menu"}</div></>} placement="top"> + {menuOpener} + </Tooltip> <input id={`${guid}`} type="checkbox" checked={BoolCast(this.props.Document.linearViewIsExpanded)} ref={this.addMenuToggle} onChange={action((e: any) => this.props.Document.linearViewIsExpanded = this.addMenuToggle.current!.checked)} /> @@ -170,12 +176,21 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { }} onPointerDown={e => e.stopPropagation()} > <span className="bottomPopup-text" > - Creating link from: {DocumentLinksButton.AnnotationId ? "Annotation in" : ""} {DocumentLinksButton.StartLink.title} </span> - <span className="bottomPopup-descriptions" onClick={this.changeDescriptionSetting} - > Labels: {LinkDescriptionPopup.showDescriptions ? LinkDescriptionPopup.showDescriptions : "ON"} + Creating link from: {DocumentLinksButton.AnnotationId ? "Annotation in" : ""} {DocumentLinksButton.StartLink.title} </span> - <span className="bottomPopup-exit" onClick={this.exitLongLinks} - >Exit</span> + + <Tooltip title={<><div className="dash-tooltip">{LinkDescriptionPopup.showDescriptions ? "Turn off description pop-up" : + "Turn on description pop-up"} </div></>} placement="top"> + <span className="bottomPopup-descriptions" onClick={this.changeDescriptionSetting}> + Labels: {LinkDescriptionPopup.showDescriptions ? LinkDescriptionPopup.showDescriptions : "ON"} + </span> + </Tooltip> + + <Tooltip title={<><div className="dash-tooltip">Exit link clicking mode </div></>} placement="top"> + <span className="bottomPopup-exit" onClick={this.exitLongLinks}> + Clear + </span> + </Tooltip> {/* <FontAwesomeIcon icon="times-circle" size="lg" style={{ color: "red" }} onClick={this.exitLongLinks} /> */} diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx index 627b22417..9a7ea2c93 100644 --- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx +++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx @@ -84,7 +84,6 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr @undoBatch rowDrop = action((e: Event, de: DragManager.DropEvent) => { - console.log("masronry row drop"); this._createAliasSelected = false; if (de.complete.docDragData) { (this.props.parent.Document.dropConverter instanceof ScriptField) && diff --git a/src/client/views/collections/CollectionViewChromes.scss b/src/client/views/collections/CollectionMenu.scss index b1e8d20ad..9da204787 100644 --- a/src/client/views/collections/CollectionViewChromes.scss +++ b/src/client/views/collections/CollectionMenu.scss @@ -1,35 +1,47 @@ @import "../globalCssVariables"; -@import '~js-datepicker/dist/datepicker.min.css'; -.collectionViewChrome-cont { - position: absolute; + +.collectionMenu-cont { + position:relative; + display:inline-flex; width: 100%; opacity: 0.9; z-index: 9001; transition: top .5s; - background: lightgrey; + background: #323232; + color: white; transform-origin: top left; + top: 0; + width:100%; + + .antimodeMenu-button { + padding: 0; + width: 30px; + display: flex; + svg { + margin:auto; + } + } - .collectionViewChrome { + .collectionMenu { display: flex; padding-bottom: 1px; height: 32px; border-bottom: .5px solid rgb(180, 180, 180); overflow: visible; z-index: 9001; + border: unset; .collectionViewBaseChrome { display: flex; .collectionViewBaseChrome-viewPicker { font-size: 75%; - //text-transform: uppercase; - //letter-spacing: 2px; - background: rgb(238, 238, 238); - color: grey; + background: #323232; outline-color: black; + color: white; border: none; - //padding: 12px 10px 11px 10px; + border-right: solid gray 1px; } .collectionViewBaseChrome-viewPicker:active { @@ -52,21 +64,22 @@ margin-left: 3px; margin-right: 0px; font-size: 75%; - background: rgb(238, 238, 238); + background: #323232; + color: white; border: none; - color: grey; + border-right: solid gray 1px; } .commandEntry-outerDiv { pointer-events: all; - background-color: gray; + background-color: #323232; display: flex; flex-direction: row; height: 30px; .commandEntry-drop { color: white; - width: 25px; + width: 30px; margin-top: auto; margin-bottom: auto; } @@ -75,7 +88,7 @@ .collectionViewBaseChrome-collapse { transition: all .5s, opacity 0.3s; position: absolute; - width: 40px; + width: 30px; transform-origin: top left; pointer-events: all; // margin-top: 10px; @@ -94,27 +107,25 @@ color: grey; margin-top: auto; margin-bottom: auto; - margin-left: 5px; } - - .collectionViewBaseChrome-viewModes { - margin-left: 25px; - } - .collectionViewBaseChrome-viewSpecs { margin-left: 5px; display: grid; + border: none; + border-right: solid gray 1px; .collectionViewBaseChrome-filterIcon { position: relative; display: flex; margin: auto; - background: gray; + background: #323232; color: white; width: 30px; height: 30px; align-items: center; justify-content: center; + border: none; + border-right: solid gray 1px; } .collectionViewBaseChrome-viewSpecsInput { @@ -190,11 +201,12 @@ font-size: 75%; //text-transform: uppercase; //letter-spacing: 2px; - background: rgb(238, 238, 238); - color: grey; + background: #121721; + color: white; outline-color: black; + color: white; border: none; - //padding: 12px 10px 11px 10px; + border-right: solid gray 1px; } .collectionGridViewChrome-viewPicker:active { @@ -297,29 +309,55 @@ } } -.collectionFreeFormViewChrome-cont { - width: 60px; - display: flex; +.collectionFreeFormMenu-cont { + display: inline-flex; position: relative; align-items: center; + .antimodeMenu-button { + text-align: center; + display: block; + } + .color-previewI { + width: 80%; + height: 20%; + bottom: 0; + position: absolute; + } + .color-previewII { + width: 80%; + height: 80%; + margin-left: 10%; + } + + .btn-group { + display: grid; + grid-template-columns: auto auto auto auto; + margin: auto; + /* Make the buttons appear below each other */ + } + + .btn-draw { + display: inline-flex; + margin: auto; + /* Make the buttons appear below each other */ + } + .fwdKeyframe, .numKeyframe, .backKeyframe { cursor: pointer; - position: absolute; + position: relative; width: 20; height: 30; bottom: 0; - background: gray; - display: flex; + background: #323232; + display: inline-flex; align-items: center; color: white; } .backKeyframe { - left: 0; - svg { display: block; margin: auto; @@ -327,19 +365,16 @@ } .numKeyframe { - left: 20; - display: flex; flex-direction: column; - padding: 5px; + padding-top: 5px; } .fwdKeyframe { - left: 40; - svg { display: block; margin: auto; } + border-right: solid gray 1px; } } diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionMenu.tsx index 7f1fe7649..992c1f600 100644 --- a/src/client/views/collections/CollectionViewChromes.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -1,74 +1,107 @@ -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, observable, runInAction, Lambda } from "mobx"; +import React = require("react"); +import { FontAwesomeIcon, FontAwesomeIconProps } from "@fortawesome/react-fontawesome"; +import { action, computed, observable, reaction, runInAction, Lambda } from "mobx"; import { observer } from "mobx-react"; -import * as React from "react"; import { Doc, DocListCast, Opt } from "../../../fields/Doc"; -import { Id } from "../../../fields/FieldSymbols"; -import { List } from "../../../fields/List"; -import { listSpec } from "../../../fields/Schema"; -import { BoolCast, Cast, NumCast, StrCast } from "../../../fields/Types"; -import { Utils, emptyFunction, setupMoveUpEvents } from "../../../Utils"; -import { DragManager } from "../../util/DragManager"; +import { BoolCast, Cast, StrCast, NumCast } from "../../../fields/Types"; +import AntimodeMenu from "../AntimodeMenu"; +import "./CollectionMenu.scss"; import { undoBatch } from "../../util/UndoManager"; -import { EditableView } from "../EditableView"; -import { COLLECTION_BORDER_WIDTH } from "../globalCssVariables.scss"; -import { CollectionViewType } from "./CollectionView"; -import { CollectionView } from "./CollectionView"; -import "./CollectionViewChromes.scss"; +import { CollectionViewType, CollectionView, COLLECTION_BORDER_WIDTH } from "./CollectionView"; +import { emptyFunction, setupMoveUpEvents, Utils } from "../../../Utils"; +import { DragManager } from "../../util/DragManager"; import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; +import { List } from "../../../fields/List"; +import { EditableView } from "../EditableView"; +import { Id } from "../../../fields/FieldSymbols"; +import { listSpec } from "../../../fields/Schema"; +import FormatShapePane from "./collectionFreeForm/FormatShapePane"; +import { ActiveFillColor, SetActiveInkWidth, ActiveInkColor, SetActiveBezierApprox, SetActiveArrowEnd, SetActiveArrowStart, SetActiveFillColor, SetActiveInkColor } from "../InkingStroke"; +import GestureOverlay from "../GestureOverlay"; +import { InkTool } from "../../../fields/InkField"; +import { DocumentType } from "../../documents/DocumentTypes"; +import { Document } from "../../../fields/documentSchemas"; +import { SelectionManager } from "../../util/SelectionManager"; +import { DocumentView } from "../nodes/DocumentView"; +import { ColorState } from "react-color"; -interface CollectionViewChromeProps { - CollectionView: CollectionView; - type: CollectionViewType; - collapse?: (value: boolean) => any; - PanelWidth: () => number; +@observer +export default class CollectionMenu extends AntimodeMenu { + static Instance: CollectionMenu; + + @observable SelectedCollection: CollectionView | undefined; + + constructor(props: Readonly<{}>) { + super(props); + CollectionMenu.Instance = this; + this._canFade = false; // don't let the inking menu fade away + this.Pinned = Cast(Doc.UserDoc()["menuCollections-pinned"], "boolean", true); + this.jumpTo(300, 300); + } + + @action + toggleMenuPin = (e: React.MouseEvent) => { + Doc.UserDoc()["menuCollections-pinned"] = this.Pinned = !this.Pinned; + if (!this.Pinned && this._left < 0) { + this.jumpTo(300, 300); + } + } + + render() { + const button = <button className="antimodeMenu-button" key="pin menu" title="Pin menu" onClick={this.toggleMenuPin} style={{ backgroundColor: "#121721" }}> + <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.1s", transform: `rotate(${this.Pinned ? 45 : 0}deg)` }} /> + </button>; + + return this.getElement(!this.SelectedCollection ? [button] : + [<CollectionViewBaseChrome key="chrome" CollectionView={this.SelectedCollection} type={StrCast(this.SelectedCollection.props.Document._viewType) as CollectionViewType} />, + button]); + } } -interface Filter { - key: string; - value: string; - contains: boolean; +interface CollectionMenuProps { + CollectionView: CollectionView; + type: CollectionViewType; } const stopPropagation = (e: React.SyntheticEvent) => e.stopPropagation(); @observer -export class CollectionViewBaseChrome extends React.Component<CollectionViewChromeProps> { +export class CollectionViewBaseChrome extends React.Component<CollectionMenuProps> { //(!)?\(\(\(doc.(\w+) && \(doc.\w+ as \w+\).includes\(\"(\w+)\"\) get target() { return this.props.CollectionView.props.Document; } _templateCommand = { - params: ["target", "source"], title: "=> item view", - script: "this.target.childLayout = getDocTemplate(this.source?.[0])", - immediate: undoBatch((source: Doc[]) => source.length && (this.target.childLayout = Doc.getDocTemplate(source?.[0]))), + params: ["target", "source"], title: "item view", + script: "this.target.childLayoutTemplate = getDocTemplate(this.source?.[0])", + immediate: undoBatch((source: Doc[]) => source.length && (this.target.childLayoutTemplate = Doc.getDocTemplate(source?.[0]))), initialize: emptyFunction, }; _narrativeCommand = { - params: ["target", "source"], title: "=> child click view", + params: ["target", "source"], title: "child click view", script: "this.target.childClickedOpenTemplateView = getDocTemplate(this.source?.[0])", immediate: undoBatch((source: Doc[]) => source.length && (this.target.childClickedOpenTemplateView = Doc.getDocTemplate(source?.[0]))), initialize: emptyFunction, }; _contentCommand = { - params: ["target", "source"], title: "=> clear content", + params: ["target", "source"], title: "clear content", script: "getProto(this.target).data = copyField(this.source);", immediate: undoBatch((source: Doc[]) => Doc.GetProto(this.target).data = new List<Doc>(source)), // Doc.aliasDocs(source), initialize: emptyFunction, }; _viewCommand = { - params: ["target"], title: "=> reset view", - script: "this.target._panX = this.restoredPanX; this.target._panY = this.restoredPanY; this.target.scale = this.restoredScale;", - immediate: undoBatch((source: Doc[]) => { this.target._panX = 0; this.target._panY = 0; this.target.scale = 1; }), - initialize: (button: Doc) => { button.restoredPanX = this.target._panX; button.restoredPanY = this.target._panY; button.restoredScale = this.target.scale; }, + params: ["target"], title: "bookmark view", + script: "this.target._panX = this['target-panX']; this.target._panY = this['target-panY']; this.target._viewScale = this['target-viewScale'];", + immediate: undoBatch((source: Doc[]) => { this.target._panX = 0; this.target._panY = 0; this.target._viewScale = 1; }), + initialize: (button: Doc) => { button['target-panX'] = this.target._panX; button['target-panY'] = this.target._panY; button['target-viewScale'] = this.target._viewScale; }, }; _clusterCommand = { - params: ["target"], title: "=> fit content", + params: ["target"], title: "fit content", script: "this.target._fitToBox = !this.target._fitToBox;", immediate: undoBatch((source: Doc[]) => this.target._fitToBox = !this.target._fitToBox), initialize: emptyFunction }; _fitContentCommand = { - params: ["target"], title: "=> toggle clusters", + params: ["target"], title: "toggle clusters", script: "this.target.useClusters = !this.target.useClusters;", immediate: undoBatch((source: Doc[]) => this.target.useClusters = !this.target.useClusters), initialize: emptyFunction @@ -99,14 +132,6 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro componentDidMount = action(() => { this._currentKey = this._currentKey || (this._buttonizableCommands.length ? this._buttonizableCommands[0]?.title : ""); - // chrome status is one of disabled, collapsed, or visible. this determines initial state from document - switch (this.props.CollectionView.props.Document._chromeStatus) { - case "disabled": - throw new Error("how did you get here, if chrome status is 'disabled' on a collection, a chrome shouldn't even be instantiated!"); - case "collapsed": - this.props.collapse?.(true); - break; - } }); @undoBatch @@ -130,93 +155,16 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro this.document._facetWidth = 0; } - // @action - // openDatePicker = (e: React.PointerEvent) => { - // if (this._picker) { - // this._picker.alwaysShow = true; - // this._picker.show(); - // // TODO: calendar is offset when zoomed in/out - // // this._picker.calendar.style.position = "absolute"; - // // let transform = this.props.CollectionView.props.ScreenToLocalTransform(); - // // let x = parseInt(this._picker.calendar.style.left) / transform.Scale; - // // let y = parseInt(this._picker.calendar.style.top) / transform.Scale; - // // this._picker.calendar.style.left = x; - // // this._picker.calendar.style.top = y; - - // e.stopPropagation(); - // } - // } - - // <input className="collectionViewBaseChrome-viewSpecsMenu-rowRight" - // id={Utils.GenerateGuid()} - // ref={this.datePickerRef} - // value={this._dateValue instanceof Date ? this._dateValue.toLocaleDateString() : this._dateValue} - // onChange={(e) => runInAction(() => this._dateValue = e.target.value)} - // onPointerDown={this.openDatePicker} - // placeholder="Value" /> - // @action.bound - // applyFilter = (e: React.MouseEvent) => { - // const keyRestrictionScript = "(" + this._keyRestrictions.map(i => i[1]).filter(i => i.length > 0).join(" && ") + ")"; - // const yearOffset = this._dateWithinValue[1] === 'y' ? 1 : 0; - // const monthOffset = this._dateWithinValue[1] === 'm' ? parseInt(this._dateWithinValue[0]) : 0; - // const weekOffset = this._dateWithinValue[1] === 'w' ? parseInt(this._dateWithinValue[0]) : 0; - // const dayOffset = (this._dateWithinValue[1] === 'd' ? parseInt(this._dateWithinValue[0]) : 0) + weekOffset * 7; - // let dateRestrictionScript = ""; - // if (this._dateValue instanceof Date) { - // const lowerBound = new Date(this._dateValue.getFullYear() - yearOffset, this._dateValue.getMonth() - monthOffset, this._dateValue.getDate() - dayOffset); - // const upperBound = new Date(this._dateValue.getFullYear() + yearOffset, this._dateValue.getMonth() + monthOffset, this._dateValue.getDate() + dayOffset + 1); - // dateRestrictionScript = `((doc.creationDate as any).date >= ${lowerBound.valueOf()} && (doc.creationDate as any).date <= ${upperBound.valueOf()})`; - // } - // else { - // const createdDate = new Date(this._dateValue); - // if (!isNaN(createdDate.getTime())) { - // const lowerBound = new Date(createdDate.getFullYear() - yearOffset, createdDate.getMonth() - monthOffset, createdDate.getDate() - dayOffset); - // const upperBound = new Date(createdDate.getFullYear() + yearOffset, createdDate.getMonth() + monthOffset, createdDate.getDate() + dayOffset + 1); - // dateRestrictionScript = `((doc.creationDate as any).date >= ${lowerBound.valueOf()} && (doc.creationDate as any).date <= ${upperBound.valueOf()})`; - // } - // } - // const fullScript = dateRestrictionScript.length || keyRestrictionScript.length ? dateRestrictionScript.length ? - // `${dateRestrictionScript} ${keyRestrictionScript.length ? "&&" : ""} (${keyRestrictionScript})` : - // `(${keyRestrictionScript}) ${dateRestrictionScript.length ? "&&" : ""} ${dateRestrictionScript}` : - // "true"; - - // this.props.CollectionView.props.Document.viewSpecScript = ScriptField.MakeFunction(fullScript, { doc: Doc.name }); - // } - - // datePickerRef = (node: HTMLInputElement) => { - // if (node) { - // try { - // this._picker = datepicker("#" + node.id, { - // disabler: (date: Date) => date > new Date(), - // onSelect: (instance: any, date: Date) => runInAction(() => {}), // this._dateValue = date), - // dateSelected: new Date() - // }); - // } catch (e) { - // console.log("date picker exception:" + e); - // } - // } - // } - - - @action - toggleCollapse = () => { - this.document._chromeStatus = this.document._chromeStatus === "enabled" ? "collapsed" : "enabled"; - if (this.props.collapse) { - this.props.collapse(this.props.CollectionView.props.Document._chromeStatus !== "enabled"); - } - } @computed get subChrome() { - const collapsed = this.document._chromeStatus !== "enabled"; - if (collapsed) return null; switch (this.props.type) { - case CollectionViewType.Freeform: return (<CollectionFreeFormViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />); - case CollectionViewType.Stacking: return (<CollectionStackingViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />); - case CollectionViewType.Schema: return (<CollectionSchemaViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />); - case CollectionViewType.Tree: return (<CollectionTreeViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />); - case CollectionViewType.Masonry: return (<CollectionStackingViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />); - case CollectionViewType.Carousel3D: return (<Collection3DCarouselViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />); - case CollectionViewType.Grid: return (<CollectionGridViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />); + case CollectionViewType.Freeform: return (<CollectionFreeFormViewChrome key="collchrome" CollectionView={this.props.CollectionView} type={this.props.type} />); + case CollectionViewType.Stacking: return (<CollectionStackingViewChrome key="collchrome" CollectionView={this.props.CollectionView} type={this.props.type} />); + case CollectionViewType.Schema: return (<CollectionSchemaViewChrome key="collchrome" CollectionView={this.props.CollectionView} type={this.props.type} />); + case CollectionViewType.Tree: return (<CollectionTreeViewChrome key="collchrome" CollectionView={this.props.CollectionView} type={this.props.type} />); + case CollectionViewType.Masonry: return (<CollectionStackingViewChrome key="collchrome" CollectionView={this.props.CollectionView} type={this.props.type} />); + case CollectionViewType.Carousel3D: return (<Collection3DCarouselViewChrome key="collchrome" CollectionView={this.props.CollectionView} type={this.props.type} />); + case CollectionViewType.Grid: return (<CollectionGridViewChrome key="collchrome" CollectionView={this.props.CollectionView} type={this.props.type} />); default: return null; } } @@ -270,15 +218,13 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro } @computed get templateChrome() { - const collapsed = this.props.CollectionView.props.Document._chromeStatus !== "enabled"; - return <div className="collectionViewBaseChrome-template" ref={this.createDropTarget} style={{ display: collapsed ? "none" : undefined }}> + return <div className="collectionViewBaseChrome-template" ref={this.createDropTarget} > <div className="commandEntry-outerDiv" title="drop document to apply or drag to create button" ref={this._commandRef} onPointerDown={this.dragCommandDown}> - <div className="commandEntry-drop"> - <FontAwesomeIcon icon="bullseye" size="2x" /> - </div> + <button className={"antimodeMenu-button"} > + <FontAwesomeIcon icon="bullseye" size="lg" /> + </button> <select - className="collectionViewBaseChrome-cmdPicker" onPointerDown={stopPropagation} onChange={this.commandChanged} value={this._currentKey} - style={{ width: this.props.PanelWidth() < 300 ? 15 : undefined }}> + className="collectionViewBaseChrome-cmdPicker" onPointerDown={stopPropagation} onChange={this.commandChanged} value={this._currentKey}> <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key={"empty"} value={""} /> {this._buttonizableCommands.map(cmd => <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key={cmd.title} value={cmd.title}>{cmd.title}</option> @@ -289,14 +235,13 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro } @computed get viewModes() { - const collapsed = this.props.CollectionView.props.Document._chromeStatus !== "enabled"; - return <div className="collectionViewBaseChrome-viewModes" style={{ display: collapsed ? "none" : undefined }}> + return <div className="collectionViewBaseChrome-viewModes" > <div className="commandEntry-outerDiv" title="drop document to apply or drag to create button" ref={this._viewRef} onPointerDown={this.dragViewDown}> - <div className="commandEntry-drop"> - <FontAwesomeIcon icon="bullseye" size="2x" /> - </div> + <button className={"antimodeMenu-button"}> + <FontAwesomeIcon icon="bullseye" size="lg" /> + </button> <select - className="collectionViewBaseChrome-viewPicker" style={{ width: this.props.PanelWidth() < 300 ? 15 : undefined }} + className="collectionViewBaseChrome-viewPicker" onPointerDown={stopPropagation} onChange={this.viewChanged} value={StrCast(this.props.CollectionView.props.Document._viewType)}> @@ -315,34 +260,17 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro } render() { - const collapsed = this.props.CollectionView.props.Document._chromeStatus !== "enabled"; - const scale = Math.min(1, this.props.CollectionView.props.ScreenToLocalTransform()?.Scale); return ( - <div className="collectionViewChrome-cont" style={{ - top: collapsed ? -70 : 0, height: collapsed ? 0 : undefined, - transform: collapsed ? "" : `scale(${scale})`, - width: `${this.props.PanelWidth() / scale}px` - }}> - <div className="collectionViewChrome" style={{ border: "unset", pointerEvents: collapsed ? "none" : undefined }}> + <div className="collectionMenu-cont" > + <div className="collectionMenu"> <div className="collectionViewBaseChrome"> - <button className="collectionViewBaseChrome-collapse" - style={{ - top: collapsed ? 70 : 10, - transform: `rotate(${collapsed ? 180 : 0}deg) scale(0.5) translate(${collapsed ? "-100%, -100%" : "0, 0"})`, - opacity: 0.9, - display: (collapsed && !this.props.CollectionView.props.isSelected()) ? "none" : undefined, - left: (collapsed ? 0 : "unset"), - }} - title="Collapse collection chrome" onClick={this.toggleCollapse}> - <FontAwesomeIcon icon="caret-up" size="2x" /> - </button> {this.viewModes} - <div className="collectionViewBaseChrome-viewSpecs" title="filter documents to show" style={{ display: collapsed ? "none" : "grid" }}> - <div className="collectionViewBaseChrome-filterIcon" onPointerDown={this.toggleViewSpecs} > - <FontAwesomeIcon icon="filter" size="2x" /> - </div> - </div> {this.templateChrome} + <div className="collectionViewBaseChrome-viewSpecs" title="filter documents to show" style={{ display: "grid" }}> + <button className={"antimodeMenu-button"} onClick={this.toggleViewSpecs} > + <FontAwesomeIcon icon="filter" size="lg" /> + </button> + </div> </div> {this.subChrome} </div> @@ -352,8 +280,12 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro } @observer -export class CollectionFreeFormViewChrome extends React.Component<CollectionViewChromeProps> { - +export class CollectionFreeFormViewChrome extends React.Component<CollectionMenuProps> { + public static Instance: CollectionFreeFormViewChrome; + constructor(props: any) { + super(props); + CollectionFreeFormViewChrome.Instance = this; + } get Document() { return this.props.CollectionView.props.Document; } @computed get dataField() { return this.props.CollectionView.props.Document[Doc.LayoutFieldKey(this.props.CollectionView.props.Document)]; @@ -364,7 +296,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView @undoBatch @action nextKeyframe = (): void => { - const currentFrame = NumCast(this.Document.currentFrame); + const currentFrame = Cast(this.Document.currentFrame, "number", null); if (currentFrame === undefined) { this.Document.currentFrame = 0; CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0); @@ -376,7 +308,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView @undoBatch @action prevKeyframe = (): void => { - const currentFrame = NumCast(this.Document.currentFrame); + const currentFrame = Cast(this.Document.currentFrame, "number", null); if (currentFrame === undefined) { this.Document.currentFrame = 0; CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0); @@ -384,9 +316,155 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView CollectionFreeFormDocumentView.gotoKeyframe(this.childDocs.slice()); this.Document.currentFrame = Math.max(0, (currentFrame || 0) - 1); } + @undoBatch + @action + miniMap = (): void => { + this.Document.hideMinimap = !this.Document.hideMinimap; + } + private _palette = ["#D0021B", "#F5A623", "#F8E71C", "#8B572A", "#7ED321", "#417505", "#9013FE", "#4A90E2", "#50E3C2", "#B8E986", "#000000", "#4A4A4A", "#9B9B9B", "#FFFFFF", ""]; + private _width = ["1", "5", "10", "100"]; + private _draw = ["⎯", "→", "↔︎", "∿", "↝", "↭", "ロ", "O", "∆"]; + private _head = ["", "", "arrow", "", "", "arrow", "", "", ""]; + private _end = ["", "arrow", "arrow", "", "arrow", "arrow", "", "", ""]; + private _shape = ["line", "line", "line", "", "", "", "rectangle", "circle", "triangle"]; + + @observable _shapesNum = this._shape.length; + @observable _selected = this._shapesNum; + + @observable _keepMode = false; + + @observable _colorBtn = false; + @observable _widthBtn = false; + @observable _fillBtn = false; + + @action + clearKeep() { this._selected = this._shapesNum; } + + @action + changeColor = (color: string, type: string) => { + const col: ColorState = { + hex: color, hsl: { a: 0, h: 0, s: 0, l: 0, source: "" }, hsv: { a: 0, h: 0, s: 0, v: 0, source: "" }, + rgb: { a: 0, r: 0, b: 0, g: 0, source: "" }, oldHue: 0, source: "", + }; + if (type === "color") { + SetActiveInkColor(Utils.colorString(col)); + } else if (type === "fill") { + SetActiveFillColor(Utils.colorString(col)); + } + } + + @action + editProperties = (value: any, field: string) => { + SelectionManager.SelectedDocuments().forEach(action((element: DocumentView) => { + const doc = Document(element.rootDoc); + if (doc.type === DocumentType.INK) { + switch (field) { + case "width": doc.strokeWidth = Number(value); break; + case "color": doc.color = String(value); break; + case "fill": doc.fillColor = String(value); break; + case "dash": doc.strokeDash = value; + } + } + })); + } + + @computed get drawButtons() { + const func = action((i: number, keep: boolean) => { + this._keepMode = keep; + if (this._selected !== i) { + this._selected = i; + Doc.SetSelectedTool(InkTool.Pen); + SetActiveArrowStart(this._head[i]); + SetActiveArrowEnd(this._end[i]); + SetActiveBezierApprox("300"); + + GestureOverlay.Instance.InkShape = this._shape[i]; + } else { + this._selected = this._shapesNum; + Doc.SetSelectedTool(InkTool.None); + SetActiveArrowStart(""); + SetActiveArrowEnd(""); + GestureOverlay.Instance.InkShape = ""; + SetActiveBezierApprox("0"); + } + }); + return <div className="btn-draw" key="draw"> + {this._draw.map((icon, i) => + <button className="antimodeMenu-button" key={icon} onPointerDown={() => func(i, false)} onDoubleClick={() => func(i, true)} + style={{ backgroundColor: i === this._selected ? "121212" : "", fontSize: "20" }}> + {this._draw[i]} + </button>)} + </div>; + } + + toggleButton = (key: string, value: boolean, setter: () => {}, icon: FontAwesomeIconProps["icon"], ele: JSX.Element | null) => { + return <button className="antimodeMenu-button" key={key} title={key} + onPointerDown={action(e => setter())} + style={{ backgroundColor: value ? "121212" : "" }}> + <FontAwesomeIcon icon={icon} size="lg" /> + {ele} + </button>; + } + + @computed get widthPicker() { + const widthPicker = this.toggleButton("stroke width", this._widthBtn, () => this._widthBtn = !this._widthBtn, "bars", null); + return !this._widthBtn ? widthPicker : + <div className="btn2-group" key="width"> + {widthPicker} + {this._width.map(wid => + <button className="antimodeMenu-button" key={wid} + onPointerDown={action(() => { SetActiveInkWidth(wid); this._widthBtn = false; this.editProperties(wid, "width"); })} + style={{ backgroundColor: this._widthBtn ? "121212" : "", zIndex: 1001 }}> + {wid} + </button>)} + </div>; + } + + @computed get colorPicker() { + const colorPicker = this.toggleButton("stroke color", this._colorBtn, () => this._colorBtn = !this._colorBtn, "pen-nib", + <div className="color-previewI" style={{ backgroundColor: ActiveInkColor() ?? "121212" }} />); + return !this._colorBtn ? colorPicker : + <div className="btn-group" key="color"> + {colorPicker} + {this._palette.map(color => + <button className="antimodeMenu-button" key={color} + onPointerDown={action(() => { this.changeColor(color, "color"); this._colorBtn = false; this.editProperties(color, "color"); })} + style={{ backgroundColor: this._colorBtn ? "121212" : "", zIndex: 1001 }}> + {/* <FontAwesomeIcon icon="pen-nib" size="lg" /> */} + <div className="color-previewII" style={{ backgroundColor: color }} /> + </button>)} + </div>; + } + @computed get fillPicker() { + const fillPicker = this.toggleButton("shape fill color", this._fillBtn, () => this._fillBtn = !this._fillBtn, "fill-drip", + <div className="color-previewI" style={{ backgroundColor: ActiveFillColor() ?? "121212" }} />); + return !this._fillBtn ? fillPicker : + <div className="btn-group" key="fill" > + {fillPicker} + {this._palette.map(color => + <button className="antimodeMenu-button" key={color} + onPointerDown={action(() => { this.changeColor(color, "fill"); this._fillBtn = false; this.editProperties(color, "fill"); })} + style={{ backgroundColor: this._fillBtn ? "121212" : "", zIndex: 1001 }}> + <div className="color-previewII" style={{ backgroundColor: color }}></div> + </button>)} + + </div>; + } + + @computed get formatPane() { + return <button className="antimodeMenu-button" key="format" title="toggle foramatting pane" + onPointerDown={action(e => FormatShapePane.Instance.Pinned = !FormatShapePane.Instance.Pinned)} + style={{ backgroundColor: this._fillBtn ? "121212" : "" }}> + <FontAwesomeIcon icon="chevron-right" size="lg" /> + </button>; + } + render() { return this.Document.isAnnotationOverlay ? (null) : - <div className="collectionFreeFormViewChrome-cont"> + <div className="collectionFreeFormMenu-cont"> + <div key="map" title="mini map" className="backKeyframe" onClick={this.miniMap}> + <FontAwesomeIcon icon={"map"} size={"lg"} /> + </div> <div key="back" title="back frame" className="backKeyframe" onClick={this.prevKeyframe}> <FontAwesomeIcon icon={"caret-left"} size={"lg"} /> </div> @@ -397,12 +475,17 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView <div key="fwd" title="forward frame" className="fwdKeyframe" onClick={this.nextKeyframe}> <FontAwesomeIcon icon={"caret-right"} size={"lg"} /> </div> + + {this.widthPicker} + {this.colorPicker} + {this.fillPicker} + {this.drawButtons} + {this.formatPane} </div>; } } - @observer -export class CollectionStackingViewChrome extends React.Component<CollectionViewChromeProps> { +export class CollectionStackingViewChrome extends React.Component<CollectionMenuProps> { @observable private _currentKey: string = ""; @observable private suggestions: string[] = []; @@ -502,7 +585,7 @@ export class CollectionStackingViewChrome extends React.Component<CollectionView @observer -export class CollectionSchemaViewChrome extends React.Component<CollectionViewChromeProps> { +export class CollectionSchemaViewChrome extends React.Component<CollectionMenuProps> { // private _textwrapAllRows: boolean = Cast(this.props.CollectionView.props.Document.textwrappedSchemaRows, listSpec("string"), []).length > 0; @undoBatch @@ -549,7 +632,7 @@ export class CollectionSchemaViewChrome extends React.Component<CollectionViewCh } @observer -export class CollectionTreeViewChrome extends React.Component<CollectionViewChromeProps> { +export class CollectionTreeViewChrome extends React.Component<CollectionMenuProps> { get sortAscending() { return this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldKey + "-sortAscending"]; @@ -585,7 +668,7 @@ export class CollectionTreeViewChrome extends React.Component<CollectionViewChro // Enter scroll speed for 3D Carousel @observer -export class Collection3DCarouselViewChrome extends React.Component<CollectionViewChromeProps> { +export class Collection3DCarouselViewChrome extends React.Component<CollectionMenuProps> { @computed get scrollSpeed() { return this.props.CollectionView.props.Document._autoScrollSpeed; } @@ -624,7 +707,7 @@ export class Collection3DCarouselViewChrome extends React.Component<CollectionVi * Chrome for grid view. */ @observer -export class CollectionGridViewChrome extends React.Component<CollectionViewChromeProps> { +export class CollectionGridViewChrome extends React.Component<CollectionMenuProps> { private clicked: boolean = false; private entered: boolean = false; @@ -770,7 +853,7 @@ export class CollectionGridViewChrome extends React.Component<CollectionViewChro </span> <select className="collectionGridViewChrome-viewPicker" - style={{ marginRight: 5, width: this.props.PanelWidth() < 300 ? 25 : undefined }} + style={{ marginRight: 5 }} onPointerDown={stopPropagation} onChange={this.changeCompactType} value={StrCast(this.props.CollectionView.props.Document.gridStartCompaction, StrCast(this.props.CollectionView.props.Document.gridCompaction))}> @@ -797,4 +880,4 @@ export class CollectionGridViewChrome extends React.Component<CollectionViewChro </div> ); } -} +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx index d76b6d204..eecaf7672 100644 --- a/src/client/views/collections/CollectionSchemaCells.tsx +++ b/src/client/views/collections/CollectionSchemaCells.tsx @@ -151,7 +151,6 @@ export class CollectionSchemaCell extends React.Component<CellProps> { // let field = this.props.rowProps.original[this.props.rowProps.column.id as string]; // let doc = FieldValue(Cast(field, Doc)); - // console.log("Expanding doc", StrCast(doc!.title)); // this.props.setPreviewDoc(doc!); // // this.props.changeFocusedCellByIndex(this.props.row, this.props.col); @@ -265,7 +264,6 @@ export class CollectionSchemaCell extends React.Component<CellProps> { return "0"; } else { const cfield = ComputedField.WithoutComputed(() => FieldValue(props.Document[props.fieldKey])); - console.log(cfield); if (type === "number") { return StrCast(cfield); } @@ -286,7 +284,6 @@ export class CollectionSchemaCell extends React.Component<CellProps> { const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); if (script.compiled) { retVal = this.applyToDoc(props.Document, this.props.row, this.props.col, script.run); - console.log("compiled"); } } @@ -350,17 +347,13 @@ export class CollectionSchemaDateCell extends CollectionSchemaCell { @action handleChange = (date: any) => { - console.log(date); this._date = date; // const script = CompileScript(date.toString(), { requiredType: "Date", addReturn: true, params: { this: Doc.name } }); // if (script.compiled) { - // console.log("scripting"); // this.applyToDoc(this._document, this.props.row, this.props.col, script.run); // } else { - console.log(DateCast(date)); // ^ DateCast is always undefined for some reason, but that is what the field should be set to this._document[this.props.rowProps.column.id as string] = date as Date; - console.log(this._document[this.props.rowProps.column.id as string]); //} } @@ -425,8 +418,6 @@ export class CollectionSchemaDocCell extends CollectionSchemaCell { const results = script.compiled && script.run(); if (results && results.success) { - - console.log(results.result); this._doc = results.result; this._document[this.prop.fieldKey] = results.result; this._docTitle = this._doc?.title; @@ -457,10 +448,8 @@ export class CollectionSchemaDocCell extends CollectionSchemaCell { this._preview = false; } else { if (bool) { - console.log("show doc"); this.props.showDoc(this._doc, this.prop.DataDoc, e.clientX, e.clientY); } else { - console.log("no doc"); this.props.showDoc(undefined); } } @@ -675,7 +664,6 @@ export class CollectionSchemaListCell extends CollectionSchemaCell { @action toggleOpened(open: boolean) { - console.log("open: " + open); this._opened = open; } diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 3c42a2f1c..5553bbbb7 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -332,7 +332,6 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @action openHeader = (col: any, screenx: number, screeny: number) => { - console.log("header opening"); this._col = col; this._headerOpen = !this._headerOpen; this._pointerX = screenx; diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 9d8790dda..dd4c34885 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -123,7 +123,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) changed = true; }); } - changed && setTimeout(action(() => { if (this.columnHeaders) { this.columnHeaders.length = 0; this.columnHeaders.push(...columnHeaders); } }), 0); + changed && setTimeout(action(() => this.columnHeaders?.splice(0, this.columnHeaders.length, ...columnHeaders)), 0); return fields; } @@ -163,8 +163,8 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) this.createDashEventsTarget(ele!); //so the whole grid is the drop target? } - @computed get onChildClickHandler() { return this.props.childClickScript || ScriptCast(this.Document.onChildClick); } - @computed get onChildDoubleClickHandler() { return this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); } + @computed get onChildClickHandler() { return () => this.props.childClickScript || ScriptCast(this.Document.onChildClick); } + @computed get onChildDoubleClickHandler() { return () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); } addDocTab = (doc: Doc, where: string) => { if (where === "inPlace" && this.layoutDoc.isInPlaceContainer) { @@ -249,7 +249,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) return wid * aspect; } return layoutDoc._fitWidth ? !nh ? this.props.PanelHeight() - 2 * this.yMargin : - Math.min(wid * NumCast(layoutDoc.scrollHeight, nh) / (nw || 1), this.props.PanelHeight() - 2 * this.yMargin) : layoutDoc[HeightSym](); + Math.min(wid * NumCast(layoutDoc.scrollHeight, nh) / (nw || 1), this.props.PanelHeight() - 2 * this.yMargin) : Math.max(20, layoutDoc[HeightSym]()); } columnDividerDown = (e: React.PointerEvent) => { diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index 2f4a25bfe..76af70cd1 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -2,7 +2,7 @@ import React = require("react"); import { library } from '@fortawesome/fontawesome-svg-core'; import { faPalette } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, observable, runInAction } from "mobx"; +import { action, observable, runInAction, computed } from "mobx"; import { observer } from "mobx-react"; import { Doc, DocListCast } from "../../../fields/Doc"; import { RichTextField } from "../../../fields/RichTextField"; @@ -279,8 +279,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC const pt = this.props.screenToLocalTransform().inverse().transformPoint(x, y); ContextMenu.Instance.displayMenu(x, y); } - - render() { + @computed get innards() { TraceMobx(); const cols = this.props.cols(); const key = StrCast(this.props.parent.props.Document._pivotField); @@ -351,6 +350,43 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC </div> : (null); for (let i = 0; i < cols; i++) templatecols += `${style.columnWidth / style.numGroupColumns}px `; const chromeStatus = this.props.parent.props.Document._chromeStatus; + + return <> + {this.props.parent.Document._columnsHideIfEmpty ? (null) : headingView} + { + this.collapsed ? (null) : + <div style={{ marginTop: 5 }}> + <div key={`${heading}-stack`} className={`collectionStackingView-masonry${singleColumn ? "Single" : "Grid"}`} + style={{ + padding: singleColumn ? `${columnYMargin}px ${0}px ${style.yMargin}px ${0}px` : `${columnYMargin}px ${0}px`, + margin: "auto", + width: "max-content", //singleColumn ? undefined : `${cols * (style.columnWidth + style.gridGap) + 2 * style.xMargin - style.gridGap}px`, + height: 'max-content', + position: "relative", + gridGap: style.gridGap, + gridTemplateColumns: singleColumn ? undefined : templatecols, + gridAutoRows: singleColumn ? undefined : "0px" + }}> + {this.props.parent.children(this.props.docList, uniqueHeadings.length)} + {singleColumn ? (null) : this.props.parent.columnDragger} + </div> + {(chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ? + <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton" + style={{ width: style.columnWidth / style.numGroupColumns }}> + <EditableView {...newEditableViewProps} menuCallback={this.menuCallback} /> + </div> : null} + </div> + } + </>; + } + + + render() { + TraceMobx(); + const headings = this.props.headings(); + const heading = this._heading; + const uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx); + const chromeStatus = this.props.parent.props.Document._chromeStatus; return ( <div className={"collectionStackingViewFieldColumn" + (SnappingManager.GetIsDragging() ? "Dragging" : "")} key={heading} style={{ @@ -359,31 +395,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC background: this._background }} ref={this.createColumnDropRef} onPointerEnter={this.pointerEntered} onPointerLeave={this.pointerLeave}> - {this.props.parent.Document._columnsHideIfEmpty ? (null) : headingView} - { - this.collapsed ? (null) : - <div style={{ marginTop: 5 }}> - <div key={`${heading}-stack`} className={`collectionStackingView-masonry${singleColumn ? "Single" : "Grid"}`} - style={{ - padding: singleColumn ? `${columnYMargin}px ${0}px ${style.yMargin}px ${0}px` : `${columnYMargin}px ${0}px`, - margin: "auto", - width: "max-content", //singleColumn ? undefined : `${cols * (style.columnWidth + style.gridGap) + 2 * style.xMargin - style.gridGap}px`, - height: 'max-content', - position: "relative", - gridGap: style.gridGap, - gridTemplateColumns: singleColumn ? undefined : templatecols, - gridAutoRows: singleColumn ? undefined : "0px" - }}> - {this.props.parent.children(this.props.docList, uniqueHeadings.length)} - {singleColumn ? (null) : this.props.parent.columnDragger} - </div> - {(chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ? - <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton" - style={{ width: style.columnWidth / style.numGroupColumns }}> - <EditableView {...newEditableViewProps} menuCallback={this.menuCallback} /> - </div> : null} - </div> - } + {this.innards} </div > ); } diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index ce6872695..8480a56cc 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -1,4 +1,4 @@ -import { action, computed, IReactionDisposer, reaction } from "mobx"; +import { action, computed, IReactionDisposer, reaction, observable, runInAction } from "mobx"; import { basename } from 'path'; import CursorField from "../../../fields/CursorField"; import { Doc, Opt, Field } from "../../../fields/Doc"; @@ -19,6 +19,7 @@ import { DocComponent } from "../DocComponent"; import { FieldViewProps } from "../nodes/FieldView"; import React = require("react"); import * as rp from 'request-promise'; +import ReactLoading from 'react-loading'; export interface CollectionViewProps extends FieldViewProps { addDocument: (document: Doc | Doc[]) => boolean; @@ -90,6 +91,11 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: // to its children which may be templates. // If 'annotationField' is specified, then all children exist on that field of the extension document, otherwise, they exist directly on the data document under 'fieldKey' @computed get dataField() { + // sets the dataDoc's data field to an empty list if the data field is undefined - prevents issues with addonly + // setTimeout changes it outside of the @computed section + setTimeout(() => { + if (!this.dataDoc[this.props.annotationsKey || this.props.fieldKey]) this.dataDoc[this.props.annotationsKey || this.props.fieldKey] = new List<Doc>(); + }, 1000); return this.dataDoc[this.props.annotationsKey || this.props.fieldKey]; } @@ -214,6 +220,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: const { dataTransfer } = e; const html = dataTransfer.getData("text/html"); const text = dataTransfer.getData("text/plain"); + const uriList = dataTransfer.getData("text/uri-list"); if (text && text.startsWith("<div")) { return; @@ -306,9 +313,9 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: } } - if (text) { - if (text.includes("www.youtube.com/watch") || text.includes("www.youtube.com/embed")) { - const url = text.replace("youtube.com/watch?v=", "youtube.com/embed/").split("&")[0]; + if (uriList || text) { + if ((uriList || text).includes("www.youtube.com/watch") || text.includes("www.youtube.com/embed")) { + const url = (uriList || text).replace("youtube.com/watch?v=", "youtube.com/embed/").split("&")[0]; this.addDocument(Docs.Create.VideoDocument(url, { ...options, title: url, @@ -333,10 +340,20 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: // if ((matches = /(https:\/\/)?photos\.google\.com\/(u\/3\/)?album\/([^\\]+)/g.exec(text)) !== null) { // const albumId = matches[3]; // const mediaItems = await GooglePhotos.Query.AlbumSearch(albumId); - // console.log(mediaItems); // return; // } } + if (uriList) { + this.addDocument(Docs.Create.WebDocument(uriList, { + ...options, + title: uriList, + _width: 400, + _height: 315, + _nativeWidth: 600, + _nativeHeight: 472.5 + })); + return; + } const { items } = e.dataTransfer; const { length } = items; @@ -363,7 +380,6 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: file?.type && files.push(file); file?.type === "application/json" && Utils.readUploadedFileAsText(file).then(result => { - console.log(result); const json = JSON.parse(result as string); this.addDocument(Docs.Create.TreeDocument( json["rectangular-puzzle"].crossword.clues[0].clue.map((c: any) => { @@ -377,13 +393,20 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: }); } } + this.slowLoadDocuments(files, options, generatedDocuments, text, completed, e.clientX, e.clientY); + batch.end(); + } + slowLoadDocuments = async (files: File[], options: DocumentOptions, generatedDocuments: Doc[], text: string, completed: (() => void) | undefined, clientX: number, clientY: number) => { + runInAction(() => CollectionSubViewLoader.Waiting = "block"); + const disposer = OverlayView.Instance.addElement( + <ReactLoading type={"spinningBubbles"} color={"green"} height={250} width={250} />, { x: clientX - 125, y: clientY - 125 }); generatedDocuments.push(...await DocUtils.uploadFilesToDocs(files, options)); if (generatedDocuments.length) { const set = generatedDocuments.length > 1 && generatedDocuments.map(d => DocUtils.iconify(d)); if (set) { - this.addDocument(DocUtils.pileup(generatedDocuments, options.x!, options.y!)!); + UndoManager.RunInBatch(() => this.addDocument(DocUtils.pileup(generatedDocuments, options.x!, options.y!)!), "drop"); } else { - generatedDocuments.forEach(this.addDocument); + UndoManager.RunInBatch(() => generatedDocuments.forEach(this.addDocument), "drop"); } completed?.(); } else { @@ -391,13 +414,17 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: this.addDocument(Docs.Create.TextDocument(text, { ...options, _width: 400, _height: 315 })); } } - batch.end(); + disposer(); } } return CollectionSubView; } +export class CollectionSubViewLoader { + @observable public static Waiting = "none"; +} + import { DragManager, dropActionType } from "../../util/DragManager"; import { Docs, DocumentOptions, DocUtils } from "../../documents/Documents"; import { CurrentUserUtils } from "../../util/CurrentUserUtils"; @@ -405,4 +432,6 @@ import { DocumentType } from "../../documents/DocumentTypes"; import { FormattedTextBox, GoogleRef } from "../nodes/formattedText/FormattedTextBox"; import { CollectionView } from "./CollectionView"; import { SelectionManager } from "../../util/SelectionManager"; +import { OverlayView } from "../OverlayView"; +import { setTimeout } from "timers"; diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 26c41f524..651357e5d 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -61,8 +61,8 @@ export interface TreeViewProps { treeViewHideHeaderFields: () => boolean; treeViewPreventOpen: boolean; renderedIds: string[]; // list of document ids rendered used to avoid unending expansion of items in a cycle - onCheckedClick?: ScriptField; - onChildClick?: ScriptField; + onCheckedClick?: () => ScriptField; + onChildClick?: () => ScriptField; ignoreFields?: string[]; } @@ -76,7 +76,7 @@ export interface TreeViewProps { * treeViewExpandedView : name of field whose contents are being displayed as the document's subtree */ class TreeView extends React.Component<TreeViewProps> { - private _editTitleScript: ScriptField | undefined; + private _editTitleScript: (() => ScriptField) | undefined; private _header?: React.RefObject<HTMLDivElement> = React.createRef(); private _treedropDisposer?: DragManager.DragDropDisposer; private _dref = React.createRef<HTMLDivElement>(); @@ -124,7 +124,8 @@ class TreeView extends React.Component<TreeViewProps> { constructor(props: any) { super(props); - this._editTitleScript = ScriptField.MakeScript(`{setInPlace(self, 'editTitle', '${this._uniqueId}'); selectDoc(self);} `); + const script = ScriptField.MakeScript(`{setInPlace(self, 'editTitle', '${this._uniqueId}'); selectDoc(self);} `); + this._editTitleScript = script && (() => script); if (Doc.GetT(this.doc, "editTitle", "string", true) === "*") Doc.SetInPlace(this.doc, "editTitle", this._uniqueId, false); } @@ -368,13 +369,13 @@ class TreeView extends React.Component<TreeViewProps> { } } - get onCheckedClick() { return this.props.onCheckedClick || ScriptCast(this.doc.onCheckedClick); } + get onCheckedClick() { return this.props.onCheckedClick?.() ?? ScriptCast(this.doc.onCheckedClick); } @action bulletClick = (e: React.MouseEvent) => { if (this.onCheckedClick && this.doc.type !== DocumentType.COL) { // this.props.document.treeViewChecked = this.props.document.treeViewChecked === "check" ? "x" : this.props.document.treeViewChecked === "x" ? undefined : "check"; - this.onCheckedClick.script.run({ + this.onCheckedClick?.script.run({ this: this.doc.isTemplateForField && this.props.dataDoc ? this.props.dataDoc : this.doc, heading: this.props.containingCollection.title, checked: this.doc.treeViewChecked === "check" ? "x" : this.doc.treeViewChecked === "x" ? undefined : "check", @@ -404,6 +405,7 @@ class TreeView extends React.Component<TreeViewProps> { contextMenuItems = () => [{ script: ScriptField.MakeFunction(`DocFocus(self)`)!, label: "Focus" }]; truncateTitleWidth = () => NumCast(this.props.treeViewDoc.treeViewTruncateTitleWidth, 0); showTitleEdit = () => ["*", this._uniqueId].includes(Doc.GetT(this.doc, "editTitle", "string", true) || ""); + onChildClick = () => this.props.onChildClick?.() ?? (this._editTitleScript?.() || ScriptCast(this.doc.editTitleScript)); /** * Renders the EditableView title element for placement into the tree. */ @@ -437,7 +439,7 @@ class TreeView extends React.Component<TreeViewProps> { addDocTab={this.props.addDocTab} rootSelected={returnTrue} pinToPres={emptyFunction} - onClick={this.props.onChildClick || this._editTitleScript} + onClick={this.onChildClick} dropAction={this.props.dropAction} moveDocument={this.move} removeDocument={this.removeDoc} @@ -539,8 +541,8 @@ class TreeView extends React.Component<TreeViewProps> { treeViewPreventOpen: boolean, renderedIds: string[], libraryPath: Doc[] | undefined, - onCheckedClick: ScriptField | undefined, - onChildClick: ScriptField | undefined, + onCheckedClick: undefined | (() => ScriptField), + onChildClick: undefined | (() => ScriptField), ignoreFields: string[] | undefined ) { const viewSpecScript = Cast(containingCollection.viewSpecScript, ScriptField); @@ -658,8 +660,8 @@ class TreeView extends React.Component<TreeViewProps> { export type collectionTreeViewProps = { treeViewHideTitle?: boolean; treeViewHideHeaderFields?: boolean; - onCheckedClick?: ScriptField; - onChildClick?: ScriptField; + onCheckedClick?: () => ScriptField; + onChildClick?: () => ScriptField; }; @observer @@ -780,7 +782,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll onClicks.push({ description: "Edit onChecked Script", event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.doc, undefined, "onCheckedClick"), "edit onCheckedClick"), icon: "edit" }); - !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" }); + !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", noexpand: true, subitems: onClicks, icon: "hand-point-right" }); } outerXf = () => Utils.GetScreenTransform(this._mainEle!); onTreeDrop = (e: React.DragEvent) => this.onExternalDrop(e, {}); @@ -794,8 +796,8 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll </div >; } - onKeyPress = (e: React.KeyboardEvent) => { - console.log(e); + onChildClick = () => { + return this.props.onChildClick?.() || ScriptCast(this.doc.onChildClick); } render() { TraceMobx(); @@ -814,7 +816,6 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll paddingTop: `${NumCast(this.doc._yPadding, 20)}px`, pointerEvents: !this.props.active() && !SnappingManager.GetIsDragging() ? "none" : undefined }} - onKeyPress={this.onKeyPress} onWheel={(e) => this._mainEle && this._mainEle.scrollHeight > this._mainEle.clientHeight && e.stopPropagation()} onDrop={this.onTreeDrop} ref={this.createTreeDropTarget}> @@ -839,7 +840,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll moveDoc, dropAction, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform, this.outerXf, this.props.active, this.props.PanelWidth, this.props.ChromeHeight, this.props.renderDepth, () => this.props.treeViewHideHeaderFields || BoolCast(this.doc.treeViewHideHeaderFields), BoolCast(this.doc.treeViewPreventOpen), [], this.props.LibraryPath, this.props.onCheckedClick, - this.props.onChildClick || ScriptCast(this.doc.onChildClick), this.props.ignoreFields) + this.onChildClick, this.props.ignoreFields) } </ul> </div > diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index df21d6a28..e2f78d6f9 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -8,7 +8,7 @@ import * as React from 'react'; import Lightbox from 'react-image-lightbox-with-rotate'; import 'react-image-lightbox-with-rotate/style.css'; // This only needs to be imported once in your app import { DateField } from '../../../fields/DateField'; -import { AclAddonly, AclReadonly, AclSym, DataSym, Doc, DocListCast, Field, Opt } from '../../../fields/Doc'; +import { AclAddonly, AclReadonly, DataSym, Doc, DocListCast, Field, Opt, AclEdit, AclSym, AclPrivate } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { ObjectField } from '../../../fields/ObjectField'; @@ -17,7 +17,7 @@ import { listSpec } from '../../../fields/Schema'; import { ComputedField, ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; -import { TraceMobx } from '../../../fields/util'; +import { TraceMobx, GetEffectiveAcl, getPlaygroundMode, distributeAcls } from '../../../fields/util'; import { emptyFunction, emptyPath, returnEmptyFilter, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; @@ -47,7 +47,8 @@ import { SubCollectionViewProps } from './CollectionSubView'; import { CollectionTimeView } from './CollectionTimeView'; import { CollectionTreeView } from "./CollectionTreeView"; import './CollectionView.scss'; -import { CollectionViewBaseChrome } from './CollectionViewChromes'; +import CollectionMenu from './CollectionMenu'; +import { SharingPermissions } from '../../util/SharingManager'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -106,6 +107,13 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; + private AclMap = new Map<symbol, string>([ + [AclPrivate, SharingPermissions.None], + [AclReadonly, SharingPermissions.View], + [AclAddonly, SharingPermissions.Add], + [AclEdit, SharingPermissions.Edit] + ]); + get collectionViewType(): CollectionViewType | undefined { const viewField = StrCast(this.props.Document._viewType); if (CollectionView._safeMode) { @@ -128,36 +136,54 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus if (this.props.filterAddDocument?.(doc) === false) { return false; } + const docs = doc instanceof Doc ? [doc] : doc; const targetDataDoc = this.props.Document[DataSym]; const docList = DocListCast(targetDataDoc[this.props.fieldKey]); const added = docs.filter(d => !docList.includes(d)); + const effectiveAcl = GetEffectiveAcl(this.props.Document); + if (added.length) { - if (this.dataDoc[AclSym] === AclReadonly) { + if (effectiveAcl === AclReadonly && !getPlaygroundMode()) { return false; - } else if (this.dataDoc[AclSym] === AclAddonly) { - added.map(doc => Doc.AddDocToList(targetDataDoc, this.props.fieldKey, doc)); - } else { - added.map(doc => { - const context = Cast(doc.context, Doc, null); - if (context && (context.type === DocumentType.VID || context.type === DocumentType.WEB || context.type === DocumentType.PDF || context.type === DocumentType.IMG) && - !DocListCast(doc.links).some(d => d.isPushpin)) { - const pushpin = Docs.Create.FontIconDocument({ - icon: "map-pin", x: Cast(doc.x, "number", null), y: Cast(doc.y, "number", null), _backgroundColor: "#0000003d", color: "#ACCEF7", - _width: 15, _height: 15, _xPadding: 0, isLinkButton: true, displayTimecode: Cast(doc.displayTimecode, "number", null) - }); - Doc.AddDocToList(context, Doc.LayoutFieldKey(context) + "-annotations", pushpin); - const pushpinLink = DocUtils.MakeLink({ doc: pushpin }, { doc: doc }, "pushpin", ""); - const first = DocListCast(pushpin.links).find(d => d instanceof Doc); - first && (first.hidden = true); - pushpinLink && (Doc.GetProto(pushpinLink).isPushpin = true); - doc.displayTimecode = undefined; - } - doc.context = this.props.Document; - }); - added.map(add => Doc.AddDocToList(Cast(Doc.UserDoc().myCatalog, Doc, null), "data", add)); - targetDataDoc[this.props.fieldKey] = new List<Doc>([...docList, ...added]); - targetDataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now())); + } + else { + if (this.props.Document[AclSym]) { + // change so it only adds if more restrictive + added.forEach(d => { + // const dataDoc = d[DataSym]; + for (const [key, value] of Object.entries(this.props.Document[AclSym])) { + distributeAcls(key, this.AclMap.get(value) as SharingPermissions, d, true); + } + // dataDoc[AclSym] = d[AclSym] = this.props.Document[AclSym]; + }); + } + + if (effectiveAcl === AclAddonly) { + added.map(doc => Doc.AddDocToList(targetDataDoc, this.props.fieldKey, doc)); + } + else { + added.map(doc => { + const context = Cast(doc.context, Doc, null); + if (context && (context.type === DocumentType.VID || context.type === DocumentType.WEB || context.type === DocumentType.PDF || context.type === DocumentType.IMG)) { + const pushpin = Docs.Create.FontIconDocument({ + title: "pushpin", label: "", + icon: "map-pin", x: Cast(doc.x, "number", null), y: Cast(doc.y, "number", null), _backgroundColor: "#0000003d", color: "#ACCEF7", + _width: 15, _height: 15, _xPadding: 0, isLinkButton: true, displayTimecode: Cast(doc.displayTimecode, "number", null) + }); + pushpin.isPushpin = true; + Doc.GetProto(pushpin).annotationOn = doc.annotationOn; + Doc.SetInPlace(doc, "annotationOn", undefined, true); + Doc.AddDocToList(context, Doc.LayoutFieldKey(context) + "-annotations", pushpin); + const pushpinLink = DocUtils.MakeLink({ doc: pushpin }, { doc: doc }, "pushpin", ""); + doc.displayTimecode = undefined; + } + doc.context = this.props.Document; + }); + added.map(add => Doc.AddDocToList(Cast(Doc.UserDoc().myCatalog, Doc, null), "data", add)); + targetDataDoc[this.props.fieldKey] = new List<Doc>([...docList, ...added]); + targetDataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now())); + } } } return true; @@ -165,13 +191,15 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus @action.bound removeDocument = (doc: any): boolean => { - const docs = doc instanceof Doc ? [doc] : doc as Doc[]; - const targetDataDoc = this.props.Document[DataSym]; - const value = DocListCast(targetDataDoc[this.props.fieldKey]); - const result = value.filter(v => !docs.includes(v)); - if (result.length !== value.length) { - targetDataDoc[this.props.fieldKey] = new List<Doc>(result); - return true; + if (GetEffectiveAcl(this.props.Document) === AclEdit || getPlaygroundMode()) { + const docs = doc instanceof Doc ? [doc] : doc as Doc[]; + const targetDataDoc = this.props.Document[DataSym]; + const value = DocListCast(targetDataDoc[this.props.fieldKey]); + const result = value.filter(v => !docs.includes(v)); + if (result.length !== value.length) { + targetDataDoc[this.props.fieldKey] = new List<Doc>(result); + return true; + } } return false; } @@ -234,16 +262,8 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus } } - @action - private collapse = (value: boolean) => { - this.props.Document._chromeStatus = value ? "collapsed" : "enabled"; - } - private SubView = (type: CollectionViewType, renderProps: CollectionRenderProps) => { - // currently cant think of a reason for collection docking view to have a chrome. mind may change if we ever have nested docking views -syip - const chrome = this.props.Document._chromeStatus === "disabled" || this.props.Document._chromeStatus === "replaced" || type === CollectionViewType.Docking ? (null) : - <CollectionViewBaseChrome key="chrome" CollectionView={this} PanelWidth={this.bodyPanelWidth} type={type} collapse={this.collapse} />; - return <>{chrome} {this.SubViewHelper(type, renderProps)}</>; + return this.SubViewHelper(type, renderProps); } @@ -272,7 +292,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus subItems.push({ description: "Custom", icon: "fingerprint", event: AddCustomFreeFormLayout(this.props.Document, this.props.fieldKey) }); } addExtras && subItems.push({ description: "lightbox", event: action(() => this._isLightboxOpen = true), icon: "eye" }); - !existingVm && ContextMenu.Instance.addItem({ description: category, subitems: subItems, icon: "eye" }); + !existingVm && ContextMenu.Instance.addItem({ description: category, noexpand: true, subitems: subItems, icon: "eye" }); } onContextMenu = (e: React.MouseEvent): void => { @@ -285,18 +305,18 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus return newRendition; }, false); - const existing = cm.findByDescription("Options..."); - const layoutItems = existing && "subitems" in existing ? existing.subitems : []; - layoutItems.push({ description: `${this.props.Document.forceActive ? "Select" : "Force"} Contents Active`, event: () => this.props.Document.forceActive = !this.props.Document.forceActive, icon: "project-diagram" }); + const options = cm.findByDescription("Options..."); + const optionItems = options && "subitems" in options ? options.subitems : []; + optionItems.splice(0, 0, { description: `${this.props.Document.forceActive ? "Select" : "Force"} Contents Active`, event: () => this.props.Document.forceActive = !this.props.Document.forceActive, icon: "project-diagram" }); if (this.props.Document.childLayout instanceof Doc) { - layoutItems.push({ description: "View Child Layout", event: () => this.props.addDocTab(this.props.Document.childLayout as Doc, "onRight"), icon: "project-diagram" }); + optionItems.push({ description: "View Child Layout", event: () => this.props.addDocTab(this.props.Document.childLayout as Doc, "onRight"), icon: "project-diagram" }); } if (this.props.Document.childClickedOpenTemplateView instanceof Doc) { - layoutItems.push({ description: "View Child Detailed Layout", event: () => this.props.addDocTab(this.props.Document.childClickedOpenTemplateView as Doc, "onRight"), icon: "project-diagram" }); + optionItems.push({ description: "View Child Detailed Layout", event: () => this.props.addDocTab(this.props.Document.childClickedOpenTemplateView as Doc, "onRight"), icon: "project-diagram" }); } - layoutItems.push({ description: `${this.props.Document.isInPlaceContainer ? "Unset" : "Set"} inPlace Container`, event: () => this.props.Document.isInPlaceContainer = !this.props.Document.isInPlaceContainer, icon: "project-diagram" }); + !Doc.UserDoc().noviceMode && optionItems.push({ description: `${this.props.Document.isInPlaceContainer ? "Unset" : "Set"} inPlace Container`, event: () => this.props.Document.isInPlaceContainer = !this.props.Document.isInPlaceContainer, icon: "project-diagram" }); - !existing && cm.addItem({ description: "Options...", subitems: layoutItems, icon: "hand-point-right" }); + !options && cm.addItem({ description: "Options...", subitems: optionItems, icon: "hand-point-right" }); const existingOnClick = cm.findByDescription("OnClick..."); const onClicks = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; @@ -305,16 +325,18 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus { key: "onChildDoubleClick", name: "On Child Double Clicked" }]; funcs.map(func => onClicks.push({ description: `Edit ${func.name} script`, icon: "edit", event: (obj: any) => { - ScriptBox.EditButtonScript(func.name + "...", this.props.Document, func.key, obj.x, obj.y, { thisContainer: Doc.name }); + const alias = Doc.MakeAlias(this.props.Document); + DocUtils.makeCustomViewClicked(alias, undefined, func.key); + this.props.addDocTab(alias, "onRight"); } })); DocListCast(Cast(Doc.UserDoc()["clickFuncs-child"], Doc, null).data).forEach(childClick => onClicks.push({ description: `Set child ${childClick.title}`, icon: "edit", - event: () => this.props.Document[StrCast(childClick.targetScriptKey)] = ObjectField.MakeCopy(ScriptCast(childClick.data)), + event: () => Doc.GetProto(this.props.Document)[StrCast(childClick.targetScriptKey)] = ObjectField.MakeCopy(ScriptCast(childClick.data)), })); - !existingOnClick && cm.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" }); + !existingOnClick && cm.addItem({ description: "OnClick...", noexpand: true, subitems: onClicks, icon: "hand-point-right" }); if (!Doc.UserDoc().noviceMode) { const more = cm.findByDescription("More..."); @@ -470,7 +492,8 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus get ignoreFields() { return ["_docFilters", "_docRangeFilters"]; } // this makes the tree view collection ignore these filters (otherwise, the filters would filter themselves) @computed get scriptField() { const scriptText = "setDocFilter(containingTreeView, heading, this.title, checked)"; - return ScriptField.MakeScript(scriptText, { this: Doc.name, heading: "string", checked: "string", containingTreeView: Doc.name }); + const script = ScriptField.MakeScript(scriptText, { this: Doc.name, heading: "string", checked: "string", containingTreeView: Doc.name }); + return script ? () => script : undefined; } @computed get filterView() { TraceMobx(); @@ -523,7 +546,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus ContentScaling={returnOne} focus={returnFalse} treeViewHideHeaderFields={true} - onCheckedClick={this.scriptField!} + onCheckedClick={this.scriptField} ignoreFields={this.ignoreFields} annotationsKey={""} dontRegisterView={true} @@ -549,6 +572,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus ChildLayoutTemplate: this.childLayoutTemplate, ChildLayoutString: this.childLayoutString, }; + setTimeout(action(() => this.props.isSelected(true) && (CollectionMenu.Instance.SelectedCollection = this)), 0); const boxShadow = Doc.UserDoc().renderStyle === "comic" || this.props.Document.isBackground || this.collectionViewType === CollectionViewType.Linear ? undefined : `${Cast(Doc.UserDoc().activeWorkspace, Doc, null)?.darkScheme ? "rgb(30, 32, 31) " : "#9c9396 "} ${StrCast(this.props.Document.boxShadow, "0.2vw 0.2vw 0.8vw")}`; return (<div className={"collectionView"} onContextMenu={this.onContextMenu} diff --git a/src/client/views/collections/SchemaTable.tsx b/src/client/views/collections/SchemaTable.tsx index 9e3b4d961..cde795098 100644 --- a/src/client/views/collections/SchemaTable.tsx +++ b/src/client/views/collections/SchemaTable.tsx @@ -135,7 +135,6 @@ export class SchemaTable extends React.Component<SchemaTableProps> { @action changeSorting = (col: any) => { - console.log(col.heading); if (col.desc === undefined) { // no sorting this.props.changeColumnSort(col, true); @@ -149,7 +148,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { } @action - changeTitleMode = () => { console.log("header clicked"); this._showTitleDropdown = !this._showTitleDropdown; } + changeTitleMode = () => this._showTitleDropdown = !this._showTitleDropdown; @computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); } @computed get tableColumns(): Column<Doc>[] { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss index 05111adb4..8cbda310a 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss @@ -16,4 +16,5 @@ stroke: rgb(0,0,0); opacity: 0.5; pointer-events: all; + cursor: move; } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index a24693c30..bfe569853 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -1,13 +1,12 @@ import { observer } from "mobx-react"; import { Doc } from "../../../../fields/Doc"; -import { Utils } from '../../../../Utils'; +import { Utils, setupMoveUpEvents, emptyFunction, returnFalse } from '../../../../Utils'; import { DocumentView } from "../../nodes/DocumentView"; import "./CollectionFreeFormLinkView.scss"; import React = require("react"); -import v5 = require("uuid/v5"); import { DocumentType } from "../../../documents/DocumentTypes"; -import { observable, action, reaction, IReactionDisposer } from "mobx"; -import { StrCast, Cast } from "../../../../fields/Types"; +import { observable, action, reaction, IReactionDisposer, trace, computed } from "mobx"; +import { StrCast, Cast, NumCast } from "../../../../fields/Types"; import { Id } from "../../../../fields/FieldSymbols"; import { SnappingManager } from "../../../util/SnappingManager"; @@ -20,18 +19,27 @@ export interface CollectionFreeFormLinkViewProps { @observer export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFormLinkViewProps> { @observable _opacity: number = 0; + @observable _start = 0; _anchorDisposer: IReactionDisposer | undefined; + _timeout: NodeJS.Timeout | undefined; + componentWillUnmount() { + this._anchorDisposer?.(); + } @action + timeout = () => (Date.now() < this._start++ + 1000) && setTimeout(this.timeout, 25) componentDidMount() { this._anchorDisposer = reaction(() => [this.props.A.props.ScreenToLocalTransform(), this.props.B.props.ScreenToLocalTransform(), this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document), this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document)], action(() => { - if (SnappingManager.GetIsDragging()) return; + this._start = Date.now(); + this._timeout && clearTimeout(this._timeout); + this._timeout = setTimeout(this.timeout, 25); + if (SnappingManager.GetIsDragging() || !this.props.A.ContentDiv || !this.props.B.ContentDiv) return; setTimeout(action(() => this._opacity = 1), 0); // since the render code depends on querying the Dom through getBoudndingClientRect, we need to delay triggering render() setTimeout(action(() => (!this.props.LinkDocs.length || !this.props.LinkDocs[0].linkDisplay) && (this._opacity = 0.05)), 750); // this will unhighlight the link line. - const acont = this.props.A.props.Document.type === DocumentType.LINK ? this.props.A.ContentDiv!.getElementsByClassName("linkAnchorBox-cont") : []; - const bcont = this.props.B.props.Document.type === DocumentType.LINK ? this.props.B.ContentDiv!.getElementsByClassName("linkAnchorBox-cont") : []; - const adiv = (acont.length ? acont[0] : this.props.A.ContentDiv!); - const bdiv = (bcont.length ? bcont[0] : this.props.B.ContentDiv!); + const acont = this.props.A.props.Document.type === DocumentType.LINK ? this.props.A.ContentDiv.getElementsByClassName("linkAnchorBox-cont") : []; + const bcont = this.props.B.props.Document.type === DocumentType.LINK ? this.props.B.ContentDiv.getElementsByClassName("linkAnchorBox-cont") : []; + const adiv = (acont.length ? acont[0] : this.props.A.ContentDiv); + const bdiv = (bcont.length ? bcont[0] : this.props.B.ContentDiv); const a = adiv.getBoundingClientRect(); const b = bdiv.getBoundingClientRect(); const abounds = adiv.parentElement!.getBoundingClientRect(); @@ -82,17 +90,31 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo }) , { fireImmediately: true }); } - @action - componentWillUnmount() { - this._anchorDisposer?.(); + + + pointerDown = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, (e, down, delta) => { + this.props.LinkDocs[0].linkOffsetX = NumCast(this.props.LinkDocs[0].linkOffsetX) + delta[0]; + this.props.LinkDocs[0].linkOffsetY = NumCast(this.props.LinkDocs[0].linkOffsetY) + delta[1]; + return false; + }, emptyFunction, () => { + // OverlayView.Instance.addElement( + // <LinkEditor sourceDoc={this.props.A.props.Document} linkDoc={this.props.LinkDocs[0]} + // showLinks={action(() => { })} + // />, { x: 300, y: 300 }); + }); } - render() { - if (SnappingManager.GetIsDragging()) return null; + + @computed get renderData() { + this._start; + if (SnappingManager.GetIsDragging() || !this.props.A.ContentDiv || !this.props.B.ContentDiv || !this.props.LinkDocs.length) { + return undefined; + } this.props.A.props.ScreenToLocalTransform().transform(this.props.B.props.ScreenToLocalTransform()); - const acont = this.props.A.ContentDiv!.getElementsByClassName("linkAnchorBox-cont"); - const bcont = this.props.B.ContentDiv!.getElementsByClassName("linkAnchorBox-cont"); - const a = (acont.length ? acont[0] : this.props.A.ContentDiv!).getBoundingClientRect(); - const b = (bcont.length ? bcont[0] : this.props.B.ContentDiv!).getBoundingClientRect(); + const acont = this.props.A.ContentDiv.getElementsByClassName("linkAnchorBox-cont"); + const bcont = this.props.B.ContentDiv.getElementsByClassName("linkAnchorBox-cont"); + const a = (acont.length ? acont[0] : this.props.A.ContentDiv).getBoundingClientRect(); + const b = (bcont.length ? bcont[0] : this.props.B.ContentDiv).getBoundingClientRect(); const apt = Utils.closestPtBetweenRectangles(a.left, a.top, a.width, a.height, b.left, b.top, b.width, b.height, a.left + a.width / 2, a.top + a.height / 2); @@ -105,18 +127,26 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo const pt2vec = [pt2[0] - (b.left + b.width / 2), pt2[1] - (b.top + b.height / 2)]; const pt1len = Math.sqrt((pt1vec[0] * pt1vec[0]) + (pt1vec[1] * pt1vec[1])); const pt2len = Math.sqrt((pt2vec[0] * pt2vec[0]) + (pt2vec[1] * pt2vec[1])); - const ptlen = Math.sqrt((pt1[0] - pt2[0]) * (pt1[0] - pt2[0]) + (pt1[1] - pt2[1]) * (pt1[1] - pt2[1])) / 3; + const ptlen = Math.sqrt((pt1[0] - pt2[0]) * (pt1[0] - pt2[0]) + (pt1[1] - pt2[1]) * (pt1[1] - pt2[1])) / 2; const pt1norm = [pt1vec[0] / pt1len * ptlen, pt1vec[1] / pt1len * ptlen]; const pt2norm = [pt2vec[0] / pt2len * ptlen, pt2vec[1] / pt2len * ptlen]; const aActive = this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document); - const bActive = this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document); - const text = StrCast(this.props.A.props.Document.linkRelationship); - return !a.width || !b.width || ((!this.props.LinkDocs.length || !this.props.LinkDocs[0].linkDisplay) && !aActive && !bActive) ? (null) : (<> - <text x={(Math.min(pt1[0], pt2[0]) * 2 + Math.max(pt1[0], pt2[0])) / 3} y={(pt1[1] + pt2[1]) / 2}> - {text !== "-ungrouped-" ? text : ""} - </text> + const bActive = this.props.B.isSelected() || Doc.IsBrushed(this.props.B.props.Document); + + const textX = (Math.min(pt1[0], pt2[0]) + Math.max(pt1[0], pt2[0])) / 2 + NumCast(this.props.LinkDocs[0].linkOffsetX); + const textY = (pt1[1] + pt2[1]) / 2 + NumCast(this.props.LinkDocs[0].linkOffsetY); + return { a, b, pt1norm, pt2norm, aActive, bActive, textX, textY, pt1, pt2 }; + } + + render() { + if (!this.renderData) return (null); + const { a, b, pt1norm, pt2norm, aActive, bActive, textX, textY, pt1, pt2 } = this.renderData; + return !a.width || !b.width || ((!this.props.LinkDocs[0].linkDisplay) && !aActive && !bActive) ? (null) : (<> <path className="collectionfreeformlinkview-linkLine" style={{ opacity: this._opacity, strokeDasharray: "2 2" }} d={`M ${pt1[0]} ${pt1[1]} C ${pt1[0] + pt1norm[0]} ${pt1[1] + pt1norm[1]}, ${pt2[0] + pt2norm[0]} ${pt2[1] + pt2norm[1]}, ${pt2[0]} ${pt2[1]}`} /> + <text className="collectionfreeformlinkview-linkText" x={textX} y={textY} onPointerDown={this.pointerDown} > + {StrCast(this.props.LinkDocs[0].description)} + </text> </>); } }
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 9bf425db2..979b21321 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -46,6 +46,8 @@ import "./CollectionFreeFormView.scss"; import MarqueeOptionsMenu from "./MarqueeOptionsMenu"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); +import { SearchUtil } from "../../../util/SearchUtil"; +import { LinkManager } from "../../../util/LinkManager"; library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload); @@ -75,7 +77,8 @@ export type collectionFreeformViewProps = { forceScaling?: boolean; // whether to force scaling of content (needed by ImageBox) viewDefDivClick?: ScriptField; childPointerEvents?: boolean; - scaleField?: string; + scaleField?: string; // used by formattedTextBox when displaying a sidebar freeform view which needs its own scale field + noOverlay?: boolean; // used to suppress docs in the overlay (z) layer (ie, for minimap since overlay doesn't scale) }; @observer @@ -101,6 +104,10 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @observable _clusterSets: (Doc[])[] = []; @observable _timelineRef = React.createRef<Timeline>(); + @observable _marqueeRef = React.createRef<HTMLDivElement>(); + @observable canPanX: boolean = true; + @observable canPanY: boolean = true; + @computed get fitToContentScaling() { return this.fitToContent ? NumCast(this.layoutDoc.fitToContentScaling, 1) : 1; } @computed get fitToContent() { return (this.props.fitToBox || this.Document._fitToBox) && !this.isAnnotationOverlay; } @computed get parentScaling() { return this.props.ContentScaling && this.fitToContent && !this.isAnnotationOverlay ? this.props.ContentScaling() : 1; } @@ -180,7 +187,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P private selectDocuments = (docs: Doc[]) => { SelectionManager.DeselectAll(); - docs.map(doc => DocumentManager.Instance.getDocumentView(doc)).map(dv => dv && SelectionManager.SelectDoc(dv, true)); + docs.map(doc => DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView)).map(dv => dv && SelectionManager.SelectDoc(dv, true)); } public isCurrent(doc: Doc) { return (Math.abs(NumCast(doc.displayTimecode, -1) - NumCast(this.Document.currentTimecode, -1)) < 1.5 || NumCast(doc.displayTimecode, -1) === -1); } @@ -205,7 +212,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P for (let i = 0; i < docDragData.droppedDocuments.length; i++) { const d = docDragData.droppedDocuments[i]; const layoutDoc = Doc.Layout(d); - if (this.Document.currentFrame !== undefined && !this.props.isAnnotationOverlay) { + if (this.Document.currentFrame !== undefined) { const vals = CollectionFreeFormDocumentView.getValues(d, NumCast(d.activeFrame, 1000)); CollectionFreeFormDocumentView.setValues(this.Document.currentFrame, d, x + vals.x - dropPos[0], y + vals.y - dropPos[1], vals.opacity); } else { @@ -490,10 +497,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P const start = this.getTransform().transformPoint(Math.min(...ge.points.map(p => p.X)), Math.min(...ge.points.map(p => p.Y))); this._inkToTextStartX = start[0]; this._inkToTextStartY = start[1]; - console.log("start"); break; case GestureUtils.Gestures.EndBracket: - console.log("end"); if (this._inkToTextStartX && this._inkToTextStartY) { const end = this.getTransform().transformPoint(Math.max(...ge.points.map(p => p.X)), Math.max(...ge.points.map(p => p.Y))); const setDocs = this.getActiveDocuments().filter(s => s.proto?.type === "rtf" && s.color); @@ -932,9 +937,9 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } @computed get libraryPath() { return this.props.LibraryPath ? [...this.props.LibraryPath, this.props.Document] : []; } - @computed get onChildClickHandler() { return this.props.childClickScript || ScriptCast(this.Document.onChildClick); } - @computed get onChildDoubleClickHandler() { return this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); } @computed get backgroundActive() { return this.layoutDoc.isBackground && (this.props.ContainingCollectionView?.active() || this.props.active()); } + onChildClickHandler = () => this.props.childClickScript || ScriptCast(this.Document.onChildClick); + onChildDoubleClickHandler = () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); backgroundHalo = () => BoolCast(this.Document.useClusters); parentActive = (outsideReaction: boolean) => this.props.active(outsideReaction) || this.backgroundActive ? true : false; getChildDocumentViewProps(childLayout: Doc, childData?: Doc): DocumentViewProps { @@ -946,8 +951,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P DataDoc: childData, Document: childLayout, LibraryPath: this.libraryPath, - LayoutTemplate: this.props.ChildLayoutTemplate, - LayoutTemplateString: this.props.ChildLayoutString, + LayoutTemplate: childLayout.z ? undefined : this.props.ChildLayoutTemplate, + LayoutTemplateString: childLayout.z ? undefined : this.props.ChildLayoutString, FreezeDimensions: this.props.freezeChildDimensions, layoutKey: undefined, setupDragLines: this.setupDragLines, @@ -1027,7 +1032,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P const transform = `translate(${x}px, ${y}px)`; if (viewDef.type === "text") { const text = Cast(viewDef.text, "string"); // don't use NumCast, StrCast, etc since we want to test for undefined below - const fontSize = Cast(viewDef.fontSize, "number"); + const fontSize = Cast(viewDef.fontSize, "string"); return [text, x, y].some(val => val === undefined) ? undefined : { ele: <div className="collectionFreeform-customText" key={(text || "") + x + y + z + color} style={{ width, height, color, fontSize, transform }}> @@ -1065,7 +1070,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P doFreeformLayout(poolData: Map<string, PoolData>) { const layoutDocs = this.childLayoutPairs.map(pair => pair.layout); - const initResult = this.Document.arrangeInit && this.Document.arrangeInit.script.run({ docs: layoutDocs, collection: this.Document }, console.log); + const initResult = this.Document.arrangeInit?.script.run({ docs: layoutDocs, collection: this.Document }, console.log); const state = initResult?.success ? initResult.result.scriptState : undefined; const elements = initResult?.success ? this.viewDefsToJSX(initResult.result.views) : []; @@ -1143,10 +1148,15 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P this._layoutComputeReaction = reaction(() => this.doLayoutComputation, (elements) => this._layoutElements = elements || [], { fireImmediately: true, name: "doLayout" }); + + this._marqueeRef.current?.addEventListener("dashDragAutoScroll", this.onDragAutoScroll as any); } + componentWillUnmount() { this._layoutComputeReaction?.(); + this._marqueeRef.current?.removeEventListener("dashDragAutoScroll", this.onDragAutoScroll as any); } + @computed get views() { return this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele); } elementFunc = () => this._layoutElements; @@ -1155,6 +1165,29 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY)); } + + // <div ref={this._marqueeRef}> + + @action + onDragAutoScroll = (e: CustomEvent<React.DragEvent>) => { + if ((e as any).handlePan || this.props.isAnnotationOverlay) return; + (e as any).handlePan = true; + + if (this._marqueeRef?.current) { + const dragX = e.detail.clientX; + const dragY = e.detail.clientY; + const bounds = this._marqueeRef.current?.getBoundingClientRect(); + + const deltaX = dragX - bounds.left < 25 ? -(25 + (bounds.left - dragX)) : bounds.right - dragX < 25 ? 25 - (bounds.right - dragX) : 0; + const deltaY = dragY - bounds.top < 25 ? -(25 + (bounds.top - dragY)) : bounds.bottom - dragY < 25 ? 25 - (bounds.bottom - dragY) : 0; + if (deltaX !== 0 || deltaY !== 0) { + this.Document._panY = NumCast(this.Document._panY) + deltaY / 2; + this.Document._panX = NumCast(this.Document._panX) + deltaX / 2; + } + } + e.stopPropagation(); + } + promoteCollection = undoBatch(action(() => { const childDocs = this.childDocs.slice(); childDocs.forEach(doc => { @@ -1205,55 +1238,69 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P const appearance = ContextMenu.Instance.findByDescription("Appearance..."); const appearanceItems = appearance && "subitems" in appearance ? appearance.subitems : []; - appearanceItems.push({ description: "reset view", event: () => { this.props.Document._panX = this.props.Document._panY = 0; this.props.Document[this.scaleFieldKey] = 1; }, icon: "compress-arrows-alt" }); - appearanceItems.push({ description: `${this.fitToContent ? "Unset" : "Set"} Fit To Container`, event: () => this.Document._fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" }); - appearanceItems.push({ description: `${this.Document.useClusters ? "Uncluster" : "Use Clusters"}`, event: () => this.updateClusters(!this.Document.useClusters), icon: "braille" }); - appearanceItems.push({ description: "Use Background Color as Default", event: () => Cast(Doc.UserDoc().emptyCollection, Doc, null)._backgroundColor = StrCast(this.layoutDoc._backgroundColor), icon: "palette" }); + appearanceItems.push({ description: "Reset View", event: () => { this.props.Document._panX = this.props.Document._panY = 0; this.props.Document[this.scaleFieldKey] = 1; }, icon: "compress-arrows-alt" }); + appearanceItems.push({ description: `${this.fitToContent ? "Make Zoomable" : "Scale to Window"}`, event: () => this.Document._fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" }); + appearanceItems.push({ description: "Arrange contents in grid", event: this.layoutDocsInGrid, icon: "table" }); !appearance && ContextMenu.Instance.addItem({ description: "Appearance...", subitems: appearanceItems, icon: "eye" }); + const viewctrls = ContextMenu.Instance.findByDescription("View Controls..."); + const viewCtrlItems = viewctrls && "subitems" in viewctrls ? viewctrls.subitems : []; + viewCtrlItems.push({ description: (Doc.UserDoc().showSnapLines ? "Hide" : "Show") + " Snap Lines", event: () => Doc.UserDoc().showSnapLines = !Doc.UserDoc().showSnapLines, icon: "compress-arrows-alt" }); + viewCtrlItems.push({ description: (this.Document.useClusters ? "Hide" : "Show") + " Clusters", event: () => this.updateClusters(!this.Document.useClusters), icon: "braille" }); + !viewctrls && ContextMenu.Instance.addItem({ description: "View Controls...", subitems: viewCtrlItems, icon: "eye" }); + const options = ContextMenu.Instance.findByDescription("Options..."); const optionItems = options && "subitems" in options ? options.subitems : []; - !this.props.isAnnotationOverlay && + !this.props.isAnnotationOverlay && !Doc.UserDoc().noviceMode && optionItems.push({ description: (this.showTimeline ? "Close" : "Open") + " Animation Timeline", event: action(() => this.showTimeline = !this.showTimeline), icon: faEye }); this.props.ContainingCollectionView && optionItems.push({ description: "Promote Collection", event: this.promoteCollection, icon: "table" }); - optionItems.push({ description: (Doc.UserDoc().showSnapLines ? "Hide" : "Show") + " snap lines", event: () => Doc.UserDoc().showSnapLines = !Doc.UserDoc().showSnapLines, icon: "compress-arrows-alt" }); optionItems.push({ description: this.layoutDoc._lockedTransform ? "Unlock Transform" : "Lock Transform", event: this.toggleLockTransform, icon: this.layoutDoc._lockedTransform ? "unlock" : "lock" }); - optionItems.push({ description: "Arrange contents in grid", event: this.layoutDocsInGrid, icon: "table" }); + appearanceItems.push({ description: "Use Background Color as Default", event: () => Cast(Doc.UserDoc().emptyCollection, Doc, null)._backgroundColor = StrCast(this.layoutDoc._backgroundColor), icon: "palette" }); if (!Doc.UserDoc().noviceMode) { optionItems.push({ description: (!this.layoutDoc._nativeWidth || !this.layoutDoc._nativeHeight ? "Freeze" : "Unfreeze") + " Aspect", event: this.toggleNativeDimensions, icon: "snowflake" }); optionItems.push({ description: `${this.Document._freeformLOD ? "Enable LOD" : "Disable LOD"}`, event: () => this.Document._freeformLOD = !this.Document._freeformLOD, icon: "table" }); - optionItems.push({ - description: "Import document", icon: "upload", event: ({ x, y }) => { - const input = document.createElement("input"); - input.type = "file"; - input.accept = ".zip"; - input.onchange = async _e => { - const upload = Utils.prepend("/uploadDoc"); - const formData = new FormData(); - const file = input.files && input.files[0]; - if (file) { - formData.append('file', file); - formData.append('remap', "true"); - const response = await fetch(upload, { method: "POST", body: formData }); - const json = await response.json(); - if (json !== "error") { - const doc = await DocServer.GetRefField(json); - if (doc instanceof Doc) { - const [xx, yy] = this.props.ScreenToLocalTransform().transformPoint(x, y); - doc.x = xx, doc.y = yy; - this.props.addDocument?.(doc); - } - } - } - }; - input.click(); - } - }); + } !options && ContextMenu.Instance.addItem({ description: "Options...", subitems: optionItems, icon: "eye" }); + const mores = ContextMenu.Instance.findByDescription("More..."); + const moreItems = mores && "subitems" in mores ? mores.subitems : []; + moreItems.push({ description: "Import document", icon: "upload", event: ({ x, y }) => this.importDocument(x, y) }); + !mores && ContextMenu.Instance.addItem({ description: "More...", subitems: moreItems, icon: "eye" }); + } + importDocument = (x: number, y: number) => { + const input = document.createElement("input"); + input.type = "file"; + input.accept = ".zip"; + input.onchange = async _e => { + const upload = Utils.prepend("/uploadDoc"); + const formData = new FormData(); + const file = input.files && input.files[0]; + if (file) { + formData.append('file', file); + formData.append('remap', "true"); + const response = await fetch(upload, { method: "POST", body: formData }); + const json = await response.json(); + if (json !== "error") { + const doc = await DocServer.GetRefField(json); + if (doc instanceof Doc) { + const [xx, yy] = this.props.ScreenToLocalTransform().transformPoint(x, y); + doc.x = xx, doc.y = yy; + this.props.addDocument?.(doc); + setTimeout(() => { + SearchUtil.Search(`{!join from=id to=proto_i}id:link*`, true, {}).then(docs => { + docs.docs.forEach(d => LinkManager.Instance.addLink(d)); + }) + }, 2000); // need to give solr some time to update so that this query will find any link docs we've added. + } + } + } + }; + input.click(); } + + @observable showTimeline = false; intersectRect(r1: { left: number, top: number, width: number, height: number }, @@ -1336,7 +1383,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P return false; }); @computed get marqueeView() { - return <MarqueeView {...this.props} + return <MarqueeView + {...this.props} nudge={this.nudge} addDocTab={this.addDocTab} activeDocuments={this.getActiveDocuments} @@ -1346,14 +1394,15 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}> - <CollectionFreeFormViewPannableContents - centeringShiftX={this.centeringShiftX} - centeringShiftY={this.centeringShiftY} - transition={Cast(this.layoutDoc._viewTransition, "string", null)} - viewDefDivClick={this.props.viewDefDivClick} - zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}> - {this.children} - </CollectionFreeFormViewPannableContents> + <div ref={this._marqueeRef}> + <CollectionFreeFormViewPannableContents + centeringShiftX={this.centeringShiftX} + centeringShiftY={this.centeringShiftY} + transition={Cast(this.layoutDoc._viewTransition, "string", null)} + viewDefDivClick={this.props.viewDefDivClick} + zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}> + {this.children} + </CollectionFreeFormViewPannableContents></div> {this.showTimeline ? <Timeline ref={this._timelineRef} {...this.props} /> : (null)} </MarqueeView>; } @@ -1370,6 +1419,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P render() { TraceMobx(); const clientRect = this._mainCont?.getBoundingClientRect(); + !this.fitToContent && this._layoutElements?.length && setTimeout(() => this.Document._renderContentBounds = new List<number>([this.contentBounds.x, this.contentBounds.y, this.contentBounds.r, this.contentBounds.b]), 0); return <div className={"collectionfreeformview-container"} ref={this.createDashEventsTarget} onPointerOver={this.onPointerOver} onWheel={this.onPointerWheel} @@ -1388,7 +1438,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P }}> {this.Document._freeformLOD && !this.props.active() && !this.props.isAnnotationOverlay && !this.props.annotationsKey && this.props.renderDepth > 0 ? this.placeholder : this.marqueeView} - <CollectionFreeFormOverlayView elements={this.elementFunc} /> + {!this.props.noOverlay ? <CollectionFreeFormOverlayView elements={this.elementFunc} /> : (null)} <div className={"pullpane-indicator"} style={{ diff --git a/src/client/views/collections/collectionFreeForm/FormatShapePane.scss b/src/client/views/collections/collectionFreeForm/FormatShapePane.scss new file mode 100644 index 000000000..88876471c --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/FormatShapePane.scss @@ -0,0 +1,64 @@ +.antimodeMenu-button { + width: 200px; + position: relative; + text-align: left; + + .color-previewI { + width: 100%; + height: 40%; + } + + .color-previewII { + width: 100%; + height: 100%; + } +} + +.antimenu-Buttonup { + position: absolute; + width: 20; + height: 10; + right: 0; + padding: 0; +} + +.formatShapePane-inputBtn { + width: inherit; + position: absolute; +} + +.sketch-picker { + background: #323232; + + .flexbox-fit { + background: #323232; + } +} + +.btn-group { + display: grid; + grid-template-columns: auto auto auto auto; + /* Make the buttons appear below each other */ +} + +.btn-group-palette { + display: block; + /* Make the buttons appear below each other */ +} + +.btn-draw { + display: inline; + /* Make the buttons appear below each other */ +} + +.btn2-group { + display: block; + background: #323232; + grid-template-columns: auto; + + /* Make the buttons appear below each other */ + .antimodeMenu-button { + background: #323232; + display: block; + } +}
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/FormatShapePane.tsx b/src/client/views/collections/collectionFreeForm/FormatShapePane.tsx new file mode 100644 index 000000000..4e328d838 --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/FormatShapePane.tsx @@ -0,0 +1,307 @@ +import React = require("react"); +import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { action, computed, observable } from "mobx"; +import { observer } from "mobx-react"; +import { Doc, Field, Opt } from "../../../../fields/Doc"; +import { Document } from "../../../../fields/documentSchemas"; +import { InkField } from "../../../../fields/InkField"; +import { BoolCast, Cast, NumCast } from "../../../../fields/Types"; +import { DocumentType } from "../../../documents/DocumentTypes"; +import { SelectionManager } from "../../../util/SelectionManager"; +import AntimodeMenu from "../../AntimodeMenu"; +import "./FormatShapePane.scss"; +import { undoBatch } from "../../../util/UndoManager"; + +@observer +export default class FormatShapePane extends AntimodeMenu { + static Instance: FormatShapePane; + + private _lastFill = "#D0021B"; + private _lastLine = "#D0021B"; + private _lastDash = "2"; + private _palette = ["#D0021B", "#F5A623", "#F8E71C", "#8B572A", "#7ED321", "#417505", "#9013FE", "#4A90E2", "#50E3C2", "#B8E986", "#000000", "#4A4A4A", "#9B9B9B", "#FFFFFF"]; + private _mode = ["fill-drip", "ruler-combined"]; + + @observable private _subOpen = [false, false, false, false]; + @observable private _currMode = "fill-drip"; + @observable private _lock = false; + @observable private _fillBtn = false; + @observable private _lineBtn = false; + + getField(key: string) { + return this.selectedInk?.reduce((p, i) => + (p === undefined || (p && p === i.rootDoc[key])) && i.rootDoc[key] !== "0" ? Field.toString(i.rootDoc[key] as Field) : "", undefined as Opt<string>); + } + + @computed get selectedInk() { + const inks = SelectionManager.SelectedDocuments().filter(i => Document(i.rootDoc).type === DocumentType.INK); + return inks.length ? inks : undefined; + } + @computed get unFilled() { return this.selectedInk?.reduce((p, i) => p && !i.rootDoc.fillColor ? true : false, true) || false; } + @computed get unStrokd() { return this.selectedInk?.reduce((p, i) => p && !i.rootDoc.color ? true : false, true) || false; } + @computed get solidFil() { return this.selectedInk?.reduce((p, i) => p && i.rootDoc.fillColor ? true : false, true) || false; } + @computed get solidStk() { return this.selectedInk?.reduce((p, i) => p && i.rootDoc.color && (!i.rootDoc.strokeDash || i.rootDoc.strokeDash === "0") ? true : false, true) || false; } + @computed get dashdStk() { return !this.unStrokd && this.getField("strokeDash") || ""; } + @computed get colorFil() { const ccol = this.getField("fillColor") || ""; ccol && (this._lastFill = ccol); return ccol; } + @computed get colorStk() { const ccol = this.getField("color") || ""; ccol && (this._lastLine = ccol); return ccol; } + @computed get widthStk() { return this.getField("strokeWidth") || "1"; } + @computed get markHead() { return this.getField("strokeStartMarker") || ""; } + @computed get markTail() { return this.getField("strokeEndMarker") || ""; } + @computed get shapeHgt() { return this.getField("_height"); } + @computed get shapeWid() { return this.getField("_width"); } + @computed get shapeXps() { return this.getField("x"); } + @computed get shapeYps() { return this.getField("y"); } + @computed get shapeRot() { return this.getField("rotation"); } + set unFilled(value) { this.colorFil = value ? "" : this._lastFill; } + set solidFil(value) { this.unFilled = !value; } + set colorFil(value) { value && (this._lastFill = value); this.selectedInk?.forEach(i => i.rootDoc.fillColor = value ? value : undefined); } + set colorStk(value) { value && (this._lastLine = value); this.selectedInk?.forEach(i => i.rootDoc.color = value ? value : undefined); } + set markHead(value) { this.selectedInk?.forEach(i => i.rootDoc.strokeStartMarker = value); } + set markTail(value) { this.selectedInk?.forEach(i => i.rootDoc.strokeEndMarker = value); } + set unStrokd(value) { this.colorStk = value ? "" : this._lastLine; } + set solidStk(value) { this.dashdStk = ""; this.unStrokd = !value; } + set dashdStk(value) { + value && (this._lastDash = value) && (this.unStrokd = false); + this.selectedInk?.forEach(i => i.rootDoc.strokeDash = value ? this._lastDash : undefined); + } + set shapeXps(value) { this.selectedInk?.forEach(i => i.rootDoc.x = Number(value)); } + set shapeYps(value) { this.selectedInk?.forEach(i => i.rootDoc.y = Number(value)); } + set shapeRot(value) { this.selectedInk?.forEach(i => i.rootDoc.rotation = Number(value)); } + set widthStk(value) { this.selectedInk?.forEach(i => i.rootDoc.strokeWidth = Number(value)); } + set shapeWid(value) { + this.selectedInk?.filter(i => i.rootDoc._width && i.rootDoc._height).forEach(i => { + const oldWidth = NumCast(i.rootDoc._width); + i.rootDoc._width = Number(value); + this._lock && (i.rootDoc._height = (i.rootDoc._width * NumCast(i.rootDoc._height)) / oldWidth); + }); + } + set shapeHgt(value) { + this.selectedInk?.filter(i => i.rootDoc._width && i.rootDoc._height).forEach(i => { + const oldHeight = NumCast(i.rootDoc._height); + i.rootDoc._height = Number(value); + this._lock && (i.rootDoc._width = (i.rootDoc._height * NumCast(i.rootDoc._width)) / oldHeight); + }); + } + + constructor(props: Readonly<{}>) { + super(props); + FormatShapePane.Instance = this; + this._canFade = false; + this.Pinned = BoolCast(Doc.UserDoc()["menuFormatShape-pinned"]); + } + + @action + closePane = () => { + this.fadeOut(false); + this.Pinned = false; + } + + @action + upDownButtons = (dirs: string, field: string) => { + switch (field) { + case "rot": this.rotate((dirs === "up" ? .1 : -.1)); break; + case "Xps": this.selectedInk?.forEach(i => i.rootDoc.x = NumCast(i.rootDoc.x) + (dirs === "up" ? 10 : -10)); break; + case "Yps": this.selectedInk?.forEach(i => i.rootDoc.y = NumCast(i.rootDoc.y) + (dirs === "up" ? 10 : -10)); break; + case "stk": this.selectedInk?.forEach(i => i.rootDoc.strokeWidth = NumCast(i.rootDoc.strokeWidth) + (dirs === "up" ? .1 : -.1)); break; + case "wid": this.selectedInk?.filter(i => i.rootDoc._width && i.rootDoc._height).forEach(i => { + const oldWidth = NumCast(i.rootDoc._width); + i.rootDoc._width = oldWidth + (dirs === "up" ? 10 : - 10); + this._lock && (i.rootDoc._height = (i.rootDoc._width / oldWidth * NumCast(i.rootDoc._height))); + }); + break; + case "hgt": this.selectedInk?.filter(i => i.rootDoc._width && i.rootDoc._height).forEach(i => { + const oldHeight = NumCast(i.rootDoc._height); + i.rootDoc._height = oldHeight + (dirs === "up" ? 10 : - 10); + this._lock && (i.rootDoc._width = (i.rootDoc._height / oldHeight * NumCast(i.rootDoc._width))); + }); + break; + } + } + + @undoBatch + @action + rotate = (degrees: number) => { + this.selectedInk?.forEach(action(inkView => { + const doc = Document(inkView.rootDoc); + if (doc.type === DocumentType.INK && doc.x && doc.y && doc._width && doc._height && doc.data) { + const angle = Number(degrees) - Number(doc.rotation); + doc.rotation = Number(degrees); + const ink = Cast(doc.data, InkField)?.inkData; + if (ink) { + const xs = ink.map(p => p.X); + const ys = ink.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 _centerPoints: { X: number, Y: number }[] = []; + _centerPoints.push({ X: left, Y: top }); + + const newPoints: { X: number, Y: number }[] = []; + for (var i = 0; i < ink.length; i++) { + const newX = Math.cos(angle) * (ink[i].X - _centerPoints[0].X) - Math.sin(angle) * (ink[i].Y - _centerPoints[0].Y) + _centerPoints[0].X; + const newY = Math.sin(angle) * (ink[i].X - _centerPoints[0].X) + Math.cos(angle) * (ink[i].Y - _centerPoints[0].Y) + _centerPoints[0].Y; + newPoints.push({ X: newX, Y: newY }); + } + doc.data = new InkField(newPoints); + const xs2 = newPoints.map(p => p.X); + const ys2 = newPoints.map(p => p.Y); + const left2 = Math.min(...xs2); + const top2 = Math.min(...ys2); + const right2 = Math.max(...xs2); + const bottom2 = Math.max(...ys2); + doc._height = (bottom2 - top2) * inkView.props.ScreenToLocalTransform().Scale; + doc._width = (right2 - left2) * inkView.props.ScreenToLocalTransform().Scale; + } + } + })); + } + + + colorPicker(setter: (color: string) => {}) { + return <div className="btn-group-palette" key="colorpicker" > + {this._palette.map(color => + <button className="antimodeMenu-button" key={color} onPointerDown={undoBatch(action(() => setter(color)))} style={{ zIndex: 1001, position: "relative" }}> + <div className="color-previewII" style={{ backgroundColor: color }} /> + </button>)} + </div>; + } + inputBox = (key: string, value: any, setter: (val: string) => {}) => { + return <> + <input style={{ color: "black", width: 80, position: "absolute", right: 20 }} + type="text" value={value} + onChange={e => setter(e.target.value)} + autoFocus /> + <button className="antiMenu-Buttonup" key="up" onPointerDown={undoBatch(action(() => this.upDownButtons("up", key)))}> + ˄ + </button> + <br /> + <button className="antiMenu-Buttonup" key="down" onPointerDown={undoBatch(action(() => this.upDownButtons("down", key)))} style={{ marginTop: -8 }}> + ˅ + </button> + </>; + } + + colorButton(value: string, setter: () => {}) { + return <> + <button className="antimodeMenu-button" key="fill" onPointerDown={undoBatch(action(e => setter()))} style={{ position: "absolute", right: 80 }}> + <FontAwesomeIcon icon="fill-drip" size="lg" /> + <div className="color-previewI" style={{ backgroundColor: value ?? "121212" }} /> + </button> + <br /> <br /> + </>; + } + + @computed get fillButton() { return this.colorButton(this.colorFil, () => this._fillBtn = !this._fillBtn); } + @computed get lineButton() { return this.colorButton(this.colorStk, () => this._lineBtn = !this._lineBtn); } + + @computed get fillPicker() { return this.colorPicker((color: string) => this.colorFil = color); } + @computed get linePicker() { return this.colorPicker((color: string) => this.colorStk = color); } + + @computed get stkInput() { return this.inputBox("stk", this.widthStk, (val: string) => this.widthStk = val); } + @computed get hgtInput() { return this.inputBox("hgt", this.shapeHgt, (val: string) => this.shapeHgt = val); } + @computed get widInput() { return this.inputBox("wid", this.shapeWid, (val: string) => this.shapeWid = val); } + @computed get rotInput() { return this.inputBox("rot", this.shapeRot, (val: string) => this.shapeRot = val); } + @computed get XpsInput() { return this.inputBox("Xps", this.shapeXps, (val: string) => this.shapeXps = val); } + @computed get YpsInput() { return this.inputBox("Yps", this.shapeYps, (val: string) => this.shapeYps = val); } + + @computed get propertyGroupItems() { + const fillCheck = <div key="fill" style={{ display: this._subOpen[0] ? "" : "none", width: "inherit", backgroundColor: "#323232", color: "white", }}> + <input className="formatShapePane-inputBtn" type="radio" checked={this.unFilled} onChange={undoBatch(action(() => this.unFilled = true))} /> + No Fill + <br /> + <input className="formatShapePane-inputBtn" type="radio" checked={this.solidFil} onChange={undoBatch(action(() => this.solidFil = true))} /> + Solid Fill + <br /> <br /> + {this.solidFil ? "Color" : ""} + {this.solidFil ? this.fillButton : ""} + {this._fillBtn && this.solidFil ? this.fillPicker : ""} + </div>; + + const markers = <> + <input key="markHead" className="formatShapePane-inputBtn" type="checkbox" checked={this.markHead !== ""} onChange={undoBatch(action(() => this.markHead = this.markHead ? "" : "arrow"))} /> + Arrow Head + <br /> + <input key="markTail" className="formatShapePane-inputBtn" type="checkbox" checked={this.markTail !== ""} onChange={undoBatch(action(() => this.markTail = this.markTail ? "" : "arrow"))} /> + Arrow End + <br /> + </>; + + const lineCheck = <div key="lineCheck" style={{ display: this._subOpen[1] ? "" : "none", width: "inherit", backgroundColor: "#323232", color: "white", }}> + <input className="formatShapePane-inputBtn" type="radio" checked={this.unStrokd} onChange={undoBatch(action(() => this.unStrokd = true))} /> + No Line + <br /> + <input className="formatShapePane-inputBtn" type="radio" checked={this.solidStk} onChange={undoBatch(action(() => this.solidStk = true))} /> + Solid Line + <br /> + <input className="formatShapePane-inputBtn" type="radio" checked={this.dashdStk ? true : false} onChange={undoBatch(action(() => this.dashdStk = "2"))} /> + Dash Line + <br /> + <br /> + {(this.solidStk || this.dashdStk) ? "Color" : ""} + {(this.solidStk || this.dashdStk) ? this.lineButton : ""} + {(this.solidStk || this.dashdStk) && this._lineBtn ? this.linePicker : ""} + <br /> + {(this.solidStk || this.dashdStk) ? "Width" : ""} + {(this.solidStk || this.dashdStk) ? this.stkInput : ""} + {(this.solidStk || this.dashdStk) ? <input type="range" defaultValue={Number(this.widthStk)} min={1} max={100} onChange={e => this.widthStk = e.target.value} /> : (null)} + <br /> <br /> + {(this.solidStk || this.dashdStk) ? markers : ""} + </div>; + + const sizeCheck = <div key="sizeCheck" style={{ display: this._subOpen[2] ? "" : "none", width: "inherit", backgroundColor: "#323232", color: "white", }}> + Height {this.hgtInput} + <br /> <br /> + Width {this.widInput} + <br /> <br /> + <input className="formatShapePane-inputBtn" style={{ right: 0 }} type="checkbox" checked={this._lock} onChange={undoBatch(action(() => this._lock = !this._lock))} /> + Lock Ratio + <br /> <br /> + Rotation {this.rotInput} + <br /> <br /> + </div>; + + const positionCheck = <div key="posCheck" style={{ display: this._subOpen[3] ? "" : "none", width: "inherit", backgroundColor: "#323232", color: "white", }}> + Horizontal {this.XpsInput} + <br /> <br /> + Vertical {this.YpsInput} + <br /> <br /> + </div>; + + const subMenus = this._currMode === "fill-drip" ? [`fill`, `line`] : [`size`, `position`]; + const menuItems = this._currMode === "fill-drip" ? [fillCheck, lineCheck] : [sizeCheck, positionCheck]; + const indexOffset = this._currMode === "fill-drip" ? 0 : 2; + return <div className="antimodeMenu-sub" key="submenu" style={{ position: "absolute", width: "inherit", top: 60 }}> + {subMenus.map((subMenu, i) => + <div key={subMenu} style={{ width: "inherit" }}> + <button className="antimodeMenu-button" onPointerDown={action(() => this._subOpen[i + indexOffset] = !this._subOpen[i + indexOffset])} + style={{ backgroundColor: "121212", position: "relative", width: "inherit" }}> + {this._subOpen[i + indexOffset] ? "▼" : "▶︎"} + {subMenu} + </button> + {menuItems[i]} + </div>)} + </div>; + } + + @computed get closeBtn() { + return <button className="antimodeMenu-button" key="close" onPointerDown={action(() => this.closePane())} style={{ position: "absolute", right: 0 }}> + X + </button>; + } + + @computed get propertyGroupBtn() { + return <div className="antimodeMenu-button-tab" key="modes"> + {this._mode.map(mode => + <button className="antimodeMenu-button" key={mode} onPointerDown={action(() => this._currMode = mode)} + style={{ backgroundColor: this._currMode === mode ? "121212" : "", position: "relative", top: 30 }}> + <FontAwesomeIcon icon={mode as IconProp} size="lg" /> + </button>)} + </div>; + } + + render() { + return this.getElementVert([this.closeBtn, this.propertyGroupBtn, this.propertyGroupItems]); + } +}
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/InkOptionsMenu.scss b/src/client/views/collections/collectionFreeForm/InkOptionsMenu.scss deleted file mode 100644 index 753de6bef..000000000 --- a/src/client/views/collections/collectionFreeForm/InkOptionsMenu.scss +++ /dev/null @@ -1,78 +0,0 @@ -.antimodeMenu-button { - .color-previewI { - width: 100%; - height: 40%; - } - - .color-previewII { - width: 100%; - height: 100%; - } - -} - -.sketch-picker { - background: #323232; - - .flexbox-fit { - background: #323232; - } -} - -.btn-group { - display: grid; - grid-template-columns: auto auto auto auto; - /* Make the buttons appear below each other */ -} - -.btn2-group { - display: block; - background: #323232; - grid-template-columns: auto; - - /* Make the buttons appear below each other */ - .antimodeMenu-button { - background: #323232; - display: block; - - } -} - -@media only screen and (max-device-width: 480px) { - .antimodeMenu-button { - font-size: 50%; - - .color-preview { - width: 100%; - height: 100%; - } - - } - - .sketch-picker { - background: #323232; - - .flexbox-fit { - background: #323232; - } - } - - .btn-group { - display: grid; - grid-template-columns: auto auto; - /* Make the buttons appear below each other */ - } - - .btn2-group { - display: block; - background: #323232; - grid-template-columns: auto; - - /* Make the buttons appear below each other */ - .antimodeMenu-button { - background: #323232; - display: block; - font-size: 50%; - } - } -}
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx deleted file mode 100644 index f47fca6ac..000000000 --- a/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx +++ /dev/null @@ -1,332 +0,0 @@ -import React = require("react"); -import AntimodeMenu from "../../AntimodeMenu"; -import { observer } from "mobx-react"; -import { observable, action, computed } from "mobx"; -import "./InkOptionsMenu.scss"; -import { ActiveInkColor, ActiveInkBezierApprox, ActiveFillColor, ActiveArrowStart, ActiveArrowEnd, SetActiveInkWidth, SetActiveInkColor, SetActiveBezierApprox, SetActiveFillColor, SetActiveArrowStart, SetActiveArrowEnd, ActiveDash, SetActiveDash } from "../../InkingStroke"; -import { Scripting } from "../../../util/Scripting"; -import { InkTool } from "../../../../fields/InkField"; -import { ColorState } from "react-color"; -import { Utils } from "../../../../Utils"; -import GestureOverlay from "../../GestureOverlay"; -import { Doc } from "../../../../fields/Doc"; -import { SelectionManager } from "../../../util/SelectionManager"; -import { DocumentView } from "../../../views/nodes/DocumentView"; -import { Document } from "../../../../fields/documentSchemas"; -import { DocumentType } from "../../../documents/DocumentTypes"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; -import { faBold, faItalic, faChevronLeft, faUnderline, faStrikethrough, faSubscript, faSuperscript, faIndent, faEyeDropper, faCaretDown, faPalette, faArrowsAlt, faHighlighter, faLink, faPaintRoller, faSleigh, faBars, faFillDrip, faBrush, faPenNib, faShapes, faArrowLeft, faEllipsisH, faBezierCurve, } from "@fortawesome/free-solid-svg-icons"; - -library.add(faBold, faItalic, faChevronLeft, faUnderline, faStrikethrough, faSuperscript, faSubscript, faIndent, faEyeDropper, faCaretDown, faPalette, faArrowsAlt, faHighlighter, faLink, faPaintRoller, faBars, faFillDrip, faBrush, faPenNib, faShapes, faArrowLeft, faEllipsisH, faBezierCurve); - - - -@observer -export default class InkOptionsMenu extends AntimodeMenu { - static Instance: InkOptionsMenu; - - private _palette = ["#D0021B", "#F5A623", "#F8E71C", "#8B572A", "#7ED321", "#417505", "#9013FE", "#4A90E2", "#50E3C2", "#B8E986", "#000000", "#4A4A4A", "#9B9B9B", "#FFFFFF", "none"]; - private _width = ["1", "5", "10", "100"]; - // private _buttons = ["circle", "triangle", "rectangle", "arrow", "line"]; - // private _icons = ["O", "∆", "ロ", "➜", "-"]; - private _buttons = ["circle", "triangle", "rectangle", "line", "noRec", "",]; - private _icons = ["O", "∆", "ロ", "⎯", "✖︎", " "]; - //arrowStart and arrowEnd must match and defs must exist in Inking Stroke - private _arrowStart = ["arrowHead", "arrowHead", "dot", "dot", "none"]; - private _arrowEnd = ["none", "arrowEnd", "none", "dot", "none"]; - private _arrowIcons = ["→", "↔︎", "•", "••", " "]; - - @observable _colorBtn = false; - @observable _widthBtn = false; - @observable _fillBtn = false; - @observable _arrowBtn = false; - @observable _dashBtn = false; - @observable _shapeBtn = false; - - - - constructor(props: Readonly<{}>) { - super(props); - InkOptionsMenu.Instance = this; - this._canFade = false; // don't let the inking menu fade away - } - - getColors = () => { - return this._palette; - } - - @action - changeArrow = (arrowStart: string, arrowEnd: string) => { - SetActiveArrowStart(arrowStart); - SetActiveArrowEnd(arrowEnd); - } - - @action - changeColor = (color: string, type: string) => { - const col: ColorState = { - hex: color, hsl: { a: 0, h: 0, s: 0, l: 0, source: "" }, hsv: { a: 0, h: 0, s: 0, v: 0, source: "" }, - rgb: { a: 0, r: 0, b: 0, g: 0, source: "" }, oldHue: 0, source: "", - }; - if (type === "color") { - SetActiveInkColor(Utils.colorString(col)); - } else if (type === "fill") { - SetActiveFillColor(Utils.colorString(col)); - } - } - - @action - editProperties = (value: any, field: string) => { - SelectionManager.SelectedDocuments().forEach(action((element: DocumentView) => { - const doc = Document(element.rootDoc); - if (doc.type === DocumentType.INK) { - switch (field) { - case "width": - doc.strokeWidth = Number(value); - break; - case "color": - doc.color = String(value); - break; - case "fill": - doc.fillColor = String(value); - break; - case "bezier": - // doc.strokeBezier === 300 ? doc.strokeBezier = 0 : doc.strokeBezier = 300; - break; - case "arrowStart": - doc.arrowStart = String(value); - break; - case "arrowEnd": - doc.arrowEnd = String(value); - break; - case "dash": - doc.dash = Number(value); - default: - break; - } - } - })); - } - - - @action - changeBezier = (e: React.PointerEvent): void => { - SetActiveBezierApprox(!ActiveInkBezierApprox() ? "300" : ""); - this.editProperties(0, "bezier"); - } - @action - changeDash = (e: React.PointerEvent): void => { - SetActiveDash(ActiveDash() === "0" ? "2" : "0"); - this.editProperties(ActiveDash(), "dash"); - } - - @computed get arrowPicker() { - var currIcon; - for (var i = 0; i < this._arrowStart.length; i++) { - if (this._arrowStart[i] === ActiveArrowStart() && this._arrowEnd[i] === ActiveArrowEnd()) { - currIcon = this._arrowIcons[i]; - if (this._arrowIcons[i] === " ") { - currIcon = "➤"; - } - } - } - var arrowPicker = <button - className="antimodeMenu-button" - key="arrow" - onPointerDown={action(e => this._arrowBtn = !this._arrowBtn)} - style={{ backgroundColor: this._arrowBtn ? "121212" : "" }}> - {currIcon} - </button>; - if (this._arrowBtn) { - arrowPicker = <div className="btn2-group" key="arrows"> - {arrowPicker} - {this._arrowStart.map((arrowStart, i) => { - return <button - className="antimodeMenu-button" - key={arrowStart} - onPointerDown={action(() => { SetActiveArrowStart(arrowStart); SetActiveArrowEnd(this._arrowEnd[i]); this.editProperties(arrowStart, "arrowStart"), this.editProperties(this._arrowEnd[i], "arrowEnd"); this._arrowBtn = false; })} - style={{ backgroundColor: this._arrowBtn ? "121212" : "" }}> - {this._arrowIcons[i]} - </button>; - })} - </div>; - } - return arrowPicker; - } - - @computed get widthPicker() { - var widthPicker = <button - className="antimodeMenu-button" - key="width" - onPointerDown={action(e => this._widthBtn = !this._widthBtn)} - style={{ backgroundColor: this._widthBtn ? "121212" : "" }}> - <FontAwesomeIcon icon="bars" size="lg" /> - </button>; - if (this._widthBtn) { - widthPicker = <div className="btn2-group" key="width"> - {widthPicker} - {this._width.map(wid => { - return <button - className="antimodeMenu-button" - key={wid} - onPointerDown={action(() => { SetActiveInkWidth(wid); this._widthBtn = false; this.editProperties(wid, "width"); })} - style={{ backgroundColor: this._widthBtn ? "121212" : "" }}> - {wid} - </button>; - })} - </div>; - } - return widthPicker; - } - - - - @computed get colorPicker() { - var colorPicker = <button - className="antimodeMenu-button" - key="color" - title="colorChanger" - onPointerDown={action(e => this._colorBtn = !this._colorBtn)} - style={{ backgroundColor: this._colorBtn ? "121212" : "" }}> - <FontAwesomeIcon icon="pen-nib" size="lg" /> - <div className="color-previewI" style={{ backgroundColor: ActiveInkColor() ?? "121212" }}></div> - - </button>; - if (this._colorBtn) { - colorPicker = <div className="btn-group" key="color"> - {colorPicker} - {this._palette.map(color => { - return <button - className="antimodeMenu-button" - key={color} - onPointerDown={action(() => { this.changeColor(color, "color"); this._colorBtn = false; this.editProperties(color, "color"); })} - style={{ backgroundColor: this._colorBtn ? "121212" : "" }}> - {/* <FontAwesomeIcon icon="pen-nib" size="lg" /> */} - <div className="color-previewII" style={{ backgroundColor: color }}></div> - </button>; - })} - </div>; - } - return colorPicker; - } - - @computed get fillPicker() { - var fillPicker = <button - className="antimodeMenu-button" - key="fill" - title="fillChanger" - onPointerDown={action(e => this._fillBtn = !this._fillBtn)} - style={{ backgroundColor: this._fillBtn ? "121212" : "" }}> - <FontAwesomeIcon icon="fill-drip" size="lg" /> - <div className="color-previewI" style={{ backgroundColor: ActiveFillColor() ?? "121212" }}></div> - </button>; - if (this._fillBtn) { - fillPicker = <div className="btn-group" key="fill"> - {fillPicker} - {this._palette.map(color => { - return <button - className="antimodeMenu-button" - key={color} - onPointerDown={action(() => { this.changeColor(color, "fill"); this._fillBtn = false; this.editProperties(color, "fill"); })} - style={{ backgroundColor: this._fillBtn ? "121212" : "" }}> - <div className="color-previewII" style={{ backgroundColor: color }}></div> - </button>; - })} - - </div>; - } - return fillPicker; - } - - @computed get shapePicker() { - var currIcon; - if (GestureOverlay.Instance.InkShape === "") { - currIcon = <FontAwesomeIcon icon="shapes" size="lg" />; - } else { - for (var i = 0; i < this._icons.length; i++) { - if (GestureOverlay.Instance.InkShape === this._buttons[i]) { - currIcon = this._icons[i]; - } - } - } - var shapePicker = <button - className="antimodeMenu-button" - key="shape" - onPointerDown={action(e => this._shapeBtn = !this._shapeBtn)} - style={{ backgroundColor: this._shapeBtn ? "121212" : "" }}> - {currIcon} - </button>; - if (this._shapeBtn) { - shapePicker = <div className="btn2-group" key="shape"> - {shapePicker} - {this._buttons.map((btn, i) => { - var ttl = btn; - if (btn === "") { - ttl = "no shape"; - } - if (btn === "noRec") { - ttl = "disable shape recognition"; - } - return <button - className="antimodeMenu-button" - title={`Draw ${btn}`} - key={ttl} - onPointerDown={action((e) => { GestureOverlay.Instance.InkShape = btn; this._shapeBtn = false; })} - style={{ backgroundColor: this._shapeBtn ? "121212" : "" }}> - {this._icons[i]} - </button>; - })} - </div>; - } - return shapePicker; - } - - @computed get bezierButton() { - return <button - className="antimodeMenu-button" - title="Bezier changer" - key="bezier" - onPointerDown={e => this.changeBezier(e)} - style={{ backgroundColor: ActiveInkBezierApprox() ? "121212" : "" }}> - <FontAwesomeIcon icon="bezier-curve" size="lg" /> - - </button>; - } - - @computed get dashButton() { - return <button - className="antimodeMenu-button" - title="dash changer" - key="dash" - onPointerDown={e => this.changeDash(e)} - style={{ backgroundColor: ActiveDash() !== "0" ? "121212" : "" }}> - <FontAwesomeIcon icon="ellipsis-h" size="lg" /> - - </button>; - } - - render() { - const buttons = [ - // <button className="antimodeMenu-button" title="Drag" key="drag" onPointerDown={e => this.dragStart(e)}> - // <FontAwesomeIcon icon="arrows-alt" size="lg" /> - // </button>, - this.shapePicker, - this.bezierButton, - this.widthPicker, - this.colorPicker, - this.fillPicker, - this.arrowPicker, - this.dashButton, - ]; - return this.getElement(buttons); - } -} -Scripting.addGlobal(function activatePen(penBtn: any) { - if (penBtn) { - Doc.SetSelectedTool(InkTool.Pen); - InkOptionsMenu.Instance.jumpTo(300, 300); - } else { - Doc.SetSelectedTool(InkTool.None); - InkOptionsMenu.Instance.fadeOut(true); - } -}); diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index b47236bea..aa9a3b4ae 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -1,6 +1,7 @@ import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; -import { Doc, Opt, DocListCast, DataSym } from "../../../../fields/Doc"; +import { Doc, Opt, DocListCast, DataSym, AclEdit, AclAddonly } from "../../../../fields/Doc"; +import { GetEffectiveAcl, getPlaygroundMode } from "../../../../fields/util"; import { InkData, InkField, InkTool } from "../../../../fields/InkField"; import { List } from "../../../../fields/List"; import { RichTextField } from "../../../../fields/RichTextField"; @@ -129,7 +130,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque } else if (!e.ctrlKey && !e.metaKey) { FormattedTextBox.SelectOnLoadChar = FormattedTextBox.DefaultLayout ? e.key : ""; const tbox = Docs.Create.TextDocument("", { - _width: 200, _height: 100, x: x, y: y, _autoHeight: true, _fontSize: NumCast(Doc.UserDoc().fontSize), + _width: 200, _height: 100, x: x, y: y, _autoHeight: true, _fontSize: StrCast(Doc.UserDoc().fontSize), _fontFamily: StrCast(Doc.UserDoc().fontFamily), title: "-typed text-" }); @@ -188,15 +189,18 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque onPointerDown = (e: React.PointerEvent): void => { this._downX = this._lastX = e.clientX; this._downY = this._lastY = e.clientY; - // allow marquee if right click OR alt+left click OR space bar + left click - if (e.button === 2 || (e.button === 0 && (e.altKey || (MarqueeView.DragMarquee && this.props.active(true))))) { - // if (e.altKey || (MarqueeView.DragMarquee && this.props.active(true))) { - this.setPreviewCursor(e.clientX, e.clientY, true); - // (!e.altKey) && e.stopPropagation(); // bcz: removed so that you can alt-click on button in a collection to switch link following behaviors. - e.preventDefault(); - // } - // bcz: do we need this? it kills the context menu on the main collection if !altKey - // e.stopPropagation(); + if (!(e.nativeEvent as any).marqueeHit) { + (e.nativeEvent as any).marqueeHit = true; + // allow marquee if right click OR alt+left click OR space bar + left click + if (e.button === 2 || (e.button === 0 && (e.altKey || (MarqueeView.DragMarquee && this.props.active(true))))) { + // if (e.altKey || (MarqueeView.DragMarquee && this.props.active(true))) { + this.setPreviewCursor(e.clientX, e.clientY, true); + // (!e.altKey) && e.stopPropagation(); // bcz: removed so that you can alt-click on button in a collection to switch link following behaviors. + e.preventDefault(); + // } + // bcz: do we need this? it kills the context menu on the main collection if !altKey + // e.stopPropagation(); + } } } @@ -276,7 +280,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque } else { this._downX = x; this._downY = y; - PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument, this.props.nudge); + const effectiveAcl = GetEffectiveAcl(this.props.Document); + if (effectiveAcl === AclEdit || effectiveAcl === AclAddonly || getPlaygroundMode()) PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument, this.props.nudge); this.clearSelection(); } }); @@ -286,7 +291,10 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) { if (Doc.GetSelectedTool() === InkTool.None) { - !(e.nativeEvent as any).formattedHandled && this.setPreviewCursor(e.clientX, e.clientY, false); + if (!(e.nativeEvent as any).marqueeHit) { + (e.nativeEvent as any).marqueeHit = true; + !(e.nativeEvent as any).formattedHandled && this.setPreviewCursor(e.clientX, e.clientY, false); + } } // let the DocumentView stopPropagation of this event when it selects this document } else { // why do we get a click event when the cursor have moved a big distance? @@ -426,8 +434,6 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque }); CognitiveServices.Inking.Appliers.InterpretStrokes(strokes).then((results) => { // const wordResults = results.filter((r: any) => r.category === "inkWord"); - // console.log(wordResults); - // console.log(results); // for (const word of wordResults) { // const indices: number[] = word.strokeIds; // indices.forEach(i => { @@ -468,7 +474,6 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque // }); // } const lines = results.filter((r: any) => r.category === "line"); - console.log(lines); const text = lines.map((l: any) => l.recognizedText).join("\r\n"); this.props.addDocument(Docs.Create.TextDocument(text, { _width: this.Bounds.width, _height: this.Bounds.height, x: this.Bounds.left + this.Bounds.width, y: this.Bounds.top, title: text })); }); diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.scss b/src/client/views/collections/collectionGrid/CollectionGridView.scss index 9c2d5cbff..4d8473be9 100644 --- a/src/client/views/collections/collectionGrid/CollectionGridView.scss +++ b/src/client/views/collections/collectionGrid/CollectionGridView.scss @@ -8,7 +8,6 @@ .collectionGridView-gridContainer { height: 100%; overflow-y: auto; - background-color: white; overflow-x: hidden; display: flex; @@ -22,7 +21,7 @@ } .react-grid-layout { - width : 100%; + width: 100%; } .react-grid-item>.react-resizable-handle { diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.tsx b/src/client/views/collections/collectionGrid/CollectionGridView.tsx index 2015ca930..21f77e47b 100644 --- a/src/client/views/collections/collectionGrid/CollectionGridView.tsx +++ b/src/client/views/collections/collectionGrid/CollectionGridView.tsx @@ -1,7 +1,7 @@ import { action, computed, Lambda, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from "react"; -import { Doc, Opt } from '../../../../fields/Doc'; +import { Doc, Opt, WidthSym } from '../../../../fields/Doc'; import { documentSchema } from '../../../../fields/documentSchemas'; import { Id } from '../../../../fields/FieldSymbols'; import { makeInterface } from '../../../../fields/Schema'; @@ -30,8 +30,9 @@ export class CollectionGridView extends CollectionSubView(GridSchema) { private _resetListenerDisposer: Opt<Lambda>; // listens for when the reset button is clicked @observable private _rowHeight: Opt<number>; // temporary store of row height to make change undoable @observable private _scroll: number = 0; // required to make sure the decorations box container updates on scroll + private dropLocation: object = {}; // sets the drop location for external drops - @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); } + onChildClickHandler = () => ScriptCast(this.Document.onChildClick); @computed get numCols() { return NumCast(this.props.Document.gridNumCols, 10); } @computed get rowHeight() { return this._rowHeight === undefined ? NumCast(this.props.Document.gridRowHeight, 100) : this._rowHeight; } @@ -47,6 +48,9 @@ export class CollectionGridView extends CollectionSubView(GridSchema) { @computed get flexGrid() { return BoolCast(this.props.Document.gridFlex, true); } // is grid static/flexible i.e. whether nodes be moved around and resized @computed get compaction() { return StrCast(this.props.Document.gridStartCompaction, StrCast(this.props.Document.gridCompaction, "vertical")); } // is grid static/flexible i.e. whether nodes be moved around and resized + /** + * Sets up the listeners for the list of documents and the reset button. + */ componentDidMount() { this._changeListenerDisposer = reaction(() => this.childLayoutPairs, (pairs) => { const newLayouts: Layout[] = []; @@ -54,7 +58,15 @@ export class CollectionGridView extends CollectionSubView(GridSchema) { pairs.forEach((pair, i) => { const existing = oldLayouts.find(l => l.i === pair.layout[Id]); if (existing) newLayouts.push(existing); - else this.addLayoutItem(newLayouts, this.makeLayoutItem(pair.layout, this.unflexedPosition(i), !this.flexGrid)); + else { + if (Object.keys(this.dropLocation).length) { // external drop + this.addLayoutItem(newLayouts, this.makeLayoutItem(pair.layout, this.dropLocation as { x: number, y: number }, !this.flexGrid)); + this.dropLocation = {}; + } + else { // internal drop + this.addLayoutItem(newLayouts, this.makeLayoutItem(pair.layout, this.unflexedPosition(i), !this.flexGrid)); + } + } }); pairs?.length && this.setLayoutList(newLayouts); }, { fireImmediately: true }); @@ -68,11 +80,18 @@ export class CollectionGridView extends CollectionSubView(GridSchema) { }); } + /** + * Disposes the listeners. + */ componentWillUnmount() { this._changeListenerDisposer?.(); this._resetListenerDisposer?.(); } + /** + * @returns the default location of the grid node (i.e. when the grid is static) + * @param index + */ unflexedPosition(index: number): Omit<Layout, "i"> { return { x: (index % Math.floor(this.numCols / this.defaultW)) * this.defaultW, @@ -83,6 +102,9 @@ export class CollectionGridView extends CollectionSubView(GridSchema) { }; } + /** + * Maps the x- and y- coordinates of the event to a grid cell. + */ screenToCell(sx: number, sy: number) { const pt = this.props.ScreenToLocalTransform().transformPoint(sx, sy); const x = Math.floor(pt[0] / this.colWidthPlusGap); @@ -90,10 +112,16 @@ export class CollectionGridView extends CollectionSubView(GridSchema) { return { x, y }; } + /** + * Creates a layout object for a grid item + */ makeLayoutItem = (doc: Doc, pos: { x: number, y: number }, Static: boolean = false, w: number = this.defaultW, h: number = this.defaultH) => { return ({ i: doc[Id], w, h, x: pos.x, y: pos.y, static: Static }); } + /** + * Adds a layout to the list of layouts. + */ addLayoutItem = (layouts: Layout[], layout: Layout) => { const f = layouts.findIndex(l => l.i === layout.i); f !== -1 && layouts.splice(f, 1); @@ -215,6 +243,9 @@ export class CollectionGridView extends CollectionSubView(GridSchema) { this.savedLayoutList.map((layout, index) => Object.assign(layout, this.unflexedPosition(index))); } + /** + * Handles internal drop of Dash documents. + */ @action onInternalDrop = (e: Event, de: DragManager.DropEvent) => { const savedLayouts = this.savedLayoutList; @@ -228,12 +259,24 @@ export class CollectionGridView extends CollectionSubView(GridSchema) { } /** + * Handles external drop of images/PDFs etc from outside Dash. + */ + @action + onExternalDrop = async (e: React.DragEvent): Promise<void> => { + this.dropLocation = this.screenToCell(e.clientX, e.clientY); + super.onExternalDrop(e, {}); + } + + /** * Handles the change in the value of the rowHeight slider. */ @action onSliderChange = (event: React.ChangeEvent<HTMLInputElement>) => { this._rowHeight = event.currentTarget.valueAsNumber; } + /** + * Handles the user clicking on the slider. + */ @action onSliderDown = (e: React.PointerEvent) => { this._rowHeight = this.rowHeight; // uses _rowHeight during dragging and sets doc's rowHeight when finished so that operation is undoable @@ -248,11 +291,13 @@ export class CollectionGridView extends CollectionSubView(GridSchema) { */ onContextMenu = () => { const displayOptionsMenu: ContextMenuProps[] = []; - displayOptionsMenu.push({ description: "Contents", event: () => this.props.Document.display = "contents", icon: "copy" }); - displayOptionsMenu.push({ description: "Undefined", event: () => this.props.Document.display = undefined, icon: "exclamation" }); + displayOptionsMenu.push({ description: "Toggle Content Display Style", event: () => this.props.Document.display = this.props.Document.display ? undefined : "contents", icon: "copy" }); ContextMenu.Instance.addItem({ description: "Display", subitems: displayOptionsMenu, icon: "tv" }); } + /** + * Handles text document creation on double click. + */ onPointerDown = (e: React.PointerEvent) => { if (this.props.active(true)) { setupMoveUpEvents(this, e, returnFalse, returnFalse, @@ -276,8 +321,11 @@ export class CollectionGridView extends CollectionSubView(GridSchema) { <div className="collectionGridView-contents" ref={this.createDashEventsTarget} style={{ pointerEvents: !this.props.active() && !SnappingManager.GetIsDragging() ? "none" : undefined }} onContextMenu={this.onContextMenu} - onPointerDown={e => this.onPointerDown(e)} > + onPointerDown={this.onPointerDown} + onDrop={this.onExternalDrop} + > <div className="collectionGridView-gridContainer" ref={this._containerRef} + style={{ backgroundColor: StrCast(this.layoutDoc._backgroundColor, "white") }} onWheel={e => e.stopPropagation()} onScroll={action(e => { if (!this.props.isSelected()) e.currentTarget.scrollTop = this._scroll; diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx index cd25c21b4..21d283547 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx @@ -202,9 +202,8 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu } - @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); } - @computed get onChildDoubleClickHandler() { return ScriptCast(this.Document.onChildDoubleClick); } - + onChildClickHandler = () => ScriptCast(this.Document.onChildClick); + onChildDoubleClickHandler = () => ScriptCast(this.Document.onChildDoubleClick); addDocTab = (doc: Doc, where: string) => { if (where === "inPlace" && this.layoutDoc.isInPlaceContainer) { diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx index 51dcdcfe6..d02088a6c 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx @@ -202,8 +202,8 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument) } - @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); } - @computed get onChildDoubleClickHandler() { return ScriptCast(this.Document.onChildDoubleClick); } + onChildClickHandler = () => ScriptCast(this.Document.onChildClick); + onChildDoubleClickHandler = () => ScriptCast(this.Document.onChildDoubleClick); addDocTab = (doc: Doc, where: string) => { if (where === "inPlace" && this.layoutDoc.isInPlaceContainer) { diff --git a/src/client/views/linking/LinkEditor.scss b/src/client/views/linking/LinkEditor.scss index 406a38c26..d26b7920a 100644 --- a/src/client/views/linking/LinkEditor.scss +++ b/src/client/views/linking/LinkEditor.scss @@ -3,31 +3,60 @@ .linkEditor { width: 100%; height: auto; - font-size: 12px; // TODO + font-size: 13px; // TODO user-select: none; } .linkEditor-button-back { - margin-bottom: 6px; + //margin-bottom: 6px; border-radius: 10px; width: 18px; height: 18px; padding: 0; + + &:hover { + cursor: pointer; + } } .linkEditor-info { //border-bottom: 0.5px solid $light-color-secondary; - padding-bottom: 4px; + //padding-bottom: 1px; padding-top: 5px; padding-left: 5px; //margin-bottom: 6px; display: flex; justify-content: space-between; + color: black; .linkEditor-linkedTo { width: calc(100% - 26px); padding-left: 5px; - padding-right: 5px + padding-right: 5px; + + .linkEditor-downArrow { + &:hover { + cursor: pointer; + } + } + } +} + +.linkEditor-moreInfo { + margin-left: 12px; + padding-left: 13px; + padding-right: 6.5px; + padding-bottom: 4px; + font-size: 9px; + //font-style: italic; + text-decoration-color: grey; + + .button { + color: black; + + &:hover { + cursor: pointer; + } } } @@ -36,25 +65,58 @@ padding-right: 6.5px; padding-bottom: 3.5px; - .linkEditor-description-text { - text-decoration-color: grey; + .linkEditor-description-label { + text-decoration-color: black; + color: black; } .linkEditor-description-input { - border: 1px solid grey; - border-radius: 4px; - background-color: rgb(236, 236, 236); - padding-left: 2px; - padding-right: 2px; - color: grey; - text-decoration-color: grey; + display: flex; + + .linkEditor-description-editing { + min-width: 85%; + //border: 1px solid grey; + //border-radius: 4px; + padding-left: 2px; + padding-right: 2px; + //margin-right: 4px; + color: black; + text-decoration-color: grey; + } + + .linkEditor-description-add-button { + display: inline; + /* float: right; */ + border-radius: 7px; + font-size: 9px; + background-color: black; + /* padding: 3px; */ + padding-top: 4px; + padding-left: 7px; + padding-bottom: 4px; + padding-right: 8px; + height: 80%; + color: white; + + &:hover { + cursor: pointer; + } + } } } .linkEditor-followingDropdown { padding-left: 6.5px; padding-right: 6.5px; - padding-bottom: 3.5px; + padding-bottom: 6px; + + &:hover { + cursor: pointer; + } + + .linkEditor-followingDropdown-label { + color: black; + } .linkEditor-followingDropdown-dropdown { @@ -62,14 +124,15 @@ border: 1px solid grey; border-radius: 4px; - background-color: rgb(236, 236, 236); + //background-color: rgb(236, 236, 236); padding-left: 2px; padding-right: 2px; - color: grey; - text-decoration-color: grey; + text-decoration-color: black; + color: rgb(94, 94, 94); .linkEditor-followingDropdown-icon { float: right; + color: black; } } @@ -77,17 +140,22 @@ padding-left: 3px; padding-right: 3px; + &:last-child { + border-bottom: none; + } + .linkEditor-followingDropdown-option { - border: 0.25px dotted grey; - background-color: rgb(236, 236, 236); + border: 0.25px solid grey; + //background-color: rgb(236, 236, 236); padding-left: 2px; padding-right: 2px; color: grey; text-decoration-color: grey; font-size: 9px; + border-top: none; &:hover { - background-color: rgb(211, 210, 210); + background-color: rgb(187, 220, 231); } } @@ -98,6 +166,10 @@ } + + + + .linkEditor-button, .linkEditor-addbutton { width: 18px; diff --git a/src/client/views/linking/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx index 014d57ed0..04329182e 100644 --- a/src/client/views/linking/LinkEditor.tsx +++ b/src/client/views/linking/LinkEditor.tsx @@ -1,10 +1,10 @@ import { library } from "@fortawesome/fontawesome-svg-core"; import { faArrowLeft, faCog, faEllipsisV, faExchangeAlt, faPlus, faTable, faTimes, faTrash } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, observable, computed } from "mobx"; +import { action, observable, computed, toJS } from "mobx"; import { observer } from "mobx-react"; import { Doc, Opt } from "../../../fields/Doc"; -import { StrCast } from "../../../fields/Types"; +import { StrCast, DateCast } from "../../../fields/Types"; import { Utils } from "../../../Utils"; import { LinkManager } from "../../util/LinkManager"; import './LinkEditor.scss'; @@ -13,6 +13,8 @@ import { DocumentView } from "../nodes/DocumentView"; import { DocumentLinksButton } from "../nodes/DocumentLinksButton"; import { EditableView } from "../EditableView"; import { RefObject } from "react"; +import { Tooltip } from "@material-ui/core"; +import { undoBatch } from "../../util/UndoManager"; library.add(faArrowLeft, faEllipsisV, faTable, faTrash, faCog, faExchangeAlt, faTimes, faPlus); @@ -285,15 +287,16 @@ interface LinkEditorProps { @observer export class LinkEditor extends React.Component<LinkEditorProps> { - @observable description = StrCast(LinkManager.currentLink?.description); @observable openDropdown: boolean = false; - - @observable followBehavior = this.props.linkDoc.follow ? this.props.linkDoc.follow : "Default"; + @observable showInfo: boolean = false; + @computed get infoIcon() { if (this.showInfo) { return "chevron-up"; } return "chevron-down"; } + @observable private buttonColor: string = "black"; //@observable description = this.props.linkDoc.description ? StrCast(this.props.linkDoc.description) : "DESCRIPTION"; + @undoBatch @action deleteLink = (): void => { LinkManager.Instance.deleteLink(this.props.linkDoc); @@ -304,23 +307,54 @@ export class LinkEditor extends React.Component<LinkEditorProps> { setDescripValue = (value: string) => { if (LinkManager.currentLink) { LinkManager.currentLink.description = value; + this.buttonColor = "rgb(62, 133, 55)"; + setTimeout(action(() => { + this.buttonColor = "black"; + }), 750); return true; } } + @action + onKey = (e: React.KeyboardEvent<HTMLInputElement>) => { + if (e.key === "Enter") { + this.setDescripValue(this.description); + document.getElementById('input')?.blur(); + } + } + + @action + onDown = () => { + this.setDescripValue(this.description); + } + + @action + handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { + this.description = e.target.value; + } + + @computed get editDescription() { return <div className="linkEditor-description"> <div className="linkEditor-description-label"> - Link Description:</div> + Link Label:</div> <div className="linkEditor-description-input"> - <EditableView - GetValue={() => StrCast(LinkManager.currentLink?.description)} - SetValue={value => { this.setDescripValue(value); return false; }} - contents={LinkManager.currentLink?.description} - placeholder={"(optional) enter link description"} - color={"rgb(88, 88, 88)"} - ></EditableView></div></div>; + <div className="linkEditor-description-editing"> + <input + style={{ width: "100%" }} + id="input" + value={this.description} + placeholder={"enter link label"} + // color={"rgb(88, 88, 88)"} + onKeyDown={this.onKey} + onChange={this.handleChange} + ></input> + </div> + <div className="linkEditor-description-add-button" + style={{ backgroundColor: this.buttonColor }} + onPointerDown={this.onDown}>Set</div> + </div></div>; } @action @@ -331,8 +365,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> { @action changeFollowBehavior = (follow: string) => { this.openDropdown = false; - this.followBehavior = follow; - this.props.linkDoc.follow = follow; + Doc.GetProto(this.props.linkDoc).followLinkLocation = follow; } @computed @@ -343,10 +376,10 @@ export class LinkEditor extends React.Component<LinkEditorProps> { <div className="linkEditor-followingDropdown-dropdown"> <div className="linkEditor-followingDropdown-header" onPointerDown={this.changeDropdown}> - {this.followBehavior} + {StrCast(this.props.linkDoc.followLinkLocation, "Default")} <FontAwesomeIcon className="linkEditor-followingDropdown-icon" icon={this.openDropdown ? "chevron-up" : "chevron-down"} - size={"lg"} onPointerDown={this.changeDropdown} /> + size={"lg"} /> </div> <div className="linkEditor-followingDropdown-optionsList" style={{ display: this.openDropdown ? "" : "none" }}> @@ -355,11 +388,11 @@ export class LinkEditor extends React.Component<LinkEditorProps> { Default </div> <div className="linkEditor-followingDropdown-option" - onPointerDown={() => this.changeFollowBehavior("Always open in right tab")}> + onPointerDown={() => this.changeFollowBehavior("onRight")}> Always open in right tab </div> <div className="linkEditor-followingDropdown-option" - onPointerDown={() => this.changeFollowBehavior("Always open in new tab")}> + onPointerDown={() => this.changeFollowBehavior("inTab")}> Always open in new tab </div> </div> @@ -367,6 +400,11 @@ export class LinkEditor extends React.Component<LinkEditorProps> { </div>; } + @action + changeInfo = () => { + this.showInfo = !this.showInfo; + } + render() { const destination = LinkManager.Instance.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc); @@ -378,15 +416,23 @@ export class LinkEditor extends React.Component<LinkEditorProps> { return !destination ? (null) : ( <div className="linkEditor"> <div className="linkEditor-info"> - <button className="linkEditor-button-back" - style={{ display: this.props.hideback ? "none" : "" }} - onClick={this.props.showLinks}> - <FontAwesomeIcon icon="arrow-left" size="sm" /> </button> - <p className="linkEditor-linkedTo">editing link to: <b>{ + <Tooltip title={<><div className="dash-tooltip">Return to link menu</div></>} placement="top"> + <button className="linkEditor-button-back" + style={{ display: this.props.hideback ? "none" : "" }} + onClick={this.props.showLinks}> + <FontAwesomeIcon icon="arrow-left" size="sm" /> </button> + </Tooltip> + <p className="linkEditor-linkedTo">Editing Link to: <b>{ destination.proto?.title ?? destination.title ?? "untitled"}</b></p> - <button className="linkEditor-button" onPointerDown={() => this.deleteLink()} title="Delete link"> - <FontAwesomeIcon icon="trash" size="sm" /></button> + <Tooltip title={<><div className="dash-tooltip">Show more link information</div></>} placement="top"> + <div className="linkEditor-downArrow"><FontAwesomeIcon className="button" icon={this.infoIcon} size="lg" onPointerDown={this.changeInfo} /></div> + </Tooltip> </div> + {this.showInfo ? <div className="linkEditor-moreInfo"> + <div>{this.props.linkDoc.author ? <div> <b>Author:</b> {this.props.linkDoc.author}</div> : null}</div> + <div>{this.props.linkDoc.creationDate ? <div> <b>Creation Date:</b> + {DateCast(this.props.linkDoc.creationDate).toString()}</div> : null}</div> + </div> : null} <div>{this.editDescription}</div> <div>{this.followingDropdown}</div> diff --git a/src/client/views/linking/LinkMenu.scss b/src/client/views/linking/LinkMenu.scss index 4b1a3f425..98e4171f0 100644 --- a/src/client/views/linking/LinkMenu.scss +++ b/src/client/views/linking/LinkMenu.scss @@ -1,33 +1,66 @@ @import "../globalCssVariables"; .linkMenu { - width: 100%; + width: auto; height: auto; - //border: 1px solid black; + position: absolute; + top: 0; + left: 0; + + .linkMenu-list { + + display: inline-block; + + border: 1px solid black; + + box-shadow: 3px 3px 1.5px grey; + + max-height: 170px; + overflow-y: scroll; + position: relative; + z-index: 10; + background: white; + min-width: 170px; + //border-radius: 5px; + //padding-top: 6.5px; + //padding-bottom: 6.5px; + //padding-left: 6.5px; + //padding-right: 2px; + //width: calc(auto + 50px); + + white-space: nowrap; + overflow-x: hidden; + width: 240px; + scrollbar-color: white; + + &:last-child { + border-bottom: none; + } - &:hover { - width: calc(auto + 26px); + &:hover { + scrollbar-color: rgb(201, 239, 252); + } } -} -.linkMenu-list { - border: 1px solid black; - max-height: 200px; - overflow-y: scroll; - position: absolute; - z-index: 10; - background: white; - min-width: 150px; - border-radius: 5px; - padding-top: 6.5px; - padding-bottom: 6.5px; - padding-left: 6.5px; - padding-right: 2px; - //width: calc(auto + 50px); + .linkMenu-listEditor { + + display: inline-block; + + border: 1px solid black; + + box-shadow: 3px 3px 1.5px grey; + + max-height: 170px; + overflow-y: scroll; + position: relative; + z-index: 10; + background: white; + min-width: 170px; + } } .linkMenu-group { - //border-bottom: 0.5px solid lightgray; + border-bottom: 0.5px solid lightgray; //@extend: 5px 0; @@ -36,7 +69,6 @@ } .linkMenu-group-name { - display: flex; &:hover { @@ -46,7 +78,7 @@ } p.expand-one { - width: calc(100% + 26px); + width: calc(100% + 20px); } .linkEditor-tableButton { @@ -56,7 +88,7 @@ p { width: 100%; - padding: 4px 6px; + //padding: 4px 6px; line-height: 12px; border-radius: 5px; font-weight: bold; diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx index 8a7b12f48..7b5fb0127 100644 --- a/src/client/views/linking/LinkMenu.tsx +++ b/src/client/views/linking/LinkMenu.tsx @@ -1,4 +1,4 @@ -import { action, observable } from "mobx"; +import { action, observable, computed } from "mobx"; import { observer } from "mobx-react"; import { DocumentView } from "../nodes/DocumentView"; import { LinkEditor } from "./LinkEditor"; @@ -29,15 +29,23 @@ export class LinkMenu extends React.Component<Props> { @observable private _linkMenuRef = React.createRef<HTMLDivElement>(); private _editorRef = React.createRef<HTMLDivElement>(); + //@observable private _numLinks: number = 0; + + // @computed get overflow() { + // if (this._numLinks) { + // return "scroll"; + // } + // return "auto"; + // } + @action onClick = (e: PointerEvent) => { LinkDocPreview.LinkInfo = undefined; - if (this._linkMenuRef && !!!this._linkMenuRef.current?.contains(e.target as any)) { - if (this._editorRef && !!!this._editorRef.current?.contains(e.target as any)) { - console.log("outside click"); + if (this._linkMenuRef && !this._linkMenuRef.current?.contains(e.target as any)) { + if (this._editorRef && !this._editorRef.current?.contains(e.target as any)) { DocumentLinksButton.EditLink = undefined; } } @@ -81,13 +89,18 @@ export class LinkMenu extends React.Component<Props> { const sourceDoc = this.props.docView.props.Document; const groups: Map<string, Doc[]> = LinkManager.Instance.getRelatedGroupedLinks(sourceDoc); return <div className="linkMenu" ref={this._linkMenuRef} > - <div className="linkMenu-list" - style={{ left: this.props.location[0], top: this.props.location[1] }}> - {!this._editingLink ? - this.renderAllGroups(groups) : + {!this._editingLink ? <div className="linkMenu-list" style={{ + left: this.props.location[0], top: this.props.location[1] + }}> + {this.renderAllGroups(groups)} + </div> : <div className="linkMenu-listEditor" style={{ + left: this.props.location[0], top: this.props.location[1] + }}> <LinkEditor sourceDoc={this.props.docView.props.Document} linkDoc={this._editingLink} showLinks={action(() => this._editingLink = undefined)} /> - } - </div> </div>; + </div> + } + + </div>; } }
\ No newline at end of file diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx index ec17776e3..2ae87ac13 100644 --- a/src/client/views/linking/LinkMenuGroup.tsx +++ b/src/client/views/linking/LinkMenuGroup.tsx @@ -11,6 +11,7 @@ import { DocumentView } from "../nodes/DocumentView"; import './LinkMenu.scss'; import { LinkMenuItem, StartLinkTargetsDrag } from "./LinkMenuItem"; import React = require("react"); +import { Cast } from "../../../fields/Types"; interface LinkMenuGroupProps { sourceDoc: Doc; @@ -66,7 +67,8 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> { render() { const groupItems = this.props.group.map(linkDoc => { - const destination = LinkManager.Instance.getOppositeAnchor(linkDoc, this.props.sourceDoc); + const destination = LinkManager.Instance.getOppositeAnchor(linkDoc, this.props.sourceDoc) || + LinkManager.Instance.getOppositeAnchor(linkDoc, Cast(linkDoc.anchor2, Doc, null).annotationOn === this.props.sourceDoc ? Cast(linkDoc.anchor2, Doc, null) : Cast(linkDoc.anchor1, Doc, null)); if (destination && this.props.sourceDoc) { return <LinkMenuItem key={destination[Id] + this.props.sourceDoc[Id]} groupType={this.props.groupType} @@ -82,11 +84,13 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> { return ( <div className="linkMenu-group" ref={this._menuRef}> + {/* <div className="linkMenu-group-name"> <p ref={this._drag} onPointerDown={this.onLinkButtonDown} className={this.props.groupType === "*" || this.props.groupType === "" ? "" : "expand-one"} > {this.props.groupType}:</p> {this.props.groupType === "*" || this.props.groupType === "" ? <></> : this.viewGroupAsTable(this.props.groupType)} </div> */} + <div className="linkMenu-group-wrapper"> {groupItems} </div> diff --git a/src/client/views/linking/LinkMenuItem.scss b/src/client/views/linking/LinkMenuItem.scss index 67bf71fb9..4e13ef8c8 100644 --- a/src/client/views/linking/LinkMenuItem.scss +++ b/src/client/views/linking/LinkMenuItem.scss @@ -4,21 +4,72 @@ // border-top: 0.5px solid $main-accent; position: relative; display: flex; + border-bottom: 0.5px solid black; + + padding-left: 6.5px; + padding-right: 2px; + + padding-top: 6.5px; + padding-bottom: 6.5px; + + background-color: white; .linkMenu-name { position: relative; + width: auto; .linkMenu-text { padding: 4px 2px; //display: inline; - .linkMenu-destination-title { + .linkMenu-source-title { text-decoration: none; - color: rgb(85, 120, 196); - font-size: 14px; - padding-bottom: 2px; + color: rgb(43, 43, 43); + font-size: 7px; + padding-bottom: 0.75px; + + margin-left: 20px; + } + + + .linkMenu-title-wrapper { + + display: flex; + + .destination-icon-wrapper { + //border: 0.5px solid rgb(161, 161, 161); + margin-right: 2px; + padding-right: 2px; + + .destination-icon { + float: left; + width: 12px; + height: 12px; + padding: 1px; + margin-right: 3px; + color: rgb(161, 161, 161); + } + } + + .linkMenu-destination-title { + text-decoration: none; + color: rgb(85, 120, 196); + font-size: 14px; + padding-bottom: 2px; + padding-right: 4px; + margin-right: 4px; + float: right; + + &:hover { + text-decoration: underline; + color: rgb(60, 90, 156); + //display: inline; + text-overflow: break; + cursor: pointer; + } + } } .linkMenu-description { @@ -26,15 +77,20 @@ font-style: italic; color: rgb(95, 97, 102); font-size: 10px; + margin-left: 20px; + max-width: 125px; + height: auto; + white-space: break-spaces; } p { - //padding: 4px 2px; + padding-right: 4px; line-height: 12px; border-radius: 5px; - overflow-wrap: break-word; + //overflow-wrap: break-word; user-select: none; } + } } @@ -53,6 +109,7 @@ &:hover { + background-color: rgb(201, 239, 252); .linkMenu-item-buttons { display: flex; @@ -66,20 +123,6 @@ //display: inline; text-overflow: break; } - - &.expand-two p { - width: calc(100% - 52px); - //text-decoration: underline; - //color: rgb(15, 57, 148); - //background-color: lightgray; - } - - &.expand-three p { - width: calc(100% - 84px); - //text-decoration: underline; - //color: rgb(15, 57, 148); - //background-color: lightgray; - } } } } @@ -96,7 +139,8 @@ width: 20px; height: 20px; margin: 0; - margin-right: 6px; + margin-right: 4px; + padding-right: 6px; border-radius: 50%; pointer-events: auto; background-color: $dark-color; @@ -119,7 +163,7 @@ &:hover { background: $main-accent; - cursor: grab; + cursor: pointer; } } }
\ No newline at end of file diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx index 30571a165..cae90cd0c 100644 --- a/src/client/views/linking/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -1,6 +1,6 @@ import { library } from '@fortawesome/fontawesome-svg-core'; -import { faArrowRight, faChevronDown, faChevronUp, faEdit, faEye, faTimes, faPencilAlt } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faArrowRight, faChevronDown, faChevronUp, faEdit, faEye, faTimes, faPencilAlt, faEyeSlash } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon, FontAwesomeIconProps } from '@fortawesome/react-fontawesome'; import { action, observable } from 'mobx'; import { observer } from "mobx-react"; import { Doc, DocListCast } from '../../../fields/Doc'; @@ -15,10 +15,12 @@ import { setupMoveUpEvents, emptyFunction, Utils } from '../../../Utils'; import { DocumentView } from '../nodes/DocumentView'; import { DocumentLinksButton } from '../nodes/DocumentLinksButton'; import { LinkDocPreview } from '../nodes/LinkDocPreview'; -import { WebField } from '../../../fields/URLField'; import { Hypothesis } from '../../apis/hypothesis/HypothesisApiUtils'; import { Id } from '../../../fields/FieldSymbols'; -library.add(faEye, faEdit, faTimes, faArrowRight, faChevronDown, faChevronUp, faPencilAlt); +import { Tooltip } from '@material-ui/core'; +import { DocumentType } from '../../documents/DocumentTypes'; +import { undoBatch } from '../../util/UndoManager'; +library.add(faEye, faEdit, faTimes, faArrowRight, faChevronDown, faChevronUp, faPencilAlt, faEyeSlash); interface LinkMenuItemProps { @@ -116,7 +118,6 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { document.addEventListener("pointerup", this.onLinkButtonUp); if (this._buttonRef && !!!this._buttonRef.current?.contains(e.target as any)) { - console.log("outside click"); LinkDocPreview.LinkInfo = undefined; } } @@ -150,20 +151,12 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { @action.bound async followDefault() { - console.log("FOLLOWWW"); DocumentLinksButton.EditLink = undefined; LinkDocPreview.LinkInfo = undefined; // this.props.linkDoc.linksToAnnotation && (Doc.GetProto(this.props.destinationDoc).data = new WebField(StrCast(this.props.linkDoc.annotationUrl))); // if destination is a Hypothes.is annotation, redirect website to the annotation's URL to scroll to the annotation - - if (this.props.linkDoc.follow) { - if (this.props.linkDoc.follow === "Default") { - DocumentManager.Instance.FollowLink(this.props.linkDoc, this.props.sourceDoc, doc => this.props.addDocTab(doc, "onRight"), false); - } else if (this.props.linkDoc.follow === "Always open in right tab") { - this.props.addDocTab(this.props.destinationDoc, "onRight"); - } else if (this.props.linkDoc.follow === "Always open in new tab") { - this.props.addDocTab(this.props.destinationDoc, "inTab"); - } + if (this.props.linkDoc.followLinkLocation && this.props.linkDoc.followLinkLocation !== "Default") { + this.props.addDocTab(this.props.destinationDoc, StrCast(this.props.linkDoc.followLinkLocation)); } else { DocumentManager.Instance.FollowLink(this.props.linkDoc, this.props.sourceDoc, doc => this.props.addDocTab(doc, "onRight"), false); } @@ -171,23 +164,63 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { this.props.linkDoc.linksToAnnotation && setTimeout(() => window.open(StrCast(this.props.linkDoc.annotationUrl), '_blank'), 1000); // open external page if hypothes.is annotation } + @undoBatch @action deleteLink = (): void => { this.props.linkDoc.linksToAnnotation && Hypothesis.deleteLink(StrCast(this.props.linkDoc.annotationId), Utils.prepend("/doc/" + this.props.sourceDoc[Id])); // delete hyperlink in annotation this.props.linkDoc.linksToAnnotation && console.log("annotationId", this.props.linkDoc.annotationId); LinkManager.Instance.deleteLink(this.props.linkDoc); - //this.props.showLinks(); LinkDocPreview.LinkInfo = undefined; DocumentLinksButton.EditLink = undefined; } + @action + showLink = () => { + this.props.linkDoc.hidden = !this.props.linkDoc.hidden; + } + render() { const keys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);//groupMetadataKeys.get(this.props.groupType); const canExpand = keys ? keys.length > 0 : false; + const eyeIcon = this.props.linkDoc.hidden ? "eye-slash" : "eye"; + + let destinationIcon: FontAwesomeIconProps["icon"] = "question"; + switch (this.props.destinationDoc.type) { + case DocumentType.IMG: destinationIcon = "image"; break; + case DocumentType.COMPARISON: destinationIcon = "columns"; break; + case DocumentType.RTF: destinationIcon = "font"; break; + case DocumentType.COL: destinationIcon = "folder"; break; + case DocumentType.WEB: destinationIcon = "globe-asia"; break; + case DocumentType.SCREENSHOT: destinationIcon = "photo-video"; break; + case DocumentType.WEBCAM: destinationIcon = "video"; break; + case DocumentType.AUDIO: destinationIcon = "microphone"; break; + case DocumentType.BUTTON: destinationIcon = "bolt"; break; + case DocumentType.PRES: destinationIcon = "tv"; break; + case DocumentType.QUERY: destinationIcon = "search"; break; + case DocumentType.SCRIPTING: destinationIcon = "terminal"; break; + case DocumentType.IMPORT: destinationIcon = "cloud-upload-alt"; break; + case DocumentType.DOCHOLDER: destinationIcon = "expand"; break; + case DocumentType.VID: destinationIcon = "video"; break; + case DocumentType.INK: destinationIcon = "pen-nib"; break; + default: destinationIcon = "question"; break; + } + + const title = StrCast(this.props.destinationDoc.title).length > 18 ? + StrCast(this.props.destinationDoc.title).substr(0, 14) + "..." : this.props.destinationDoc.title; + + // ... + // from anika to bob: here's where the text that is specifically linked would show up (linkDoc.storedText) + // ... + const source = this.props.sourceDoc.type === DocumentType.RTF ? this.props.linkDoc.storedText ? + StrCast(this.props.linkDoc.storedText).length > 17 ? + StrCast(this.props.linkDoc.storedText).substr(0, 18) + : this.props.linkDoc.storedText : undefined : undefined; + return ( <div className="linkMenu-item"> <div className={canExpand ? "linkMenu-item-content expand-three" : "linkMenu-item-content expand-two"}> + <div ref={this._drag} className="linkMenu-name" //title="drag to view target. click to customize." onPointerLeave={action(() => LinkDocPreview.LinkInfo = undefined)} onPointerEnter={action(e => this.props.linkDoc && (LinkDocPreview.LinkInfo = { @@ -199,9 +232,16 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { onPointerDown={this.onLinkButtonDown}> <div className="linkMenu-text"> - <p className="linkMenu-destination-title" - onPointerDown={this.followDefault}> - {(this.props.linkDoc.linksToAnnotation ? "Annotation in " : "") + StrCast(this.props.destinationDoc.title)}</p> + {source ? <p className="linkMenu-source-title"> + <b>Source: {source}</b></p> : null} + <div className="linkMenu-title-wrapper"> + <div className="destination-icon-wrapper" > + <FontAwesomeIcon className="destination-icon" icon={destinationIcon} size="sm" /></div> + <p className="linkMenu-destination-title" + onPointerDown={this.followDefault}> + {this.props.linkDoc.linksToAnnotation ? "Annotation in" : ""} {title} + </p> + </div> {this.props.linkDoc.description !== "" ? <p className="linkMenu-description"> {StrCast(this.props.linkDoc.description)}</p> : null} </div> @@ -209,10 +249,19 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { {canExpand ? <div title="Show more" className="button" onPointerDown={e => this.toggleShowMore(e)}> <FontAwesomeIcon className="fa-icon" icon={this._showMore ? "chevron-up" : "chevron-down"} size="sm" /></div> : <></>} - <div title="Edit link" className="button" ref={this._editRef} onPointerDown={this.onEdit}> - <FontAwesomeIcon className="fa-icon" icon="edit" size="sm" /></div> - <div title="Delete link" className="button" onPointerDown={this.deleteLink}> - <FontAwesomeIcon className="fa-icon" icon="trash" size="sm" /></div> + <Tooltip title={<><div className="dash-tooltip">{this.props.linkDoc.hidden ? "Show link" : "Hide link"}</div></>}> + <div className="button" ref={this._editRef} onPointerDown={this.showLink}> + <FontAwesomeIcon className="fa-icon" icon={eyeIcon} size="sm" /></div> + </Tooltip> + + <Tooltip title={<><div className="dash-tooltip">Edit Link</div></>}> + <div className="button" ref={this._editRef} onPointerDown={this.onEdit}> + <FontAwesomeIcon className="fa-icon" icon="edit" size="sm" /></div> + </Tooltip> + <Tooltip title={<><div className="dash-tooltip">Delete Link</div></>}> + <div className="button" onPointerDown={this.deleteLink}> + <FontAwesomeIcon className="fa-icon" icon="trash" size="sm" /></div> + </Tooltip> {/* <div title="Follow link" className="button" onPointerDown={this.followDefault} onContextMenu={this.onContextMenu}> <FontAwesomeIcon className="fa-icon" icon="arrow-right" size="sm" /></div> */} </div> diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index a3020f912..ce39c3735 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -15,6 +15,7 @@ import { numberRange } from "../../../Utils"; import { ComputedField } from "../../../fields/ScriptField"; import { listSpec } from "../../../fields/Schema"; import { DocumentType } from "../../documents/DocumentTypes"; +import { InkingStroke } from "../InkingStroke"; export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { dataProvider?: (doc: Doc, replica: string) => { x: number, y: number, zIndex?: number, opacity?: number, highlight?: boolean, z: number, transition?: string } | undefined; @@ -37,7 +38,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF return min + rnd * (max - min); } get displayName() { return "CollectionFreeFormDocumentView(" + this.rootDoc.title + ")"; } // this makes mobx trace() statements more descriptive - get maskCentering() { return this.props.Document.isInkMask ? 2500 : 0; } + get maskCentering() { return this.props.Document.isInkMask ? InkingStroke.MaskDim / 2 : 0; } get transform() { return `scale(${this.props.ContentScaling()}) translate(${this.X - this.maskCentering}px, ${this.Y - this.maskCentering}px) rotate(${this.random(-1, 1) * this.props.jitterRotation}deg)`; } get X() { return this.dataProvider ? this.dataProvider.x : (this.Document.x || 0); } get Y() { return this.dataProvider ? this.dataProvider.y : (this.Document.y || 0); } @@ -73,9 +74,9 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF public static getValues(doc: Doc, time: number) { const timecode = Math.round(time); return ({ - x: Cast(doc["x-indexed"], listSpec("number"), []).reduce((p, x, i) => (i <= timecode && x !== undefined) || p === undefined ? x : p, undefined as any as number), - y: Cast(doc["y-indexed"], listSpec("number"), []).reduce((p, y, i) => (i <= timecode && y !== undefined) || p === undefined ? y : p, undefined as any as number), - opacity: Cast(doc["opacity-indexed"], listSpec("number"), []).reduce((p, o, i) => i <= timecode || p === undefined ? o : p, undefined as any as number), + x: Cast(doc["x-indexed"], listSpec("number"), [NumCast(doc.x)]).reduce((p, x, i) => (i <= timecode && x !== undefined) || p === undefined ? x : p, undefined as any as number), + y: Cast(doc["y-indexed"], listSpec("number"), [NumCast(doc.y)]).reduce((p, y, i) => (i <= timecode && y !== undefined) || p === undefined ? y : p, undefined as any as number), + opacity: Cast(doc["opacity-indexed"], listSpec("number"), [NumCast(doc.opacity, 1)]).reduce((p, o, i) => i <= timecode || p === undefined ? o : p, undefined as any as number), }); } @@ -157,8 +158,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF outline: this.Highlight ? "orange solid 2px" : "", transform: this.transform, transition: this.props.dataTransition ? this.props.dataTransition : this.dataProvider ? this.dataProvider.transition : StrCast(this.layoutDoc.dataTransition), - width: this.props.Document.isInkMask ? 5000 : this.width, - height: this.props.Document.isInkMask ? 5000 : this.height, + width: this.props.Document.isInkMask ? InkingStroke.MaskDim : this.width, + height: this.props.Document.isInkMask ? InkingStroke.MaskDim : this.height, zIndex: this.ZInd, mixBlendMode: StrCast(this.layoutDoc.mixBlendMode) as any, display: this.ZInd === -99 ? "none" : undefined, diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx index b3a9b6fee..57028b0ca 100644 --- a/src/client/views/nodes/ColorBox.tsx +++ b/src/client/views/nodes/ColorBox.tsx @@ -14,7 +14,7 @@ import { ViewBoxBaseComponent } from "../DocComponent"; import { ActiveInkPen, ActiveInkWidth, ActiveInkBezierApprox, SetActiveInkColor, SetActiveInkWidth, SetActiveBezierApprox } from "../InkingStroke"; import "./ColorBox.scss"; import { FieldView, FieldViewProps } from './FieldView'; -import { FormattedTextBox } from "./formattedText/FormattedTextBox"; +import { DocumentType } from "../../documents/DocumentTypes"; type ColorDocument = makeInterface<[typeof documentSchema]>; const ColorDocument = makeInterface(documentSchema); @@ -63,9 +63,15 @@ 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" defaultValue={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); + SelectionManager.SelectedDocuments().filter(i => StrCast(i.rootDoc.type) === DocumentType.INK).map(i => i.rootDoc.strokeWidth = Number(e.target.value)); + }} /> <div> {ActiveInkBezierApprox() ?? 2}</div> - <input type="range" defaultValue={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); + SelectionManager.SelectedDocuments().filter(i => StrCast(i.rootDoc.type) === DocumentType.INK).map(i => i.rootDoc.strokeBezier = e.target.value); + }} /> <br /> <br /> </div> diff --git a/src/client/views/nodes/ContentFittingDocumentView.tsx b/src/client/views/nodes/ContentFittingDocumentView.tsx index ba075886b..6081def5d 100644 --- a/src/client/views/nodes/ContentFittingDocumentView.tsx +++ b/src/client/views/nodes/ContentFittingDocumentView.tsx @@ -2,7 +2,6 @@ import React = require("react"); import { computed } from "mobx"; import { observer } from "mobx-react"; import { Transform } from "nodemailer/lib/xoauth2"; -import "react-table/react-table.css"; import { Doc, HeightSym, Opt, WidthSym } from "../../../fields/Doc"; import { ScriptField } from "../../../fields/ScriptField"; import { Cast, NumCast, StrCast } from "../../../fields/Types"; @@ -10,7 +9,6 @@ import { TraceMobx } from "../../../fields/util"; import { emptyFunction } from "../../../Utils"; import { dropActionType } from "../../util/DragManager"; import { CollectionView } from "../collections/CollectionView"; -import '../DocumentDecorations.scss'; import { DocumentView, DocumentViewProps } from "../nodes/DocumentView"; import "./ContentFittingDocumentView.scss"; diff --git a/src/client/views/nodes/DocHolderBox.tsx b/src/client/views/nodes/DocHolderBox.tsx index 0cf5505cc..0c4242172 100644 --- a/src/client/views/nodes/DocHolderBox.tsx +++ b/src/client/views/nodes/DocHolderBox.tsx @@ -180,7 +180,7 @@ export class DocHolderBox extends ViewBoxAnnotatableComponent<FieldViewProps, Do render() { const containedDoc = Cast(this.dataDoc[this.fieldKey], Doc, null); TraceMobx(); - return <div className="documentBox-container" ref={this._contRef} + return !containedDoc ? (null) : <div className="documentBox-container" ref={this._contRef} onContextMenu={this.specificContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick} style={{ diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index f1438fd54..47dc0a773 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -1,6 +1,6 @@ import { computed } from "mobx"; import { observer } from "mobx-react"; -import { Doc, Opt, Field, AclSym, AclPrivate } from "../../../fields/Doc"; +import { Doc, Opt, Field, AclPrivate } from "../../../fields/Doc"; import { Cast, StrCast, NumCast } from "../../../fields/Types"; import { OmitKeys, Without, emptyPath } from "../../../Utils"; import { DirectoryImportBox } from "../../util/Import & Export/DirectoryImportBox"; @@ -36,7 +36,7 @@ import { WebBox } from "./WebBox"; import { InkingStroke } from "../InkingStroke"; import React = require("react"); import { RecommendationsBox } from "../RecommendationsBox"; -import { TraceMobx } from "../../../fields/util"; +import { TraceMobx, GetEffectiveAcl } from "../../../fields/util"; import { ScriptField } from "../../../fields/ScriptField"; import XRegExp = require("xregexp"); @@ -184,8 +184,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { const bindings = this.CreateBindings(onClick, onInput); // layoutFrame = splits.length > 1 ? splits[0] + splits[1].replace(/{([^{}]|(?R))*}/, replacer4) : ""; // might have been more elegant if javascript supported recursive patterns - - return (this.props.renderDepth > 12 || !layoutFrame || !this.layoutDoc || this.layoutDoc[AclSym] === AclPrivate) ? (null) : + return (this.props.renderDepth > 12 || !layoutFrame || !this.layoutDoc || GetEffectiveAcl(this.layoutDoc) === AclPrivate) ? (null) : <ObserverJsxParser key={42} blacklistedAttrs={[]} @@ -201,7 +200,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { jsx={layoutFrame} showWarnings={true} - onError={(test: any) => { console.log(test); }} + onError={(test: any) => { console.log("DocumentContentsView:" + test); }} />; } }
\ No newline at end of file diff --git a/src/client/views/nodes/DocumentLinksButton.scss b/src/client/views/nodes/DocumentLinksButton.scss index b58603978..97e714cd5 100644 --- a/src/client/views/nodes/DocumentLinksButton.scss +++ b/src/client/views/nodes/DocumentLinksButton.scss @@ -13,7 +13,7 @@ color: black; text-transform: uppercase; letter-spacing: 2px; - font-size: 75%; + font-size: 10px; transition: transform 0.2s; text-align: center; display: flex; @@ -21,17 +21,26 @@ align-items: center; &:hover { - background: deepskyblue; - transform: scale(1.05); - cursor: grab; + // background: deepskyblue; + // transform: scale(1.05); + cursor: pointer; } } + .documentLinksButton { background-color: $link-color; } + .documentLinksButton-endLink { border: red solid 2px; + + &:hover { + background: deepskyblue; + transform: scale(1.05); + cursor: pointer; + } } + .documentLinksButton-startLink { border: red solid 2px; background-color: rgba(255, 192, 203, 0.5); diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index 4c60d03fd..57fa26b80 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -3,7 +3,7 @@ import { observer } from "mobx-react"; import { Doc, DocListCast } from "../../../fields/Doc"; import { emptyFunction, setupMoveUpEvents, returnFalse, Utils } from "../../../Utils"; import { DragManager } from "../../util/DragManager"; -import { UndoManager } from "../../util/UndoManager"; +import { UndoManager, undoBatch } from "../../util/UndoManager"; import './DocumentLinksButton.scss'; import { DocumentView } from "./DocumentView"; import React = require("react"); @@ -19,6 +19,7 @@ import { LinkDescriptionPopup } from "./LinkDescriptionPopup"; import { LinkManager } from "../../util/LinkManager"; import { Hypothesis } from "../../apis/hypothesis/HypothesisApiUtils"; import { Id } from "../../../fields/FieldSymbols"; +import { Tooltip } from "@material-ui/core"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -28,6 +29,7 @@ interface DocumentLinksButtonProps { Offset?: number[]; AlwaysOn?: boolean; InMenu?: boolean; + StartLink?: boolean; } @observer export class DocumentLinksButton extends React.Component<DocumentLinksButtonProps, {}> { @@ -74,67 +76,67 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp }); } - @action + @action @undoBatch onLinkButtonMoved = (e: PointerEvent) => { - if (this._linkButton.current !== null) { - const linkDrag = UndoManager.StartBatch("Drag Link"); - this.props.View && DragManager.StartLinkDrag(this._linkButton.current, this.props.View.props.Document, e.pageX, e.pageY, { - dragComplete: dropEv => { - const linkDoc = dropEv.linkDragData?.linkDocument as Doc; // equivalent to !dropEve.aborted since linkDocument is only assigned on a completed drop - if (this.props.View && linkDoc) { - !linkDoc.linkRelationship && (Doc.GetProto(linkDoc).linkRelationship = "hyperlink"); - - // we want to allow specific views to handle the link creation in their own way (e.g., rich text makes text hyperlinks) - // the dragged view can regiser a linkDropCallback to be notified that the link was made and to update their data structures - // however, the dropped document isn't so accessible. What we do is set the newly created link document on the documentView - // The documentView passes a function prop returning this link doc to its descendants who can react to changes to it. - dropEv.linkDragData?.linkDropCallback?.(dropEv.linkDragData); - runInAction(() => this.props.View._link = linkDoc); - setTimeout(action(() => this.props.View._link = undefined), 0); - } - linkDrag?.end(); - }, - hideSource: false - }); - return true; + if (this.props.InMenu && this.props.StartLink) { + if (this._linkButton.current !== null) { + const linkDrag = UndoManager.StartBatch("Drag Link"); + this.props.View && DragManager.StartLinkDrag(this._linkButton.current, this.props.View.props.Document, e.pageX, e.pageY, { + dragComplete: dropEv => { + const linkDoc = dropEv.linkDragData?.linkDocument as Doc; // equivalent to !dropEve.aborted since linkDocument is only assigned on a completed drop + if (this.props.View && linkDoc) { + !linkDoc.linkRelationship && (Doc.GetProto(linkDoc).linkRelationship = "hyperlink"); + + // we want to allow specific views to handle the link creation in their own way (e.g., rich text makes text hyperlinks) + // the dragged view can regiser a linkDropCallback to be notified that the link was made and to update their data structures + // however, the dropped document isn't so accessible. What we do is set the newly created link document on the documentView + // The documentView passes a function prop returning this link doc to its descendants who can react to changes to it. + dropEv.linkDragData?.linkDropCallback?.(dropEv.linkDragData); + runInAction(() => this.props.View._link = linkDoc); + setTimeout(action(() => this.props.View._link = undefined), 0); + } + linkDrag?.end(); + }, + hideSource: false + }); + return true; + } + return false; } return false; } - + @undoBatch onLinkButtonDown = (e: React.PointerEvent): void => { setupMoveUpEvents(this, e, this.onLinkButtonMoved, emptyFunction, action((e, doubleTap) => { - if (doubleTap && this.props.InMenu) { + if (doubleTap && this.props.InMenu && this.props.StartLink) { //action(() => Doc.BrushDoc(this.props.View.Document)); DocumentLinksButton.StartLink = this.props.View; - } else if (!!!this.props.InMenu) { + } else if (!this.props.InMenu) { DocumentLinksButton.EditLink = this.props.View; DocumentLinksButton.EditLinkLoc = [e.clientX + 10, e.clientY]; } })); } - @action + @action @undoBatch onLinkClick = (e: React.MouseEvent): void => { - if (this.props.InMenu) { + if (this.props.InMenu && this.props.StartLink) { DocumentLinksButton.StartLink = this.props.View; //action(() => Doc.BrushDoc(this.props.View.Document)); - } else if (!!!this.props.InMenu) { + } else if (!this.props.InMenu) { DocumentLinksButton.EditLink = this.props.View; DocumentLinksButton.EditLinkLoc = [e.clientX + 10, e.clientY]; } } - @action + @action @undoBatch completeLink = (e: React.PointerEvent): void => { setupMoveUpEvents(this, e, returnFalse, emptyFunction, action((e, doubleTap) => { - if (doubleTap) { + if (doubleTap && this.props.InMenu && !!!this.props.StartLink) { if (DocumentLinksButton.StartLink === this.props.View) { DocumentLinksButton.StartLink = undefined; DocumentLinksButton.AnnotationId = undefined; - // action((e: React.PointerEvent<HTMLDivElement>) => { - // Doc.UnBrushDoc(this.props.View.Document); - // }); } else { if (DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View) { const sourceDoc = DocumentLinksButton.StartLink.props.Document; @@ -151,16 +153,20 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp } LinkManager.currentLink = linkDoc; + runInAction(() => { - LinkCreatedBox.popupX = e.screenX; - LinkCreatedBox.popupY = e.screenY - 133; - LinkCreatedBox.linkCreated = true; + if (linkDoc) { + LinkCreatedBox.popupX = e.screenX; + LinkCreatedBox.popupY = e.screenY - 133; + LinkCreatedBox.linkCreated = true; - LinkDescriptionPopup.popupX = e.screenX; - LinkDescriptionPopup.popupY = e.screenY - 100; - LinkDescriptionPopup.descriptionPopup = true; + LinkDescriptionPopup.popupX = e.screenX; + LinkDescriptionPopup.popupY = e.screenY - 100; + LinkDescriptionPopup.descriptionPopup = true; + + setTimeout(action(() => { LinkCreatedBox.linkCreated = false; }), 2500); + } - setTimeout(action(() => { LinkCreatedBox.linkCreated = false; }), 2500); }); } } @@ -168,43 +174,48 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp })); } - @action + @action @undoBatch finishLinkClick = (e: React.MouseEvent) => { if (DocumentLinksButton.StartLink === this.props.View) { DocumentLinksButton.StartLink = undefined; DocumentLinksButton.AnnotationId = undefined; window.removeEventListener("message", this.newAnnotation); window.addEventListener("message", this.newAnnotation); - // action((e: React.PointerEvent<HTMLDivElement>) => { - // Doc.UnBrushDoc(this.props.View.Document); - // }); } else { - if (DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View) { - const sourceDoc = DocumentLinksButton.StartLink.props.Document; - const targetDoc = this.props.View.props.Document; - const linkDoc = DocUtils.MakeLink({ doc: sourceDoc }, { doc: targetDoc }, DocumentLinksButton.AnnotationId ? "hypothes.is annotation" : "long drag"); - - // if the link is to a Hypothes.is annotation - if (DocumentLinksButton.AnnotationId && DocumentLinksButton.AnnotationUri) { - const sourceUrl = DocumentLinksButton.AnnotationUri; // the URL of the annotation's source web page - Doc.GetProto(linkDoc as Doc).linksToAnnotation = true; - Doc.GetProto(linkDoc as Doc).annotationId = DocumentLinksButton.AnnotationId; - Doc.GetProto(linkDoc as Doc).annotationUrl = Hypothesis.makeAnnotationUrl(DocumentLinksButton.AnnotationId, sourceUrl); // redirect web doc to this URL when following link - Hypothesis.makeLink(StrCast(targetDoc.title), Utils.prepend("/doc/" + targetDoc[Id]), DocumentLinksButton.AnnotationId); // update and link placeholder annotation - } + if (this.props.InMenu && !!!this.props.StartLink) { + if (DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View) { + const linkDoc = DocUtils.MakeLink({ doc: DocumentLinksButton.StartLink.props.Document }, { doc: this.props.View.props.Document }, DocumentLinksButton.AnnotationId ? "hypothes.is annotation" : "long drag"); + // this notifies any of the subviews that a document is made so that they can make finer-grained hyperlinks (). see note above in onLInkButtonMoved + runInAction(() => DocumentLinksButton.StartLink!._link = this.props.View._link = linkDoc); + setTimeout(action(() => DocumentLinksButton.StartLink!._link = this.props.View._link = undefined), 0); + LinkManager.currentLink = linkDoc; - LinkManager.currentLink = linkDoc; - runInAction(() => { - LinkCreatedBox.popupX = e.screenX; - LinkCreatedBox.popupY = e.screenY - 133; - LinkCreatedBox.linkCreated = true; + // if the link is to a Hypothes.is annotation + if (DocumentLinksButton.AnnotationId && DocumentLinksButton.AnnotationUri) { + const sourceUrl = DocumentLinksButton.AnnotationUri; // the URL of the annotation's source web page + const targetDoc = this.props.View.props.Document; + Doc.GetProto(linkDoc as Doc).linksToAnnotation = true; + Doc.GetProto(linkDoc as Doc).annotationId = DocumentLinksButton.AnnotationId; + Doc.GetProto(linkDoc as Doc).annotationUrl = Hypothesis.makeAnnotationUrl(DocumentLinksButton.AnnotationId, sourceUrl); // redirect web doc to this URL when following link + Hypothesis.makeLink(StrCast(targetDoc.title), Utils.prepend("/doc/" + targetDoc[Id]), DocumentLinksButton.AnnotationId); // update and link placeholder annotation + } - LinkDescriptionPopup.popupX = e.screenX; - LinkDescriptionPopup.popupY = e.screenY - 100; - LinkDescriptionPopup.descriptionPopup = true; + runInAction(() => { + if (linkDoc) { + LinkCreatedBox.popupX = e.screenX; + LinkCreatedBox.popupY = e.screenY - 133; + LinkCreatedBox.linkCreated = true; - setTimeout(action(() => { LinkCreatedBox.linkCreated = false; }), 2500); - }); + if (LinkDescriptionPopup.showDescriptions === "ON" || !LinkDescriptionPopup.showDescriptions) { + LinkDescriptionPopup.popupX = e.screenX; + LinkDescriptionPopup.popupY = e.screenY - 100; + LinkDescriptionPopup.descriptionPopup = true; + } + + setTimeout(action(() => { LinkCreatedBox.linkCreated = false; }), 2500); + } + }); + } } } } @@ -217,32 +228,62 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp get linkButton() { const links = DocListCast(this.props.View.props.Document.links); - const title = this.props.InMenu ? "Drag or tap to create links" : "Tap to view links"; - - return (!links.length || links[0].hidden) && !this.props.AlwaysOn ? (null) : - <div title={title} ref={this._linkButton} style={{ minWidth: 20, minHeight: 20, position: "absolute", left: this.props.Offset?.[0] }}> - <div className={"documentLinksButton"} style={{ - backgroundColor: DocumentLinksButton.StartLink ? "transparent" : this.props.InMenu ? "black" : "", - color: this.props.InMenu ? "white" : "black", - width: this.props.InMenu ? "20px" : "30px", height: this.props.InMenu ? "20px" : "30px", fontWeight: "bold" - }} - onPointerDown={this.onLinkButtonDown} onClick={this.onLinkClick} - // onPointerLeave={action(() => LinkDocPreview.LinkInfo = undefined)} - // onPointerEnter={action(e => links.length && (LinkDocPreview.LinkInfo = { - // addDocTab: this.props.View.props.addDocTab, - // linkSrc: this.props.View.props.Document, - // linkDoc: links[0], - // Location: [e.clientX, e.clientY + 20] - // }))} - > - {links.length && !!!this.props.InMenu ? links.length : <FontAwesomeIcon className="documentdecorations-icon" icon="link" size="sm" />} - </div> - {DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View ? <div className={"documentLinksButton-endLink"} - style={{ width: this.props.InMenu ? "20px" : "30px", height: this.props.InMenu ? "20px" : "30px" }} - onPointerDown={this.completeLink} onClick={e => this.finishLinkClick(e)} /> : (null)} - {DocumentLinksButton.StartLink === this.props.View ? <div className={"documentLinksButton-startLink"} - style={{ width: this.props.InMenu ? "20px" : "30px", height: this.props.InMenu ? "20px" : "30px" }} /> : (null)} - </div>; + const menuTitle = this.props.StartLink ? "Drag or tap to start link" : "Tap to complete link"; + const buttonTitle = "Tap to view links"; + const title = this.props.InMenu ? menuTitle : buttonTitle; + + + const startLink = <img + style={{ width: "11px", height: "11px" }} + id={"startLink-icon"} + src={`/assets/${"startLink.png"}`} />; + + const endLink = <img + style={{ width: "14px", height: "9px" }} + id={"endLink-icon"} + src={`/assets/${"endLink.png"}`} />; + + const link = <img + style={{ width: "22px", height: "16px" }} + id={"link-icon"} + src={`/assets/${"link.png"}`} />; + + const linkButton = <div ref={this._linkButton} style={{ minWidth: 20, minHeight: 20, position: "absolute", left: this.props.Offset?.[0] }}> + <div className={"documentLinksButton"} style={{ + backgroundColor: this.props.InMenu ? "black" : "", + color: this.props.InMenu ? "white" : "black", + width: this.props.InMenu ? "20px" : "30px", height: this.props.InMenu ? "20px" : "30px", fontWeight: "bold" + }} + onPointerDown={this.onLinkButtonDown} onClick={this.onLinkClick} + // onPointerLeave={action(() => LinkDocPreview.LinkInfo = undefined)} + // onPointerEnter={action(e => links.length && (LinkDocPreview.LinkInfo = { + // addDocTab: this.props.View.props.addDocTab, + // linkSrc: this.props.View.props.Document, + // linkDoc: links[0], + // Location: [e.clientX, e.clientY + 20] + // }))} + > + + {this.props.InMenu ? this.props.StartLink ? <FontAwesomeIcon className="documentdecorations-icon" icon="link" size="sm" /> : + <FontAwesomeIcon className="documentdecorations-icon" icon="hand-paper" size="sm" /> : links.length} + + </div> + {DocumentLinksButton.StartLink && this.props.InMenu && !!!this.props.StartLink && DocumentLinksButton.StartLink !== this.props.View ? <div className={"documentLinksButton-endLink"} + style={{ width: this.props.InMenu ? "20px" : "30px", height: this.props.InMenu ? "20px" : "30px" }} + onPointerDown={this.completeLink} onClick={e => this.finishLinkClick(e)} /> : (null)} + {DocumentLinksButton.StartLink === this.props.View && this.props.InMenu && this.props.StartLink ? <div className={"documentLinksButton-startLink"} + style={{ width: this.props.InMenu ? "20px" : "30px", height: this.props.InMenu ? "20px" : "30px" }} /> : (null)} + </div>; + + return (!links.length) && !this.props.AlwaysOn ? (null) : + this.props.InMenu ? + <Tooltip title={<><div className="dash-tooltip">{title}</div></>}> + {linkButton} + </Tooltip> : !!!DocumentLinksButton.EditLink ? + <Tooltip title={<><div className="dash-tooltip">{title}</div></>}> + {linkButton} + </Tooltip> : + linkButton; } render() { return this.linkButton; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 2f19eb266..555e58bb0 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -3,16 +3,15 @@ import * as fa from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; -import * as rp from "request-promise"; -import { Doc, DocListCast, HeightSym, Opt, WidthSym, DataSym, AclSym, AclReadonly, AclPrivate } from "../../../fields/Doc"; +import { Doc, DocListCast, HeightSym, Opt, WidthSym, DataSym, AclPrivate, AclEdit } from "../../../fields/Doc"; import { Document } from '../../../fields/documentSchemas'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { listSpec } from "../../../fields/Schema"; import { SchemaHeaderField } from '../../../fields/SchemaHeaderField'; import { ScriptField } from '../../../fields/ScriptField'; -import { BoolCast, Cast, NumCast, StrCast } from "../../../fields/Types"; -import { TraceMobx } from '../../../fields/util'; +import { BoolCast, Cast, NumCast, StrCast, ScriptCast } from "../../../fields/Types"; +import { TraceMobx, GetEffectiveAcl } from '../../../fields/util'; import { GestureUtils } from '../../../pen-gestures/GestureUtils'; import { emptyFunction, OmitKeys, returnOne, returnTransparent, Utils, emptyPath } from "../../../Utils"; import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; @@ -26,7 +25,7 @@ import { InteractionUtils } from '../../util/InteractionUtils'; import { Scripting } from '../../util/Scripting'; import { SearchUtil } from '../../util/SearchUtil'; import { SelectionManager } from "../../util/SelectionManager"; -import SharingManager from '../../util/SharingManager'; +import SharingManager, { SharingPermissions } from '../../util/SharingManager'; import { Transform } from "../../util/Transform"; import { undoBatch, UndoManager } from "../../util/UndoManager"; import { CollectionView, CollectionViewType } from '../collections/CollectionView'; @@ -69,10 +68,10 @@ export interface DocumentViewProps { ignoreAutoHeight?: boolean; contextMenuItems?: () => { script: ScriptField, label: string }[]; rootSelected: (outsideReaction?: boolean) => boolean; // whether the root of a template has been selected - onClick?: ScriptField; - onDoubleClick?: ScriptField; - onPointerDown?: ScriptField; - onPointerUp?: ScriptField; + onClick?: () => ScriptField; + onDoubleClick?: () => ScriptField; + onPointerDown?: () => ScriptField; + onPointerUp?: () => ScriptField; treeViewDoc?: Doc; dropAction?: dropActionType; dragDivName?: string; @@ -128,12 +127,14 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @computed get freezeDimensions() { return this.props.FreezeDimensions; } @computed get nativeWidth() { return NumCast(this.layoutDoc._nativeWidth, this.props.NativeWidth() || (this.freezeDimensions ? this.layoutDoc[WidthSym]() : 0)); } @computed get nativeHeight() { return NumCast(this.layoutDoc._nativeHeight, this.props.NativeHeight() || (this.freezeDimensions ? this.layoutDoc[HeightSym]() : 0)); } - @computed get onClickHandler() { return this.props.onClick || Cast(this.Document.onClick, ScriptField, Cast(this.layoutDoc.onClick, ScriptField, null)); } - @computed get onDoubleClickHandler() { return this.props.onDoubleClick || Cast(this.layoutDoc.onDoubleClick, ScriptField, null) || this.Document.onDoubleClick; } - @computed get onPointerDownHandler() { return this.props.onPointerDown ? this.props.onPointerDown : this.Document.onPointerDown; } - @computed get onPointerUpHandler() { return this.props.onPointerUp ? this.props.onPointerUp : this.Document.onPointerUp; } + @computed get onClickHandler() { return this.props.onClick?.() ?? Cast(this.Document.onClick, ScriptField, Cast(this.layoutDoc.onClick, ScriptField, null)); } + @computed get onDoubleClickHandler() { return this.props.onDoubleClick?.() ?? (Cast(this.layoutDoc.onDoubleClick, ScriptField, null) ?? this.Document.onDoubleClick); } + @computed get onPointerDownHandler() { return this.props.onPointerDown?.() ?? ScriptCast(this.Document.onPointerDown); } + @computed get onPointerUpHandler() { return this.props.onPointerUp?.() ?? ScriptCast(this.Document.onPointerUp); } NativeWidth = () => this.nativeWidth; NativeHeight = () => this.nativeHeight; + onClickFunc = () => this.onClickHandler; + onDoubleClickFunc = () => this.onDoubleClickHandler; private _firstX: number = -1; private _firstY: number = -1; @@ -588,16 +589,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @undoBatch toggleLinkButtonBehavior = (): void => { + this.Document.ignoreClick = false; if (this.Document.isLinkButton || this.onClickHandler || this.Document.ignoreClick) { this.Document.isLinkButton = false; - const first = DocListCast(this.Document.links).find(d => d instanceof Doc); - first && (first.hidden = false); - this.Document.ignoreClick = false; this.Document.onClick = this.layoutDoc.onClick = undefined; } else { this.Document.isLinkButton = true; - const first = DocListCast(this.Document.links).find(d => d instanceof Doc); - first && (first.hidden = true); this.Document.followLinkZoom = false; this.Document.followLinkLocation = undefined; } @@ -605,14 +602,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @undoBatch toggleFollowInPlace = (): void => { + this.Document.ignoreClick = false; + this.Document.isLinkButton = !this.Document.isLinkButton; if (this.Document.isLinkButton) { - this.Document.isLinkButton = false; - const first = DocListCast(this.Document.links).find(d => d instanceof Doc); - first && (first.hidden = false); - } else { - this.Document.isLinkButton = true; - const first = DocListCast(this.Document.links).find(d => d instanceof Doc); - first && (first.hidden = true); this.Document.followLinkZoom = true; this.Document.followLinkLocation = "inPlace"; } @@ -620,15 +612,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @undoBatch toggleFollowOnRight = (): void => { + this.Document.ignoreClick = false; + this.Document.isLinkButton = !this.Document.isLinkButton; if (this.Document.isLinkButton) { - this.Document.isLinkButton = false; - const first = DocListCast(this.Document.links).find(d => d instanceof Doc); - first && (first.hidden = false); - } else { - this.Document.isLinkButton = true; this.Document.followLinkZoom = false; - const first = DocListCast(this.Document.links).find(d => d instanceof Doc); - first && (first.hidden = true); this.Document.followLinkLocation = "onRight"; } } @@ -640,49 +627,35 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu alert("linking to document tabs not yet supported. Drop link on document content."); return; } + const makeLink = action((linkDoc: Doc) => { + LinkManager.currentLink = linkDoc; + + LinkCreatedBox.popupX = de.x; + LinkCreatedBox.popupY = de.y - 33; + LinkCreatedBox.linkCreated = true; + + LinkDescriptionPopup.popupX = de.x; + LinkDescriptionPopup.popupY = de.y; + LinkDescriptionPopup.descriptionPopup = true; + + setTimeout(action(() => LinkCreatedBox.linkCreated = false), 2500); + }); if (de.complete.annoDragData) { /// this whole section for handling PDF annotations looks weird. Need to rethink this to make it cleaner e.stopPropagation(); de.complete.annoDragData.linkedToDoc = true; const linkDoc = DocUtils.MakeLink({ doc: de.complete.annoDragData.annotationDocument }, { doc: this.props.Document }, "link"); - LinkManager.currentLink = linkDoc; - - runInAction(() => { - LinkCreatedBox.popupX = de.x; - LinkCreatedBox.popupY = de.y - 33; - LinkCreatedBox.linkCreated = true; - - LinkDescriptionPopup.popupX = de.x; - LinkDescriptionPopup.popupY = de.y; - LinkDescriptionPopup.descriptionPopup = true; - - setTimeout(action(() => { LinkCreatedBox.linkCreated = false; }), 2500); - }); + linkDoc && makeLink(linkDoc); } if (de.complete.linkDragData) { e.stopPropagation(); - // const docs = await SearchUtil.Search(`data_l:"${destDoc[Id]}"`, true); - // const views = docs.map(d => DocumentManager.Instance.getDocumentView(d)).filter(d => d).map(d => d as DocumentView); - if (de.complete.linkDragData.linkSourceDocument !== this.props.Document) { const linkDoc = DocUtils.MakeLink({ doc: de.complete.linkDragData.linkSourceDocument }, { doc: this.props.Document }, `link`); - LinkManager.currentLink = linkDoc; - de.complete.linkDragData.linkSourceDocument !== this.props.Document && (de.complete.linkDragData.linkDocument = linkDoc); // TODODO this is where in text links get passed - runInAction(() => { - LinkCreatedBox.popupX = de.x; - LinkCreatedBox.popupY = de.y - 33; - LinkCreatedBox.linkCreated = true; - - LinkDescriptionPopup.popupX = de.x; - LinkDescriptionPopup.popupY = de.y; - LinkDescriptionPopup.descriptionPopup = true; - - setTimeout(action(() => { LinkCreatedBox.linkCreated = false; }), 2500); - }); + linkDoc && makeLink(linkDoc); } } @@ -726,7 +699,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @undoBatch @action - setAcl = (acl: "readOnly" | "addOnly" | "ownerOnly" | "write") => { + setAcl = (acl: SharingPermissions) => { this.dataDoc.ACL = this.props.Document.ACL = acl; DocListCast(this.dataDoc[Doc.LayoutFieldKey(this.dataDoc)]).map(d => { if (d.author === Doc.CurrentUserEmail) d.ACL = acl; @@ -734,9 +707,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu if (data && data.author === Doc.CurrentUserEmail) data.ACL = acl; }); } + @undoBatch @action - testAcl = (acl: "readOnly" | "addOnly" | "ownerOnly" | "write") => { + testAcl = (acl: SharingPermissions) => { this.dataDoc.author = this.props.Document.author = "ADMIN"; this.dataDoc.ACL = this.props.Document.ACL = acl; DocListCast(this.dataDoc[Doc.LayoutFieldKey(this.dataDoc)]).map(d => { @@ -780,13 +754,18 @@ 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" })); + const templateDoc = Cast(this.props.Document[StrCast(this.props.Document.layoutKey)], Doc, null); + const appearance = cm.findByDescription("Appearance..."); + const appearanceItems: ContextMenuProps[] = appearance && "subitems" in appearance ? appearance.subitems : []; + templateDoc && appearanceItems.push({ description: "Open Template ", event: () => this.props.addDocTab(templateDoc, "onRight"), icon: "eye" }); + !appearance && cm.addItem({ description: "Appearance...", subitems: appearanceItems, icon: "compass" }); + const 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); - templateDoc && optionItems.push({ description: "Open Template ", event: () => this.props.addDocTab(templateDoc, "onRight"), icon: "eye" }); - optionItems.push({ description: "Toggle Show Each Link Dot", event: () => this.layoutDoc.showLinks = !this.layoutDoc.showLinks, icon: "eye" }); + optionItems.push({ description: this.Document.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.Document.lockedPosition) ? "unlock" : "lock" }); !options && cm.addItem({ description: "Options...", subitems: optionItems, icon: "compass" }); + const existingOnClick = cm.findByDescription("OnClick..."); const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; onClicks.push({ description: "Enter Portal", event: this.makeIntoPortal, icon: "window-restore" }); @@ -796,18 +775,28 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu onClicks.push({ description: this.Document.isLinkButton ? "Remove Follow Behavior" : "Follow Link on Right", event: this.toggleFollowOnRight, icon: "concierge-bell" }); onClicks.push({ description: this.Document.isLinkButton || this.onClickHandler ? "Remove Click Behavior" : "Follow Link", event: this.toggleLinkButtonBehavior, icon: "concierge-bell" }); 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" }); + !existingOnClick && cm.addItem({ description: "OnClick...", noexpand: true, subitems: onClicks, icon: "hand-point-right" }); const funcs: ContextMenuProps[] = []; if (this.layoutDoc.onDragStart) { funcs.push({ description: "Drag an Alias", icon: "edit", event: () => this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getAlias(this.dragFactory)')) }); funcs.push({ description: "Drag a Copy", icon: "edit", event: () => this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)')) }); funcs.push({ description: "Drag Document", icon: "edit", event: () => this.layoutDoc.onDragStart = undefined }); - cm.addItem({ description: "OnDrag...", subitems: funcs, icon: "asterisk" }); + cm.addItem({ description: "OnDrag...", noexpand: true, subitems: funcs, icon: "asterisk" }); } const more = cm.findByDescription("More..."); const moreItems = more && "subitems" in more ? more.subitems : []; + moreItems.push({ + description: "Download document", icon: "download", event: async () => { + Doc.Zip(this.props.Document); + // 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(); + } + }); 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" }); @@ -817,42 +806,30 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu 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 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: "Copy ID", event: () => Utils.CopyText(Utils.prepend("/doc/" + this.props.Document[Id])), icon: "fingerprint" }); } - 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" }); + GetEffectiveAcl(this.props.Document) === AclEdit && moreItems.push({ description: "Delete", event: this.deleteClicked, icon: "trash" }); 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...")!); const help = cm.findByDescription("Help..."); const helpItems: ContextMenuProps[] = help && "subitems" in help ? help.subitems : []; - helpItems.push({ description: "Text Shortcuts Ctrl+/", event: () => this.props.addDocTab(Docs.Create.PdfDocument("http://localhost:1050/assets/cheat-sheet.pdf", { _width: 300, _height: 300 }), "onRight"), icon: "keyboard" }); - helpItems.push({ description: "Show Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "onRight"), icon: "layer-group" }); - 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: "Make Editable", event: () => this.setAcl("write"), 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" }); + //!Doc.UserDoc().novice && helpItems.push({ description: "Text Shortcuts Ctrl+/", event: () => this.props.addDocTab(Docs.Create.PdfDocument(Utils.prepend("/assets/cheat-sheet.pdf"), { _width: 300, _height: 300 }), "onRight"), icon: "keyboard" }); + !Doc.UserDoc().novice && helpItems.push({ description: "Show Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "onRight"), icon: "layer-group" }); + cm.addItem({ description: "Help...", noexpand: true, 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(SharingPermissions.Add), icon: "concierge-bell" }); + // aclItems.push({ description: "Make Read Only", event: () => this.setAcl(SharingPermissions.View), icon: "concierge-bell" }); + // aclItems.push({ description: "Make Private", event: () => this.setAcl(SharingPermissions.None), icon: "concierge-bell" }); + // aclItems.push({ description: "Make Editable", event: () => this.setAcl(SharingPermissions.Edit), icon: "concierge-bell" }); + // aclItems.push({ description: "Test Private", event: () => this.testAcl(SharingPermissions.None), icon: "concierge-bell" }); + // aclItems.push({ description: "Test Readonly", event: () => this.testAcl(SharingPermissions.View), icon: "concierge-bell" }); + // !existingAcls && cm.addItem({ description: "Privacy...", subitems: aclItems, icon: "question" }); + + // cm.addItem({ description: `${getPlaygroundMode() ? "Disable" : "Enable"} playground mode`, event: togglePlaygroundMode, icon: "concierge-bell" }); // const recommender_subitems: ContextMenuProps[] = []; @@ -934,7 +911,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu if (!ClientRecommender.Instance) new ClientRecommender({ title: "Client Recommender" }); const documents: Doc[] = []; const allDocs = await SearchUtil.GetAllDocs(); - // allDocs.forEach(doc => console.log(doc.title)); // clears internal representation of documents as vectors ClientRecommender.Instance.reset_docs(); //ClientRecommender.Instance.arxivrequest("electrons"); @@ -1083,10 +1059,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu ChromeHeight={this.chromeHeight} isSelected={this.isSelected} select={this.select} - onClick={this.onClickHandler} + onClick={this.onClickFunc} layoutKey={this.finalLayoutKey} /> - {this.layoutDoc.showLinks ? this.anchors : (null)} - {this.props.forcedBackgroundColor?.(this.Document) === "transparent" || this.props.dontRegisterView ? (null) : <DocumentLinksButton View={this} Offset={[-15, 0]} />} + {this.layoutDoc.hideAllLinks ? (null) : this.allAnchors} + {/* {this.allAnchors} */} + {this.props.forcedBackgroundColor?.(this.Document) === "transparent" || this.layoutDoc.hideLinkButton || this.props.dontRegisterView ? (null) : <DocumentLinksButton View={this} Offset={[-15, 0]} />} </div> ); } @@ -1108,12 +1085,14 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu hideLinkAnchor = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg: boolean, doc) => flg && (doc.hidden = true), true) anchorPanelWidth = () => this.props.PanelWidth() || 1; anchorPanelHeight = () => this.props.PanelHeight() || 1; - @computed get anchors() { + + @computed get allAnchors() { TraceMobx(); + if (this.props.LayoutTemplateString?.includes("LinkAnchorBox")) return null; return (this.props.treeViewDoc && this.props.LayoutTemplateString) || // render nothing for: tree view anchor dots this.layoutDoc.presBox || // presentationbox nodes this.props.dontRegisterView ? (null) : // view that are not registered - DocUtils.FilterDocs(DocListCast(this.Document.links), this.props.docFilters(), []).filter(d => !d.hidden && this.isNonTemporalLink).map((d, i) => + DocUtils.FilterDocs(LinkManager.Instance.getAllDirectLinks(this.Document), this.props.docFilters(), []).filter(d => !d.hidden && this.isNonTemporalLink).map((d, i) => <DocumentView {...this.props} key={i + 1} Document={d} ContainingCollectionView={this.props.ContainingCollectionView} @@ -1131,10 +1110,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } @computed get innards() { TraceMobx(); - if (this.props.treeViewDoc && !this.props.LayoutTemplateString) { // this happens when the document is a tree view label (but not an anchor dot) + if (this.props.treeViewDoc && !this.props.LayoutTemplateString?.includes("LinkAnchorBox")) { // this happens when the document is a tree view label (but not an anchor dot) return <div className="documentView-treeView" style={{ maxWidth: this.props.PanelWidth() || undefined }}> {StrCast(this.props.Document.title)} - {this.anchors} + {this.allAnchors} </div>; } @@ -1151,7 +1130,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu ChromeHeight={this.chromeHeight} isSelected={this.isSelected} select={this.select} - onClick={this.onClickHandler} + onClick={this.onClickFunc} layoutKey={this.finalLayoutKey} /> </div>); const titleView = (!showTitle ? (null) : @@ -1208,7 +1187,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu render() { if (!(this.props.Document instanceof Doc)) return (null); - if (this.props.Document[AclSym] && this.props.Document[AclSym] === AclPrivate) return (null); + if (GetEffectiveAcl(this.props.Document) === AclPrivate) return (null); if (this.props.Document.hidden) return (null); const backgroundColor = Doc.UserDoc().renderStyle === "comic" ? undefined : this.props.forcedBackgroundColor?.(this.Document) || StrCast(this.layoutDoc._backgroundColor) || StrCast(this.layoutDoc.backgroundColor) || StrCast(this.Document.backgroundColor) || this.props.backgroundColor?.(this.Document); const opacity = Cast(this.layoutDoc._opacity, "number", Cast(this.layoutDoc.opacity, "number", Cast(this.Document.opacity, "number", null))); @@ -1257,7 +1236,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu background: finalColor, opacity: finalOpacity, fontFamily: StrCast(this.Document._fontFamily, "inherit"), - fontSize: Cast(this.Document._fontSize, "number", null) + fontSize: Cast(this.Document._fontSize, "string", null) }}> {this.onClickHandler && this.props.ContainingCollectionView?.props.Document._viewType === CollectionViewType.Time ? <> {this.innards} diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index c57738361..48e1f6ce3 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -25,7 +25,7 @@ export interface FieldViewProps { Document: Doc; DataDoc?: Doc; LibraryPath: Doc[]; - onClick?: ScriptField; + onClick?: () => ScriptField; dropAction: dropActionType; backgroundHalo?: () => boolean; docFilters: () => string[]; diff --git a/src/client/views/nodes/FontIconBox.scss b/src/client/views/nodes/FontIconBox.scss index 68b00a5be..5b85d8b0b 100644 --- a/src/client/views/nodes/FontIconBox.scss +++ b/src/client/views/nodes/FontIconBox.scss @@ -11,13 +11,15 @@ .fontIconBox-label { background: gray; color:white; - margin-left: -10px; border-radius: 8px; width:100%; position: absolute; text-align: center; font-size: 8px; margin-top:4px; + letter-spacing: normal; + left: 0; + overflow: hidden; } svg { diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx index 5e8dd2497..ab34e13b0 100644 --- a/src/client/views/nodes/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox.tsx @@ -11,6 +11,7 @@ import { runInAction, observable, reaction, IReactionDisposer } from 'mobx'; import { Doc } from '../../../fields/Doc'; import { ContextMenu } from '../ContextMenu'; import { ScriptField } from '../../../fields/ScriptField'; +import { Tooltip } from '@material-ui/core'; const FontIconSchema = createSchema({ icon: "string" }); @@ -60,14 +61,18 @@ export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>( render() { const referenceDoc = (this.layoutDoc.dragFactory instanceof Doc ? this.layoutDoc.dragFactory : this.layoutDoc); const refLayout = Doc.Layout(referenceDoc); - return <button className="fontIconBox-outerDiv" title={StrCast(this.layoutDoc.title)} ref={this._ref} onContextMenu={this.specificContextMenu} + const button = <button className="fontIconBox-outerDiv" ref={this._ref} onContextMenu={this.specificContextMenu} style={{ padding: Cast(this.layoutDoc._xPadding, "number", null), background: StrCast(refLayout._backgroundColor, StrCast(refLayout.backgroundColor)), boxShadow: this.layoutDoc.ischecked ? `4px 4px 12px black` : undefined }}> <FontAwesomeIcon className="fontIconBox-icon" icon={this.dataDoc.icon as any} color={StrCast(this.layoutDoc.color, this._foregroundColor)} size="sm" /> - {!this.rootDoc.label ? (null) : <div className="fontIconBox-label"> {StrCast(this.rootDoc.label).substring(0, 5)} </div>} + {!this.rootDoc.title ? (null) : <div className="fontIconBox-label" style={{ width: this.rootDoc.label ? "max-content" : undefined }}> {StrCast(this.rootDoc.label, StrCast(this.rootDoc.title).substring(0, 6))} </div>} </button>; + return !this.layoutDoc.toolTip ? button : + <Tooltip title={<div className="dash-tooltip">{StrCast(this.layoutDoc.toolTip)}</div>}> + {button} + </Tooltip>; } }
\ No newline at end of file diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index d16aa528c..5f689624c 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -157,29 +157,32 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD const field = Cast(this.dataDoc[this.fieldKey], ImageField); if (field) { const funcs: ContextMenuProps[] = []; - funcs.push({ description: "Rotate", event: this.rotate, icon: "expand-arrows-alt" }); - funcs.push({ description: "Export to Google Photos", event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: "caret-square-right" }); - funcs.push({ description: "Copy path", event: () => Utils.CopyText(field.url.href), icon: "expand-arrows-alt" }); - // funcs.push({ - // description: "Reset Native Dimensions", event: action(async () => { - // const curNW = NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"]); - // const curNH = NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"]); - // if (this.props.PanelWidth() / this.props.PanelHeight() > curNW / curNH) { - // this.dataDoc[this.fieldKey + "-nativeWidth"] = this.props.PanelHeight() * curNW / curNH; - // this.dataDoc[this.fieldKey + "-nativeHeight"] = this.props.PanelHeight(); - // } else { - // this.dataDoc[this.fieldKey + "-nativeWidth"] = this.props.PanelWidth(); - // this.dataDoc[this.fieldKey + "-nativeHeight"] = this.props.PanelWidth() * curNH / curNW; - // } - // }), icon: "expand-arrows-alt" - // }); - - const existingAnalyze = ContextMenu.Instance?.findByDescription("Analyzers..."); - const modes: ContextMenuProps[] = existingAnalyze && "subitems" in existingAnalyze ? existingAnalyze.subitems : []; - modes.push({ description: "Generate Tags", event: this.generateMetadata, icon: "tag" }); - modes.push({ description: "Find Faces", event: this.extractFaces, icon: "camera" }); - //modes.push({ description: "Recommend", event: this.extractText, icon: "brain" }); - !existingAnalyze && ContextMenu.Instance?.addItem({ description: "Analyzers...", subitems: modes, icon: "hand-point-right" }); + funcs.push({ description: "Rotate Clockwise 90", event: this.rotate, icon: "expand-arrows-alt" }); + funcs.push({ description: "Make Background", event: () => this.layoutDoc.isBackground = true, icon: "expand-arrows-alt" }); + if (!Doc.UserDoc().noviceMode) { + funcs.push({ description: "Export to Google Photos", event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: "caret-square-right" }); + funcs.push({ description: "Copy path", event: () => Utils.CopyText(field.url.href), icon: "expand-arrows-alt" }); + // funcs.push({ + // description: "Reset Native Dimensions", event: action(async () => { + // const curNW = NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"]); + // const curNH = NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"]); + // if (this.props.PanelWidth() / this.props.PanelHeight() > curNW / curNH) { + // this.dataDoc[this.fieldKey + "-nativeWidth"] = this.props.PanelHeight() * curNW / curNH; + // this.dataDoc[this.fieldKey + "-nativeHeight"] = this.props.PanelHeight(); + // } else { + // this.dataDoc[this.fieldKey + "-nativeWidth"] = this.props.PanelWidth(); + // this.dataDoc[this.fieldKey + "-nativeHeight"] = this.props.PanelWidth() * curNH / curNW; + // } + // }), icon: "expand-arrows-alt" + // }); + + const existingAnalyze = ContextMenu.Instance?.findByDescription("Analyzers..."); + const modes: ContextMenuProps[] = existingAnalyze && "subitems" in existingAnalyze ? existingAnalyze.subitems : []; + modes.push({ description: "Generate Tags", event: this.generateMetadata, icon: "tag" }); + modes.push({ description: "Find Faces", event: this.extractFaces, icon: "camera" }); + //modes.push({ description: "Recommend", event: this.extractText, icon: "brain" }); + !existingAnalyze && ContextMenu.Instance?.addItem({ description: "Analyzers...", subitems: modes, icon: "hand-point-right" }); + } ContextMenu.Instance?.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); } @@ -313,7 +316,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD considerGooglePhotosLink = () => { const remoteUrl = this.dataDoc.googlePhotosUrl; - return !remoteUrl ? (null) : (<img + return !remoteUrl ? (null) : (<img draggable={false} style={{ transform: `scale(${this.props.ContentScaling()})`, transformOrigin: "bottom right" }} id={"google-photos"} src={"/assets/google_photos.png"} @@ -338,7 +341,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD } return ( <img - id={"upload-icon"} + id={"upload-icon"} draggable={false} style={{ transform: `scale(${1 / this.props.ContentScaling()})`, transformOrigin: "bottom right" }} src={`/assets/${this.uploadIcon}`} onClick={async () => { @@ -413,7 +416,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD <div className="imageBox-fader" > <img key={this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys src={srcpath} - style={{ transform, transformOrigin }} + style={{ transform, transformOrigin }} draggable={false} width={nativeWidth} ref={this._imgRef} onError={this.onError} /> @@ -421,7 +424,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD <img className="imageBox-fadeaway" key={"fadeaway" + this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys src={fadepath} - style={{ transform, transformOrigin }} + style={{ transform, transformOrigin }} draggable={false} width={nativeWidth} ref={this._imgRef} onError={this.onError} /></div>} diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx index 360ead18e..0dfbdc5cf 100644 --- a/src/client/views/nodes/LabelBox.tsx +++ b/src/client/views/nodes/LabelBox.tsx @@ -41,7 +41,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps, LabelDocument }, icon: "trash" }); - ContextMenu.Instance.addItem({ description: "OnClick...", subitems: funcs, icon: "asterisk" }); + ContextMenu.Instance.addItem({ description: "OnClick...", noexpand: true, subitems: funcs, icon: "asterisk" }); } @undoBatch @@ -67,7 +67,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps, LabelDocument <div className="labelBox-mainButton" style={{ background: StrCast(this.layoutDoc.backgroundColor), color: StrCast(this.layoutDoc.color, "inherit"), - fontSize: NumCast(this.layoutDoc._fontSize) || "inherit", + fontSize: StrCast(this.layoutDoc._fontSize) || "inherit", fontFamily: StrCast(this.layoutDoc._fontFamily) || "inherit", letterSpacing: StrCast(this.layoutDoc.letterSpacing), textTransform: StrCast(this.layoutDoc.textTransform) as any, diff --git a/src/client/views/nodes/LinkDescriptionPopup.scss b/src/client/views/nodes/LinkDescriptionPopup.scss index 54002fd1b..d92823ccc 100644 --- a/src/client/views/nodes/LinkDescriptionPopup.scss +++ b/src/client/views/nodes/LinkDescriptionPopup.scss @@ -48,6 +48,10 @@ position: relative; margin-right: 4px; justify-content: center; + + &:hover{ + cursor: pointer; + } } .linkDescriptionPopup-btn-add { @@ -62,6 +66,10 @@ text-align: center; position: relative; justify-content: center; + + &:hover{ + cursor: pointer; + } } } diff --git a/src/client/views/nodes/LinkDescriptionPopup.tsx b/src/client/views/nodes/LinkDescriptionPopup.tsx index 3bb52d9fb..06e8d30d1 100644 --- a/src/client/views/nodes/LinkDescriptionPopup.tsx +++ b/src/client/views/nodes/LinkDescriptionPopup.tsx @@ -19,15 +19,7 @@ export class LinkDescriptionPopup extends React.Component<{}> { @action descriptionChanged = (e: React.ChangeEvent<HTMLInputElement>) => { - this.description = e.currentTarget.value; - } - - @action - setDescription = () => { - if (LinkManager.currentLink) { - LinkManager.currentLink.description = this.description; - } - LinkDescriptionPopup.descriptionPopup = false; + LinkManager.currentLink && (LinkManager.currentLink.description = e.currentTarget.value); } @action @@ -58,7 +50,7 @@ export class LinkDescriptionPopup extends React.Component<{}> { left: LinkDescriptionPopup.popupX ? LinkDescriptionPopup.popupX : 700, top: LinkDescriptionPopup.popupY ? LinkDescriptionPopup.popupY : 350, }}> - <input className="linkDescriptionPopup-input" + <input className="linkDescriptionPopup-input" onKeyPress={e => e.key === "Enter" && this.onDismiss()} placeholder={"(optional) enter link label..."} onChange={(e) => this.descriptionChanged(e)}> </input> @@ -66,7 +58,7 @@ export class LinkDescriptionPopup extends React.Component<{}> { <div className="linkDescriptionPopup-btn-dismiss" onPointerDown={this.onDismiss}> Dismiss </div> <div className="linkDescriptionPopup-btn-add" - onPointerDown={this.setDescription}> Add </div> + onPointerDown={this.onDismiss}> Add </div> </div> </div>; } diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx index 197dc8df4..ebb916307 100644 --- a/src/client/views/nodes/LinkDocPreview.tsx +++ b/src/client/views/nodes/LinkDocPreview.tsx @@ -15,6 +15,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { LinkManager } from '../../util/LinkManager'; import { DocumentLinksButton } from './DocumentLinksButton'; import { ContextMenu } from '../ContextMenu'; +import { undoBatch } from '../../util/UndoManager'; interface Props { linkDoc?: Doc; @@ -32,14 +33,6 @@ export class LinkDocPreview extends React.Component<Props> { _editRef = React.createRef<HTMLDivElement>(); @action - deleteLink = (): void => { - this.props.linkDoc ? LinkManager.Instance.deleteLink(this.props.linkDoc) : null; - //this.props.showLinks(); - LinkDocPreview.LinkInfo = undefined; - DocumentLinksButton.EditLink = undefined; - } - - @action onContextMenu = (e: React.MouseEvent) => { DocumentLinksButton.EditLink = undefined; LinkDocPreview.LinkInfo = undefined; @@ -98,23 +91,11 @@ export class LinkDocPreview extends React.Component<Props> { {this._toolTipText} </div> </div> : - // <div style={{ - // border: "6px solid white", - // }}> - // <div style={{ backgroundColor: "white" }}> {this._targetDoc.title} - // <div className="wrapper" style={{ float: "right" }}> - // <div title="Delete link" className="button" style={{ display: "inline" }} ref={this._editRef} onPointerDown={this.deleteLink}> - // <FontAwesomeIcon className="fa-icon" icon="trash" size="sm" /></div> - // <div title="Follow link" className="button" style={{ display: "inline" }} onClick={this.followDefault} onContextMenu={this.onContextMenu}> - // <FontAwesomeIcon className="fa-icon" icon="arrow-right" size="sm" /> - // </div> - // </div> - // </div> + <ContentFittingDocumentView Document={this._targetDoc} LibraryPath={emptyPath} fitToBox={true} - backgroundColor={this.props.backgroundColor} moveDocument={returnFalse} rootSelected={returnFalse} ScreenToLocalTransform={Transform.Identity} @@ -128,16 +109,15 @@ export class LinkDocPreview extends React.Component<Props> { ContainingCollectionDoc={undefined} ContainingCollectionView={undefined} renderDepth={0} - PanelWidth={this.width} - PanelHeight={this.height} + PanelWidth={() => this.width() - 16} //Math.min(350, NumCast(target._width, 350))} + PanelHeight={() => this.height() - 16} //Math.min(250, NumCast(target._height, 250))} focus={emptyFunction} whenActiveChanged={returnFalse} bringToFront={returnFalse} ContentScaling={returnOne} NativeWidth={returnZero} NativeHeight={returnZero} - />; - //</div>; + backgroundColor={this.props.backgroundColor} />; } render() { @@ -145,7 +125,10 @@ export class LinkDocPreview extends React.Component<Props> { style={{ position: "absolute", left: this.props.location[0], top: this.props.location[1], width: this.width(), height: this.height(), - boxShadow: "black 2px 2px 1em", zIndex: 1000 + zIndex: 1000, + border: "8px solid white", borderRadius: "7px", + boxShadow: "3px 3px 1.5px grey", + borderBottom: "8px solid white", borderRight: "8px solid white" }}> {this.targetDocView} </div>; diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 1c5825a8f..323da1233 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -268,7 +268,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum if (!this._pdfjsRequested) { this._pdfjsRequested = true; const promise = Pdfjs.getDocument(pdfUrl.url.href).promise; - promise.then(action(pdf => { this._pdf = pdf; console.log("promise"); })); + promise.then(action(pdf => this._pdf = pdf)); } } diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx index 8818d375e..a304ced18 100644 --- a/src/client/views/nodes/PresBox.tsx +++ b/src/client/views/nodes/PresBox.tsx @@ -297,7 +297,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> (this.layoutDoc.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false) render() { - // console.log("render = " + this.layoutDoc.title + " " + this.layoutDoc.presStatus); // const presOrderedDocs = DocListCast(this.rootDoc.presOrderedDocs); // if (presOrderedDocs.length != this.childDocs.length || presOrderedDocs.some((pd, i) => pd !== this.childDocs[i])) { // this.rootDoc.presOrderedDocs = new List<Doc>(this.childDocs.slice()); diff --git a/src/client/views/nodes/RadialMenu.tsx b/src/client/views/nodes/RadialMenu.tsx index a3ac09a11..7f0956e51 100644 --- a/src/client/views/nodes/RadialMenu.tsx +++ b/src/client/views/nodes/RadialMenu.tsx @@ -89,7 +89,6 @@ export class RadialMenu extends React.Component { @action componentDidMount = () => { - console.log(this._pageX); document.addEventListener("pointerdown", this.onPointerDown); document.addEventListener("pointerup", this.onPointerUp); this.previewcircle(); diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx index f7dee0896..1cd29d795 100644 --- a/src/client/views/nodes/ScreenshotBox.tsx +++ b/src/client/views/nodes/ScreenshotBox.tsx @@ -112,7 +112,7 @@ export class ScreenshotBox extends ViewBoxBaseComponent<FieldViewProps, Screensh return returnedUri; } catch (e) { - console.log(e); + console.log("ScreenShotBox:" + e); } } @observable _screenCapture = false; diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx index 04ac34cc2..bc43cd473 100644 --- a/src/client/views/nodes/ScriptingBox.tsx +++ b/src/client/views/nodes/ScriptingBox.tsx @@ -109,7 +109,6 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps, Sc @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; diff --git a/src/client/views/nodes/SliderBox.tsx b/src/client/views/nodes/SliderBox.tsx index 9a1aefba9..45cdfc5ad 100644 --- a/src/client/views/nodes/SliderBox.tsx +++ b/src/client/views/nodes/SliderBox.tsx @@ -56,7 +56,7 @@ export class SliderBox extends ViewBoxBaseComponent<FieldViewProps, SliderDocume style={{ boxShadow: this.layoutDoc.opacity === 0 ? undefined : StrCast(this.layoutDoc.boxShadow, "") }}> <div className="sliderBox-mainButton" onContextMenu={this.specificContextMenu} style={{ background: StrCast(this.layoutDoc.backgroundColor), color: StrCast(this.layoutDoc.color, "black"), - fontSize: NumCast(this.layoutDoc._fontSize), letterSpacing: StrCast(this.layoutDoc.letterSpacing) + fontSize: StrCast(this.layoutDoc._fontSize), letterSpacing: StrCast(this.layoutDoc.letterSpacing) }} > <Slider mode={2} diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index a5c6c4a48..ee92e517c 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -205,7 +205,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD return returnedUri; } catch (e) { - console.log(e); + console.log("VideoBox :" + e); } } @observable _screenCapture = false; diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 05355caba..39bb7c01d 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -189,7 +189,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum this.dataDoc[this.fieldKey] = new WebField(URLy); this.dataDoc[this.annotationKey] = new List<Doc>([]); } catch (e) { - console.log("Error in URL :" + this._url); + console.log("WebBox URL error:" + this._url); } } diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx index 5c3f3dcc9..212da3f3d 100644 --- a/src/client/views/nodes/formattedText/DashDocView.tsx +++ b/src/client/views/nodes/formattedText/DashDocView.tsx @@ -209,7 +209,7 @@ export class DashDocView extends React.Component<IDashDocView> { 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" })); } catch (e) { - console.log(e); + console.log("DashDocView:" + e); } } diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index fc63dfbf5..38fa66d65 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -13,7 +13,7 @@ import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from " import { ReplaceStep } from 'prosemirror-transform'; import { EditorView } from "prosemirror-view"; import { DateField } from '../../../../fields/DateField'; -import { DataSym, Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym, AclSym } from "../../../../fields/Doc"; +import { DataSym, Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym, AclEdit } from "../../../../fields/Doc"; import { documentSchema } from '../../../../fields/documentSchemas'; import applyDevTools = require("prosemirror-dev-tools"); import { removeMarkWithAttrs } from "./prosemirrorPatches"; @@ -24,7 +24,7 @@ import { RichTextField } from "../../../../fields/RichTextField"; import { RichTextUtils } from '../../../../fields/RichTextUtils'; import { createSchema, makeInterface } from "../../../../fields/Schema"; import { Cast, DateCast, NumCast, StrCast, ScriptCast } from "../../../../fields/Types"; -import { TraceMobx, OVERRIDE_ACL } from '../../../../fields/util'; +import { TraceMobx, OVERRIDE_ACL, GetEffectiveAcl } from '../../../../fields/util'; import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnOne, returnZero, Utils, setupMoveUpEvents } from '../../../../Utils'; import { GoogleApiClientUtils, Pulls, Pushes } from '../../../apis/google_docs/GoogleApiClientUtils'; import { DocServer } from "../../../DocServer"; @@ -174,19 +174,25 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp linkOnDeselect: Map<string, string> = new Map(); doLinkOnDeselect() { + Array.from(this.linkOnDeselect.entries()).map(entry => { const key = entry[0]; const value = entry[1]; + const id = Utils.GenerateDeterministicGuid(this.dataDoc[Id] + key); DocServer.GetRefField(value).then(doc => { DocServer.GetRefField(id).then(linkDoc => { this.dataDoc[key] = doc || Docs.Create.FreeformDocument([], { title: value, _width: 500, _height: 500 }, value); DocUtils.Publish(this.dataDoc[key] as Doc, value, this.props.addDocument, this.props.removeDocument); - if (linkDoc) { (linkDoc as Doc).anchor2 = this.dataDoc[key] as Doc; } - else DocUtils.MakeLink({ doc: this.rootDoc }, { doc: this.dataDoc[key] as Doc }, "link to named target", id); + if (linkDoc) { + (linkDoc as Doc).anchor2 = this.dataDoc[key] as Doc; + } else { + DocUtils.MakeLink({ doc: this.rootDoc }, { doc: this.dataDoc[key] as Doc }, "portal link", "link to named target", id); + } }); }); }); + this.linkOnDeselect.clear(); } @@ -226,32 +232,37 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const curProto = Cast(Cast(this.dataDoc.proto, Doc, null)?.[this.fieldKey], RichTextField, null); // the default text inherited from a prototype const curLayout = this.rootDoc !== this.layoutDoc ? Cast(this.layoutDoc[this.fieldKey], RichTextField, null) : undefined; // the default text stored in a layout template const json = JSON.stringify(state.toJSON()); - if (!this.dataDoc[AclSym]) { + let unchanged = true; + if (GetEffectiveAcl(this.dataDoc) === AclEdit) { if (!this._applyingChange && json.replace(/"selection":.*/, "") !== curProto?.Data.replace(/"selection":.*/, "")) { this._applyingChange = true; (curText !== Cast(this.dataDoc[this.fieldKey], RichTextField)?.Text) && (this.dataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now()))); if ((!curTemp && !curProto) || curText || curLayout?.Data.includes("dash")) { // if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended) - if (json !== curLayout?.Data) { + if (json.replace(/"selection":.*/, "") !== curLayout?.Data.replace(/"selection":.*/, "")) { !curText && tx.storedMarks?.map(m => m.type.name === "pFontSize" && (Doc.UserDoc().fontSize = this.layoutDoc._fontSize = m.attrs.fontSize)); !curText && tx.storedMarks?.map(m => m.type.name === "pFontFamily" && (Doc.UserDoc().fontFamily = this.layoutDoc._fontFamily = m.attrs.fontFamily)); this.dataDoc[this.props.fieldKey] = new RichTextField(json, curText); this.dataDoc[this.props.fieldKey + "-noTemplate"] = (curTemp?.Text || "") !== curText; // mark the data field as being split from the template if it has been edited ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: curText }); + unchanged = false; } } else { // if we've deleted all the text in a note driven by a template, then restore the template data this.dataDoc[this.props.fieldKey] = undefined; this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse((curProto || curTemp).Data))); this.dataDoc[this.props.fieldKey + "-noTemplate"] = undefined; // mark the data field as not being split from any template it might have + unchanged = false; } this._applyingChange = false; + if (!unchanged) { + this.updateTitle(); + this.tryUpdateHeight(); + } } } else { const json = JSON.parse(Cast(this.dataDoc[this.fieldKey], RichTextField)?.Data!); json.selection = state.toJSON().selection; this._editorView.updateState(EditorState.fromJSON(this.config, json)); } - this.updateTitle(); - this.tryUpdateHeight(); } } @@ -414,16 +425,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { background: "moccasin" }); } if (FormattedTextBox._highlights.indexOf("Todo Items") !== -1) { - addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "todo", { outline: "black solid 1px" }); + addStyleSheetRule(FormattedTextBox._userStyleSheet, "UT-" + "todo", { outline: "black solid 1px" }); } if (FormattedTextBox._highlights.indexOf("Important Items") !== -1) { - addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "important", { "font-size": "larger" }); + addStyleSheetRule(FormattedTextBox._userStyleSheet, "UT-" + "important", { "font-size": "larger" }); } if (FormattedTextBox._highlights.indexOf("Disagree Items") !== -1) { - addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "disagree", { "text-decoration": "line-through" }); + addStyleSheetRule(FormattedTextBox._userStyleSheet, "UT-" + "disagree", { "text-decoration": "line-through" }); } if (FormattedTextBox._highlights.indexOf("Ignore Items") !== -1) { - addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "ignore", { "font-size": "1" }); + addStyleSheetRule(FormattedTextBox._userStyleSheet, "UT-" + "ignore", { "font-size": "1" }); } if (FormattedTextBox._highlights.indexOf("By Recent Minute") !== -1) { addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { opacity: "0.1" }); @@ -459,9 +470,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp specificContextMenu = (e: React.MouseEvent): void => { const cm = ContextMenu.Instance; - const appearance = ContextMenu.Instance.findByDescription("Appearance..."); - const appearanceItems = appearance && "subitems" in appearance ? appearance.subitems : []; - const changeItems: ContextMenuProps[] = []; const noteTypesDoc = Cast(Doc.UserDoc()["template-notes"], Doc, null); DocListCast(noteTypesDoc?.data).forEach(note => { @@ -473,17 +481,34 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp }); }); changeItems.push({ description: "FreeForm", event: () => DocUtils.makeCustomViewClicked(this.rootDoc, Docs.Create.FreeformDocument, "freeform"), icon: "eye" }); - appearanceItems.push({ description: "Change Perspective...", subitems: changeItems, icon: "external-link-alt" }); + const highlighting: ContextMenuProps[] = []; + ["My Text", "Text from Others", "Todo Items", "Important Items", "Ignore Items", "Disagree Items", "By Recent Minute", "By Recent Hour"].forEach(option => + highlighting.push({ + description: (FormattedTextBox._highlights.indexOf(option) === -1 ? "Highlight " : "Unhighlight ") + option, event: () => { + e.stopPropagation(); + if (FormattedTextBox._highlights.indexOf(option) === -1) { + FormattedTextBox._highlights.push(option); + } else { + FormattedTextBox._highlights.splice(FormattedTextBox._highlights.indexOf(option), 1); + } + this.updateHighlights(); + }, icon: "expand-arrows-alt" + })); + + const uicontrols: ContextMenuProps[] = []; - uicontrols.push({ description: "Toggle Sidebar", event: () => this.layoutDoc._showSidebar = !this.layoutDoc._showSidebar, icon: "expand-arrows-alt" }); - uicontrols.push({ description: "Toggle Dictation Icon", event: () => this.layoutDoc._showAudio = !this.layoutDoc._showAudio, icon: "expand-arrows-alt" }); - uicontrols.push({ description: "Toggle Menubar", event: () => this.toggleMenubar(), icon: "expand-arrows-alt" }); + uicontrols.push({ description: `${this.layoutDoc._showSidebar ? "Hide" : "Show"} Sidebar`, event: () => this.layoutDoc._showSidebar = !this.layoutDoc._showSidebar, icon: "expand-arrows-alt" }); + uicontrols.push({ description: `${this.layoutDoc._showAudio ? "Hide" : "Show"} Dictation Icon`, event: () => this.layoutDoc._showAudio = !this.layoutDoc._showAudio, icon: "expand-arrows-alt" }); + uicontrols.push({ description: "Show Highlights...", noexpand: true, subitems: highlighting, icon: "hand-point-right" }); !Doc.UserDoc().noviceMode && uicontrols.push({ description: "Broadcast Message", event: () => DocServer.GetRefField("rtfProto").then(proto => proto instanceof Doc && (proto.BROADCAST_MESSAGE = Cast(this.rootDoc[this.fieldKey], RichTextField)?.Text)), icon: "expand-arrows-alt" }); + cm.addItem({ description: "UI Controls...", subitems: uicontrols, icon: "asterisk" }); - appearanceItems.push({ description: "UI Controls...", subitems: uicontrols, icon: "asterisk" }); + const appearance = cm.findByDescription("Appearance..."); + const appearanceItems = appearance && "subitems" in appearance ? appearance.subitems : []; + appearanceItems.push({ description: "Change Perspective...", noexpand: true, subitems: changeItems, icon: "external-link-alt" }); this.rootDoc.isTemplateDoc && appearanceItems.push({ description: "Make Default Layout", event: async () => Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.rootDoc), icon: "eye" }); Doc.UserDoc().defaultTextLayout && appearanceItems.push({ description: "Reset default note style", event: () => Doc.UserDoc().defaultTextLayout = undefined, icon: "eye" }); appearanceItems.push({ @@ -511,30 +536,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp Doc.AddDocToList(Cast(Doc.UserDoc()["template-notes"], Doc, null), "data", this.rootDoc); }, icon: "eye" }); - !appearance && ContextMenu.Instance.addItem({ description: "Appearance...", subitems: appearanceItems, icon: "eye" }); - - const funcs: ContextMenuProps[] = []; - - //funcs.push({ description: `${this.Document._autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight, icon: "plus" }); - funcs.push({ description: (!this.layoutDoc._nativeWidth || !this.layoutDoc._nativeHeight ? "Freeze" : "Unfreeze") + " Aspect", event: this.toggleNativeDimensions, icon: "snowflake" }); - funcs.push({ description: "Toggle Single Line", event: () => this.layoutDoc._singleLine = !this.layoutDoc._singleLine, icon: "expand-arrows-alt" }); - - const highlighting: ContextMenuProps[] = []; - ["My Text", "Text from Others", "Todo Items", "Important Items", "Ignore Items", "Disagree Items", "By Recent Minute", "By Recent Hour"].forEach(option => - highlighting.push({ - description: (FormattedTextBox._highlights.indexOf(option) === -1 ? "Highlight " : "Unhighlight ") + option, event: () => { - e.stopPropagation(); - if (FormattedTextBox._highlights.indexOf(option) === -1) { - FormattedTextBox._highlights.push(option); - } else { - FormattedTextBox._highlights.splice(FormattedTextBox._highlights.indexOf(option), 1); - } - this.updateHighlights(); - }, icon: "expand-arrows-alt" - })); - funcs.push({ description: "highlighting...", subitems: highlighting, icon: "hand-point-right" }); - - ContextMenu.Instance.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); + cm.addItem({ description: "Appearance...", subitems: appearanceItems, icon: "eye" }); + + const options = cm.findByDescription("Options..."); + const optionItems = options && "subitems" in options ? options.subitems : []; + !Doc.UserDoc().noviceMode && optionItems.push({ description: this.Document._singleLine ? "Make Single Line" : "Make Multi Line", event: () => this.layoutDoc._singleLine = !this.layoutDoc._singleLine, icon: "expand-arrows-alt" }); + optionItems.push({ description: `${this.Document._autoHeight ? "Lock" : "Auto"} Height`, event: () => this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight, icon: "plus" }); + optionItems.push({ description: `${!this.layoutDoc._nativeWidth || !this.layoutDoc._nativeHeight ? "Lock" : "Unlock"} Aspect`, event: this.toggleNativeDimensions, icon: "snowflake" }); + !options && cm.addItem({ description: "Options...", subitems: optionItems, icon: "eye" }); this._downX = this._downY = Number.NaN; } @@ -551,11 +560,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } stopDictation = (abort: boolean) => { DictationManager.Controls.stop(!abort); }; - @action - toggleMenubar = () => { - this.layoutDoc._chromeStatus = this.layoutDoc._chromeStatus === "disabled" ? "enabled" : "disabled"; - } - recordBullet = async () => { const completedCue = "end session"; const results = await DictationManager.Controls.listen({ @@ -944,6 +948,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp frag.forEach(node => nodes.push(marker(node))); return Fragment.fromArray(nodes); } + + function addLinkMark(node: Node, title: string, linkId: string) { if (!node.isText) { const content = addMarkToFrag(node.content, (node: Node) => addLinkMark(node, title, linkId)); @@ -1028,7 +1034,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this._editorView?.destroy(); } - static _downEvent: any; + _downEvent: any; _downX = 0; _downY = 0; _break = false; @@ -1048,7 +1054,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this._downX = e.clientX; this._downY = e.clientY; this.doLinkOnDeselect(); - FormattedTextBox._downEvent = true; + this._downEvent = true; FormattedTextBoxComment.textBox = this; if (this.props.onClick && e.button === 0 && !this.props.isSelected(false)) { e.preventDefault(); @@ -1064,8 +1070,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } onPointerUp = (e: React.PointerEvent): void => { - if (!FormattedTextBox._downEvent) return; - FormattedTextBox._downEvent = false; + if (!this._downEvent) return; + this._downEvent = false; if (!(e.nativeEvent as any).formattedHandled) { const editor = this._editorView!; FormattedTextBoxComment.textBox = this; @@ -1084,7 +1090,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp onDoubleClick = (e: React.MouseEvent): void => { this.doLinkOnDeselect(); - FormattedTextBox._downEvent = true; FormattedTextBoxComment.textBox = this; if (this.props.onClick && e.button === 0 && !this.props.isSelected(false)) { e.preventDefault(); @@ -1168,7 +1173,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } } else if ([this._editorView!.state.schema.nodes.ordered_list, this._editorView!.state.schema.nodes.listItem].includes(node?.type) && node !== (this._editorView!.state.selection as NodeSelection)?.node && pcords) { - this._editorView!.dispatch(this._editorView!.state.tr.setSelection(NodeSelection.create(this._editorView!.state.doc, pcords.pos!))); + this._editorView!.dispatch(this._editorView!.state.tr.setSelection(NodeSelection.create(this._editorView!.state.doc, pcords.pos))); } } if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); return; } @@ -1236,7 +1241,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp richTextMenuPlugin() { return new Plugin({ view(newView) { - RichTextMenu.Instance && RichTextMenu.Instance.changeView(newView); + RichTextMenu.Instance?.changeView(newView); return RichTextMenu.Instance; } }); @@ -1291,9 +1296,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp if (e.key === "Tab" || e.key === "Enter") { e.preventDefault(); } - 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)); + if (e.key === " " || this._lastTimedMark?.attrs.userid !== Doc.CurrentUserEmail) { + const mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) }); + this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(mark)); + } if (!this._undoTyping) { this.startUndoTypingBatch(); @@ -1343,9 +1349,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const scale = this.props.ContentScaling() * NumCast(this.layoutDoc._viewScale, 1); const rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : ""; const interactive = Doc.GetSelectedTool() === InkTool.None && !this.layoutDoc.isBackground; - if (this.props.isSelected()) { - setTimeout(() => this._editorView && RichTextMenu.Instance.updateFromDash(this._editorView, undefined, this.props), 0); - } else if (FormattedTextBoxComment.textBox === this) { + setTimeout(() => this._editorView && RichTextMenu.Instance.updateFromDash(this._editorView, undefined, this.props), this.props.isSelected() ? 10 : 0); // need to make sure that we update a text box that is selected after updating the one that was deselected + if (!this.props.isSelected() && FormattedTextBoxComment.textBox === this) { setTimeout(() => FormattedTextBoxComment.Hide(), 0); } const selPad = this.props.isSelected() ? -10 : 0; @@ -1368,7 +1373,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp opacity: this.props.hideOnLeave ? (this._entered ? 1 : 0.1) : 1, color: this.props.color ? this.props.color : StrCast(this.layoutDoc[this.props.fieldKey + "-color"], this.props.hideOnLeave ? "white" : "inherit"), pointerEvents: interactive ? undefined : "none", - fontSize: Cast(this.layoutDoc._fontSize, "number", null), + fontSize: Cast(this.layoutDoc._fontSize, "string", null), fontFamily: StrCast(this.layoutDoc._fontFamily, "inherit"), transition: "opacity 1s" }} diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss b/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss index 9089e7039..6a403cb17 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss @@ -4,10 +4,74 @@ z-index: 20; background: white; border: 1px solid silver; - border-radius: 2px; + border-radius: 7px; margin-bottom: 7px; -webkit-transform: translateX(-50%); transform: translateX(-50%); + box-shadow: 3px 3px 1.5px grey; + + .FormattedTextBoxComment { + background-color: white; + border: 8px solid white; + + //display: flex; + .FormattedTextBoxComment-info { + + margin-bottom: 9px; + + .FormattedTextBoxComment-title { + padding-right: 4px; + float: left; + + .FormattedTextBoxComment-description { + text-decoration: none; + font-style: italic; + color: rgb(95, 97, 102); + font-size: 10px; + padding-bottom: 4px; + margin-bottom: 5px; + + } + } + + .FormattedTextBoxComment-button { + display: inline; + padding-left: 6px; + padding-right: 6px; + padding-top: 2.5px; + padding-bottom: 2.5px; + width: 17px; + height: 17px; + margin: 0; + margin-right: 3px; + border-radius: 50%; + pointer-events: auto; + background-color: rgb(0, 0, 0); + color: rgb(255, 255, 255); + transition: transform 0.2s; + text-align: center; + position: relative; + font-size: 12px; + + &:hover { + background-color: rgb(77, 77, 77); + cursor: pointer; + } + } + } + + .FormattedTextBoxComment-preview-wrapper { + width: 170px; + height: 170px; + overflow: hidden; + //padding-top: 5px; + margin-top: 10px; + margin-bottom: 8px; + align-content: center; + justify-content: center; + background-color: rgb(160, 160, 160); + } + } } .FormattedTextBox-tooltip:before { @@ -42,64 +106,4 @@ top: 50%; right: 0; transform: translateY(-50%); - - .FormattedTextBoxComment-button { - width: 20px; - height: 20px; - margin: 0; - margin-right: 6px; - border-radius: 50%; - pointer-events: auto; - background-color: rgb(38, 40, 41); - color: rgb(178, 181, 184); - font-size: 65%; - transition: transform 0.2s; - text-align: center; - position: relative; - - // margin-top: "auto"; - // margin-bottom: "auto"; - // background: black; - // color: white; - // display: inline-block; - // border-radius: 18px; - // font-size: 12.5px; - // width: 18px; - // height: 18px; - // margin-top: auto; - // margin-bottom: auto; - // margin-right: 3px; - // cursor: pointer; - // transition: transform 0.2s; - - .FormattedTextBoxComment-fa-icon { - margin-top: "auto"; - margin-bottom: "auto"; - background: black; - color: white; - display: inline-block; - border-radius: 18px; - font-size: 12.5px; - width: 18px; - height: 18px; - margin-top: auto; - margin-bottom: auto; - margin-right: 3px; - cursor: pointer; - transition: transform 0.2s; - // position: absolute; - // top: 50%; - // left: 50%; - // transform: translate(-50%, -50%); - } - - &:last-child { - margin-right: 0; - } - - &:hover { - background: rgb(53, 146, 199); - ; - } - } }
\ No newline at end of file diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx index 56826e5c7..6f3984f39 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx @@ -3,7 +3,7 @@ import { EditorState, Plugin } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import * as ReactDOM from 'react-dom'; import { Doc, DocCastAsync, Opt } from "../../../../fields/Doc"; -import { Cast, FieldValue, NumCast } from "../../../../fields/Types"; +import { Cast, FieldValue, NumCast, StrCast } from "../../../../fields/Types"; import { emptyFunction, returnEmptyString, returnFalse, Utils, emptyPath, returnZero, returnOne, returnEmptyFilter } from "../../../../Utils"; import { DocServer } from "../../../DocServer"; import { DocumentManager } from "../../../util/DocumentManager"; @@ -21,6 +21,8 @@ import { action } from "mobx"; import { LinkManager } from "../../../util/LinkManager"; import { LinkDocPreview } from "../LinkDocPreview"; import { DocumentLinksButton } from "../DocumentLinksButton"; +import { Tooltip } from "@material-ui/core"; +import { undoBatch } from "../../../util/UndoManager"; export let formattedTextBoxCommentPlugin = new Plugin({ view(editorView) { return new FormattedTextBoxComment(editorView); } @@ -85,13 +87,13 @@ export class FormattedTextBoxComment { FormattedTextBoxComment.tooltip.className = "FormattedTextBox-tooltip"; FormattedTextBoxComment.tooltip.style.pointerEvents = "all"; FormattedTextBoxComment.tooltip.style.maxWidth = "200px"; - FormattedTextBoxComment.tooltip.style.maxHeight = "206px"; + FormattedTextBoxComment.tooltip.style.maxHeight = "235px"; FormattedTextBoxComment.tooltip.style.width = "100%"; FormattedTextBoxComment.tooltip.style.height = "100%"; FormattedTextBoxComment.tooltip.style.overflow = "hidden"; FormattedTextBoxComment.tooltip.style.display = "none"; FormattedTextBoxComment.tooltip.appendChild(FormattedTextBoxComment.tooltipInput); - FormattedTextBoxComment.tooltip.onpointerdown = (e: PointerEvent) => { + FormattedTextBoxComment.tooltip.onpointerdown = async (e: PointerEvent) => { const keep = e.target && (e.target as any).type === "checkbox" ? true : false; const textBox = FormattedTextBoxComment.textBox; if (FormattedTextBoxComment.linkDoc && !keep && textBox) { @@ -103,8 +105,22 @@ export class FormattedTextBoxComment { if (FormattedTextBoxComment.linkDoc.type !== DocumentType.LINK) { textBox.props.addDocTab(FormattedTextBoxComment.linkDoc, e.ctrlKey ? "inTab" : "onRight"); } else { - DocumentManager.Instance.FollowLink(FormattedTextBoxComment.linkDoc, textBox.props.Document, - (doc: Doc, followLinkLocation: string) => textBox.props.addDocTab(doc, e.ctrlKey ? "inTab" : followLinkLocation)); + const anchor = FieldValue(Doc.AreProtosEqual(FieldValue(Cast(FormattedTextBoxComment.linkDoc.anchor1, Doc)), textBox.dataDoc) ? + Cast(FormattedTextBoxComment.linkDoc.anchor2, Doc) : (Cast(FormattedTextBoxComment.linkDoc.anchor1, Doc)) + || FormattedTextBoxComment.linkDoc); + const target = anchor?.annotationOn ? await DocCastAsync(anchor.annotationOn) : anchor; + + if (FormattedTextBoxComment.linkDoc.follow) { + if (FormattedTextBoxComment.linkDoc.follow === "Default") { + DocumentManager.Instance.FollowLink(FormattedTextBoxComment.linkDoc, textBox.props.Document, doc => textBox.props.addDocTab(doc, "onRight"), false); + } else if (FormattedTextBoxComment.linkDoc.follow === "Always open in right tab") { + if (target) { textBox.props.addDocTab(target, "onRight"); } + } else if (FormattedTextBoxComment.linkDoc.follow === "Always open in new tab") { + if (target) { textBox.props.addDocTab(target, "inTab"); } + } + } else { + DocumentManager.Instance.FollowLink(FormattedTextBoxComment.linkDoc, textBox.props.Document, doc => textBox.props.addDocTab(doc, "onRight"), false); + } } } else { if (FormattedTextBoxComment.linkDoc.type !== DocumentType.LINK) { @@ -128,6 +144,7 @@ export class FormattedTextBoxComment { } } + @undoBatch @action deleteLink = () => { FormattedTextBoxComment.linkDoc ? LinkManager.Instance.deleteLink(FormattedTextBoxComment.linkDoc) : null; @@ -241,55 +258,36 @@ export class FormattedTextBoxComment { } if (target?.author) { FormattedTextBoxComment.showCommentbox("", view, nbef); - const docPreview = <div style={{ backgroundColor: "white", border: "8px solid white" }}> - {target.title} - <div className="wrapper" style={{ float: "right" }}> - <div title="Delete link" className="FormattedTextBoxComment-button" style={{ - display: "inline", - paddingLeft: "6px", - paddingRight: "6px", - paddingTop: "2.5px", - paddingBottom: "2.5px", - width: "20px", - height: "20px", - margin: 0, - marginRight: "6px", - borderRadius: "50%", - pointerEvents: "auto", - backgroundColor: "rgb(38, 40, 41)", - color: "rgb(178, 181, 184)", - transition: "transform 0.2s", - textAlign: "center", - position: "relative" - }} ref={(r) => this._deleteRef = r}> - <FontAwesomeIcon className="FormattedTextBox-fa-icon" icon="trash" - size="sm" /></div> - <div title="Follow link" className="FormattedTextBoxComment-button" style={{ - display: "inline", - paddingLeft: "6px", - paddingRight: "6px", - paddingTop: "2.5px", - paddingBottom: "2.5px", - width: "20px", - height: "20px", - margin: 0, - marginRight: "6px", - borderRadius: "50%", - pointerEvents: "auto", - backgroundColor: "rgb(38, 40, 41)", - color: "rgb(178, 181, 184)", - transition: "transform 0.2s", - textAlign: "center", - position: "relative" - }} ref={(r) => this._followRef = r}> - <FontAwesomeIcon className="FormattedTextBox-fa-icon" icon="arrow-right" - size="sm" /> + + const title = StrCast(target.title).length > 16 ? + StrCast(target.title).substr(0, 16) + "..." : target.title; + + + const docPreview = <div className="FormattedTextBoxComment"> + <div className="FormattedTextBoxComment-info"> + <div className="FormattedTextBoxComment-title"> + {title} + {FormattedTextBoxComment.linkDoc.description !== "" ? <p className="FormattedTextBoxComment-description"> + {StrCast(FormattedTextBoxComment.linkDoc.description)}</p> : null} </div> - </div> - <div className="wrapper" style={{ - maxWidth: "180px", maxHeight: "168px", overflow: "hidden", - overflowY: "hidden", paddingTop: "5px" - }}> + <div className="wrapper" style={{ float: "right" }}> + + <Tooltip title={<><div className="dash-tooltip">Delete Link</div></>} placement="top"> + <div className="FormattedTextBoxComment-button" + ref={(r) => this._deleteRef = r}> + <FontAwesomeIcon className="FormattedTextBoxComment-fa-icon" icon="trash" color="white" + size="sm" /></div> + </Tooltip> + + <Tooltip title={<><div className="dash-tooltip">Follow Link</div></>} placement="top"> + <div className="FormattedTextBoxComment-button" + ref={(r) => this._followRef = r}> + <FontAwesomeIcon className="FormattedTextBoxComment-fa-icon" icon="arrow-right" color="white" + size="sm" /> + </div> + </Tooltip> + </div> </div> + <div className="FormattedTextBoxComment-preview-wrapper"> <ContentFittingDocumentView Document={target} LibraryPath={emptyPath} @@ -307,17 +305,20 @@ export class FormattedTextBoxComment { ContainingCollectionDoc={undefined} ContainingCollectionView={undefined} renderDepth={0} - PanelWidth={() => Math.min(350, NumCast(target._width, 350))} - PanelHeight={() => Math.min(250, NumCast(target._height, 250))} + PanelWidth={() => 175} //Math.min(350, NumCast(target._width, 350))} + PanelHeight={() => 175} //Math.min(250, NumCast(target._height, 250))} focus={emptyFunction} whenActiveChanged={returnFalse} bringToFront={returnFalse} ContentScaling={returnOne} - NativeWidth={returnZero} - NativeHeight={returnZero} + NativeWidth={() => target._nativeWidth ? NumCast(target._nativeWidth) : 0} + NativeHeight={() => target._nativeHeight ? NumCast(target._nativeHeight) : 0} /> </div> </div>; + + + FormattedTextBoxComment.showCommentbox("", view, nbef); ReactDOM.render(docPreview, FormattedTextBoxComment.tooltipText); diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts index 3f73ec436..8faf752b4 100644 --- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts +++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts @@ -11,6 +11,7 @@ import { Doc, DataSym } from "../../../../fields/Doc"; import { FormattedTextBox } from "./FormattedTextBox"; import { Id } from "../../../../fields/FieldSymbols"; import { Docs } from "../../../documents/Documents"; +import { Utils } from "../../../../Utils"; const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false; @@ -102,7 +103,7 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, props: any //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 }); + const newDoc = Docs.Create.PdfDocument(Utils.prepend("/assets/cheat-sheet.pdf"), { _fitWidth: true, _width: 300, _height: 300 }); props.addDocTab(newDoc, "onRight"); }); diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index f10c425d4..47a4911b8 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -11,7 +11,7 @@ import { EditorState, NodeSelection, TextSelection } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import { Doc } from "../../../../fields/Doc"; import { DarkPastelSchemaPalette, PastelSchemaPalette } from '../../../../fields/SchemaHeaderField'; -import { Cast, StrCast, BoolCast } from "../../../../fields/Types"; +import { Cast, StrCast, BoolCast, NumCast } from "../../../../fields/Types"; import { unimplementedFunction, Utils } from "../../../../Utils"; import { DocServer } from "../../../DocServer"; import { LinkManager } from "../../../util/LinkManager"; @@ -23,7 +23,8 @@ import { updateBullets } from "./ProsemirrorExampleTransfer"; import "./RichTextMenu.scss"; import { schema } from "./schema_rts"; import { TraceMobx } from "../../../../fields/util"; -import { UndoManager } from "../../../util/UndoManager"; +import { UndoManager, undoBatch } from "../../../util/UndoManager"; +import { Tooltip } from "@material-ui/core"; const { toggleMark } = require("prosemirror-commands"); library.add(faBold, faItalic, faChevronLeft, faUnderline, faStrikethrough, faSuperscript, faSubscript, faOutdent, faIndent, faHandPointLeft, faHandPointRight, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink, faPaintRoller); @@ -112,6 +113,7 @@ export default class RichTextMenu extends AntimodeMenu { { node: schema.nodes.ordered_list.create({ mapStyle: "bullet" }), title: "Set list type", label: ":", command: this.changeListType }, { node: schema.nodes.ordered_list.create({ mapStyle: "decimal" }), title: "Set list type", label: "1.1", command: this.changeListType }, { node: schema.nodes.ordered_list.create({ mapStyle: "multi" }), title: "Set list type", label: "A.1", command: this.changeListType }, + { node: schema.nodes.ordered_list.create({ mapStyle: "" }), title: "Set list type", label: "<none>", command: this.changeListType }, //{ node: undefined, title: "Set list type", label: "Remove", command: this.changeListType }, ]; @@ -154,7 +156,9 @@ export default class RichTextMenu extends AntimodeMenu { @action changeView(view: EditorView) { - this.view = view; + if ((view as any)?.TextView?.props.isSelected(true)) { + this.view = view; + } } update(view: EditorView, lastState: EditorState | undefined) { @@ -163,8 +167,7 @@ export default class RichTextMenu extends AntimodeMenu { @action public async updateFromDash(view: EditorView, lastState: EditorState | undefined, props: any) { - if (!view) { - console.log("no editor? why?"); + if (!view || !(view as any).TextView?.props.isSelected(true)) { return; } this.view = view; @@ -217,7 +220,7 @@ export default class RichTextMenu extends AntimodeMenu { // finds font sizes and families in selection getActiveAlignment() { - if (this.view) { + if (this.view && this.TextView.props.isSelected(true)) { const path = (this.view.state.selection.$from as any).path; for (let i = path.length - 3; i < path.length && i >= 0; i -= 3) { if (path[i]?.type === this.view.state.schema.nodes.paragraph) { @@ -230,15 +233,18 @@ export default class RichTextMenu extends AntimodeMenu { // finds font sizes and families in selection getActiveListStyle() { - if (this.view) { + if (this.view && this.TextView.props.isSelected(true)) { const path = (this.view.state.selection.$from as any).path; for (let i = 0; i < path.length; i += 3) { if (path[i].type === this.view.state.schema.nodes.ordered_list) { return path[i].attrs.mapStyle; } } + if (this.view.state.selection.$from.nodeAfter?.type === this.view.state.schema.nodes.ordered_list) { + return this.view.state.selection.$from.nodeAfter?.attrs.mapStyle; + } } - return "decimal"; + return ""; } // finds font sizes and families in selection @@ -247,16 +253,21 @@ export default class RichTextMenu extends AntimodeMenu { const activeFamilies: string[] = []; const activeSizes: string[] = []; - const state = this.view.state; - const pos = this.view.state.selection.$from; - const ref_node = this.reference_node(pos); - if (ref_node && ref_node !== this.view.state.doc && ref_node.isText) { - ref_node.marks.forEach(m => { - m.type === state.schema.marks.pFontFamily && activeFamilies.push(m.attrs.family); - m.type === state.schema.marks.pFontSize && activeSizes.push(String(m.attrs.fontSize) + "pt"); - }); + if (this.TextView.props.isSelected(true)) { + const state = this.view.state; + const pos = this.view.state.selection.$from; + const ref_node = this.reference_node(pos); + if (ref_node && ref_node !== this.view.state.doc && ref_node.isText) { + ref_node.marks.forEach(m => { + m.type === state.schema.marks.pFontFamily && activeFamilies.push(m.attrs.family); + m.type === state.schema.marks.pFontSize && activeSizes.push(String(m.attrs.fontSize) + "pt"); + }); + } + !activeFamilies.length && (activeFamilies.push(StrCast(this.TextView.layoutDoc._fontFamily, StrCast(Doc.UserDoc().fontFamily)))); + !activeSizes.length && (activeSizes.push(StrCast(this.TextView.layoutDoc._fontSize, StrCast(Doc.UserDoc().fontSize)))); } - + !activeFamilies.length && (activeFamilies.push(StrCast(Doc.UserDoc().fontFamily))); + !activeSizes.length && (activeSizes.push(StrCast(Doc.UserDoc().fontSize))); return { activeFamilies, activeSizes }; } @@ -269,14 +280,14 @@ export default class RichTextMenu extends AntimodeMenu { //finds all active marks on selection in given group getActiveMarksOnSelection() { - if (!this.view) return; + let activeMarks: MarkType[] = []; + if (!this.view || !this.TextView.props.isSelected(true)) return activeMarks; const markGroup = [schema.marks.strong, schema.marks.em, schema.marks.underline, schema.marks.strikethrough, schema.marks.superscript, schema.marks.subscript]; if (this.view.state.storedMarks) return this.view.state.storedMarks.map(mark => mark.type); //current selection const { empty, ranges, $to } = this.view.state.selection as TextSelection; const state = this.view.state; - let activeMarks: MarkType[] = []; if (!empty) { activeMarks = markGroup.filter(mark => { const has = false; @@ -308,7 +319,7 @@ export default class RichTextMenu extends AntimodeMenu { } destroy() { - this.fadeOut(true); + !this.TextView?.props.isSelected(true) && this.fadeOut(true); } @action @@ -348,22 +359,20 @@ export default class RichTextMenu extends AntimodeMenu { } return ( - <button className={"antimodeMenu-button" + (isActive ? " active" : "")} key={title} title={title} onPointerDown={onClick}> - <FontAwesomeIcon icon={faIcon as IconProp} size="lg" /> - </button> + <Tooltip title={<div className="dash-tooltip">{title}</div>} key={title} placement="bottom"> + <button className={"antimodeMenu-button" + (isActive ? " active" : "")} onPointerDown={onClick}> + <FontAwesomeIcon icon={faIcon as IconProp} size="lg" /> + </button> + </Tooltip> ); } - createMarksDropdown(activeOption: string, options: { mark: Mark | null, title: string, label: string, command: (mark: Mark, view: EditorView) => void, hidden?: boolean, style?: {} }[], key: string): JSX.Element { + createMarksDropdown(activeOption: string, options: { mark: Mark | null, title: string, label: string, command: (mark: Mark, view: EditorView) => void, hidden?: boolean, style?: {} }[], key: string, setter: (val: string) => {}): JSX.Element { const items = options.map(({ title, label, hidden, style }) => { if (hidden) { - return label === activeOption ? - <option value={label} title={title} key={label} style={style ? style : {}} selected hidden>{label}</option> : - <option value={label} title={title} key={label} style={style ? style : {}} hidden>{label}</option>; + return <option value={label} title={title} key={label} style={style ? style : {}} hidden>{label}</option>; } - return label === activeOption ? - <option value={label} title={title} key={label} style={style ? style : {}} selected>{label}</option> : - <option value={label} title={title} key={label} style={style ? style : {}}>{label}</option>; + return <option value={label} title={title} key={label} style={style ? style : {}}>{label}</option>; }); const self = this; @@ -372,37 +381,47 @@ export default class RichTextMenu extends AntimodeMenu { e.preventDefault(); self.TextView.endUndoTypingBatch(); options.forEach(({ label, mark, command }) => { - if (e.target.value === label) { - UndoManager.RunInBatch(() => self.view && mark && command(mark, self.view), "text mark dropdown"); + if (e.target.value === label && mark) { + if (!self.TextView.props.isSelected(true)) { + switch (mark.type) { + case schema.marks.pFontFamily: setter(Doc.UserDoc().fontFamily = mark.attrs.family); break; + case schema.marks.pFontSize: setter(Doc.UserDoc().fontSize = mark.attrs.fontSize.toString() + "pt"); break; + } + } + else UndoManager.RunInBatch(() => self.view && mark && command(mark, self.view), "text mark dropdown"); } }); } - return <select onChange={onChange} key={key}>{items}</select>; + return <Tooltip key={key} title={<div className="dash-tooltip">{key}</div>} placement="bottom"> + <select onChange={onChange} value={activeOption}>{items}</select> + </Tooltip>; } - createNodesDropdown(activeMap: string, options: { node: NodeType | any | null, title: string, label: string, command: (node: NodeType | any) => void, hidden?: boolean, style?: {} }[], key: string): JSX.Element { - const activeOption = activeMap === "bullet" ? ":" : activeMap === "decimal" ? "1.1" : "A.1"; + createNodesDropdown(activeMap: string, options: { node: NodeType | any | null, title: string, label: string, command: (node: NodeType | any) => void, hidden?: boolean, style?: {} }[], key: string, setter: (val: string) => {}): JSX.Element { + const activeOption = activeMap === "bullet" ? ":" : activeMap === "decimal" ? "1.1" : activeMap === "multi" ? "A.1" : "<none>"; const items = options.map(({ title, label, hidden, style }) => { if (hidden) { - return label === activeOption ? - <option value={label} title={title} key={label} style={style ? style : {}} selected hidden>{label}</option> : - <option value={label} title={title} key={label} style={style ? style : {}} hidden>{label}</option>; + return <option value={label} title={title} key={label} style={style ? style : {}} hidden>{label}</option>; } - return label === activeOption ? - <option value={label} title={title} key={label} style={style ? style : {}} selected>{label}</option> : - <option value={label} title={title} key={label} style={style ? style : {}}>{label}</option>; + return <option value={label} title={title} key={label} style={style ? style : {}}>{label}</option>; }); const self = this; function onChange(val: string) { self.TextView.endUndoTypingBatch(); options.forEach(({ label, node, command }) => { - if (val === label) { - UndoManager.RunInBatch(() => self.view && node && command(node), "nodes dropdown"); + if (val === label && node) { + if (self.TextView.props.isSelected(true)) { + UndoManager.RunInBatch(() => self.view && node && command(node), "nodes dropdown"); + setter(val); + } } }); } - return <select onChange={e => onChange(e.target.value)} key={key}>{items}</select>; + + return <Tooltip key={key} title={<div className="dash-tooltip">{key}</div>} placement="bottom"> + <select value={activeOption} onChange={e => onChange(e.target.value)}>{items}</select> + </Tooltip>; } changeFontSize = (mark: Mark, view: EditorView) => { @@ -416,10 +435,21 @@ export default class RichTextMenu extends AntimodeMenu { // TODO: remove doesn't work //remove all node type and apply the passed-in one to the selected text changeListType = (nodeType: Node | undefined) => { - if (!this.view) return; + if (!this.view || (nodeType as any)?.attrs.mapStyle === "") return; + + const nextIsOL = this.view.state.selection.$from.nodeAfter?.type === schema.nodes.ordered_list; + let inList: any = undefined; + let fromList = -1; + const path: any = Array.from((this.view.state.selection.$from as any).path); + for (let i = 0; i < path.length; i++) { + if (path[i]?.type === schema.nodes.ordered_list) { + inList = path[i]; + fromList = path[i - 1]; + } + } const marks = this.view.state.storedMarks || (this.view.state.selection.$to.parentOffset && this.view.state.selection.$from.marks()); - if (!wrapInList(schema.nodes.ordered_list)(this.view.state, (tx2: any) => { + if (inList || !wrapInList(schema.nodes.ordered_list)(this.view.state, (tx2: any) => { const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle, this.view!.state.selection.from - 1, this.view!.state.selection.to + 1); marks && tx3.ensureMarks([...marks]); marks && tx3.setStoredMarks([...marks]); @@ -427,12 +457,12 @@ export default class RichTextMenu extends AntimodeMenu { this.view!.dispatch(tx2); })) { const tx2 = this.view.state.tr; - if (nodeType && this.view.state.selection.$from.nodeAfter?.type === schema.nodes.ordered_list) { - const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle, this.view.state.selection.from, this.view.state.selection.to); + if (nodeType && (inList || nextIsOL)) { + const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle, inList ? fromList : this.view.state.selection.from, + inList ? fromList + inList.nodeSize : this.view.state.selection.to); marks && tx3.ensureMarks([...marks]); marks && tx3.setStoredMarks([...marks]); - - this.view.dispatch(tx3.setSelection(new NodeSelection(tx3.doc.resolve(this.view.state.selection.$from.pos)))); + this.view.dispatch(tx3); } } } @@ -448,13 +478,13 @@ export default class RichTextMenu extends AntimodeMenu { return true; } alignCenter = (state: EditorState<any>, dispatch: any) => { - return this.alignParagraphs(state, "center", dispatch); + return this.TextView.props.isSelected(true) && this.alignParagraphs(state, "center", dispatch); } alignLeft = (state: EditorState<any>, dispatch: any) => { - return this.alignParagraphs(state, "left", dispatch); + return this.TextView.props.isSelected(true) && this.alignParagraphs(state, "left", dispatch); } alignRight = (state: EditorState<any>, dispatch: any) => { - return this.alignParagraphs(state, "right", dispatch); + return this.TextView.props.isSelected(true) && this.alignParagraphs(state, "right", dispatch); } alignParagraphs(state: EditorState<any>, align: "left" | "right" | "center", dispatch: any) { @@ -574,10 +604,11 @@ export default class RichTextMenu extends AntimodeMenu { label = "No marks are currently stored"; } - const button = - <button className="antimodeMenu-button" title="" onPointerDown={onBrushClick} style={this.brushMarks?.size > 0 ? { backgroundColor: "121212" } : {}}> + const button = <Tooltip title={<div className="dash-tooltip">style brush</div>} placement="bottom"> + <button className="antimodeMenu-button" onPointerDown={onBrushClick} style={this.brushMarks?.size > 0 ? { backgroundColor: "121212" } : {}}> <FontAwesomeIcon icon="paint-roller" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.1s", transform: `rotate(${this.brushMarks?.size > 0 ? 45 : 0}deg)` }} /> - </button>; + </button> + </Tooltip>; const dropdownContent = <div className="dropdown"> @@ -626,7 +657,7 @@ export default class RichTextMenu extends AntimodeMenu { @action toggleColorDropdown() { this.showColorDropdown = !this.showColorDropdown; } @action setActiveColor(color: string) { this.activeFontColor = color; } - get TextView() { return (this.view as any).TextView as FormattedTextBox; } + get TextView() { return (this.view as any)?.TextView as FormattedTextBox; } createColorButton() { const self = this; @@ -646,11 +677,12 @@ export default class RichTextMenu extends AntimodeMenu { self.TextView.EditorView!.focus(); } - const button = - <button className="antimodeMenu-button color-preview-button" title="" onPointerDown={onColorClick}> + const button = <Tooltip title={<div className="dash-tooltip">set font color</div>} placement="bottom"> + <button className="antimodeMenu-button color-preview-button" onPointerDown={onColorClick}> <FontAwesomeIcon icon="palette" size="lg" /> <div className="color-preview" style={{ backgroundColor: this.activeFontColor }}></div> - </button>; + </button> + </Tooltip>; const dropdownContent = <div className="dropdown" > @@ -699,11 +731,12 @@ export default class RichTextMenu extends AntimodeMenu { UndoManager.RunInBatch(() => self.view && self.insertHighlight(self.activeHighlightColor, self.view.state, self.view.dispatch), "rt highlighter"); } - const button = - <button className="antimodeMenu-button color-preview-button" title="" key="highilghter-button" onPointerDown={onHighlightClick}> + const button = <Tooltip title={<div className="dash-tooltip">set highlight color</div>} placement="bottom"> + <button className="antimodeMenu-button color-preview-button" key="highilghter-button" onPointerDown={onHighlightClick}> <FontAwesomeIcon icon="highlighter" size="lg" /> <div className="color-preview" style={{ backgroundColor: this.activeHighlightColor }}></div> - </button>; + </button> + </Tooltip>; const dropdownContent = <div className="dropdown"> @@ -742,7 +775,9 @@ export default class RichTextMenu extends AntimodeMenu { const link = this.currentLink ? this.currentLink : ""; - const button = <FontAwesomeIcon icon="link" size="lg" />; + const button = <Tooltip title={<div className="dash-tooltip">set hyperlink</div>} placement="bottom"> + <div><FontAwesomeIcon icon="link" size="lg" /> </div> + </Tooltip>; const dropdownContent = <div className="dropdown link-menu"> @@ -753,9 +788,7 @@ export default class RichTextMenu extends AntimodeMenu { <button className="remove-button" onPointerDown={e => this.deleteLink()}>Remove link</button> </div>; - return ( - <ButtonDropdown view={this.view} key={"link button"} button={button} dropdownContent={dropdownContent} openDropdownOnButton={true} /> - ); + return <ButtonDropdown view={this.view} key={"link button"} button={button} dropdownContent={dropdownContent} openDropdownOnButton={true} />; } async getTextLinkTargetTitle() { @@ -798,6 +831,8 @@ export default class RichTextMenu extends AntimodeMenu { ((this.view as any)?.TextView as FormattedTextBox).makeLinkToSelection("", target, "onRight", "", target); } + @undoBatch + @action deleteLink = () => { if (this.view) { const link = this.view.state.selection.$from.nodeAfter?.marks.find(m => m.type === this.view!.state.schema.marks.linkAnchor); @@ -882,22 +917,22 @@ export default class RichTextMenu extends AntimodeMenu { render() { TraceMobx(); - const row1 = <div className="antimodeMenu-row" key="row1" style={{ display: this.collapsed ? "none" : undefined }}>{[ + const row1 = <div className="antimodeMenu-row" key="row 1" style={{ display: this.collapsed ? "none" : undefined }}>{[ !this.collapsed ? this.getDragger() : (null), - !this.Pinned ? (null) : <> {[ + !this.Pinned ? (null) : <div key="frag1"> {[ this.createButton("bold", "Bold", this.boldActive, toggleMark(schema.marks.strong)), this.createButton("italic", "Italic", this.italicsActive, toggleMark(schema.marks.em)), this.createButton("underline", "Underline", this.underlineActive, toggleMark(schema.marks.underline)), this.createButton("strikethrough", "Strikethrough", this.strikethroughActive, toggleMark(schema.marks.strikethrough)), this.createButton("superscript", "Superscript", this.superscriptActive, toggleMark(schema.marks.superscript)), this.createButton("subscript", "Subscript", this.subscriptActive, toggleMark(schema.marks.subscript)), - <div className="richTextMenu-divider" /> - ]}</>, + <div className="richTextMenu-divider" key="divider" /> + ]}</div>, this.createColorButton(), this.createHighlighterButton(), this.createLinkButton(), this.createBrushButton(), - <div className="richTextMenu-divider" />, + <div className="richTextMenu-divider" key="divider 2" />, this.createButton("align-left", "Align Left", this.activeAlignment === "left", this.alignLeft), this.createButton("align-center", "Align Center", this.activeAlignment === "center", this.alignCenter), this.createButton("align-right", "Align Right", this.activeAlignment === "right", this.alignRight), @@ -907,20 +942,20 @@ export default class RichTextMenu extends AntimodeMenu { this.createButton("hand-point-right", "Indent", undefined, this.indentParagraph), ]}</div>; - const row2 = <div className="antimodeMenu-row row-2" key="antimodemenu row2"> + const row2 = <div className="antimodeMenu-row row-2" key="row2"> {this.collapsed ? this.getDragger() : (null)} - <div key="row" style={{ display: this.collapsed ? "none" : undefined }}> - <div className="richTextMenu-divider" />, - {[this.createMarksDropdown(this.activeFontSize, this.fontSizeOptions, "font size"), - this.createMarksDropdown(this.activeFontFamily, this.fontFamilyOptions, "font family"), - <div className="richTextMenu-divider" />, - this.createNodesDropdown(this.activeListType, this.listTypeOptions, "nodes"), + <div key="row 2" style={{ display: this.collapsed ? "none" : undefined }}> + <div className="richTextMenu-divider" key="divider 3" />, + {[this.createMarksDropdown(this.activeFontSize, this.fontSizeOptions, "font size", action((val: string) => this.activeFontSize = val)), + this.createMarksDropdown(this.activeFontFamily, this.fontFamilyOptions, "font family", action((val: string) => this.activeFontFamily = val)), + <div className="richTextMenu-divider" key="divider 4" />, + this.createNodesDropdown(this.activeListType, this.listTypeOptions, "list type", action((val: string) => this.activeListType = val)), this.createButton("sort-amount-down", "Summarize", undefined, this.insertSummarizer), this.createButton("quote-left", "Blockquote", undefined, this.insertBlockquote), this.createButton("minus", "Horizontal Rule", undefined, this.insertHorizontalRule), - <div className="richTextMenu-divider" />,]} + <div className="richTextMenu-divider" key="divider 5" />,]} </div> - <div key="button"> + <div key="collapser"> {/* <div key="collapser"> <button className="antimodeMenu-button" key="collapse menu" title="Collapse menu" onClick={this.toggleCollapse} style={{ backgroundColor: this.collapsed ? "#121212" : "", width: 25 }}> <FontAwesomeIcon icon="chevron-left" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.3s", transform: `rotate(${this.collapsed ? 180 : 0}deg)` }} /> diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index ca30dde9d..ef0fead4a 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -90,7 +90,7 @@ export class RichTextRules { textDoc.inlineTextCount = numInlines + 1; const inlineFieldKey = "inline" + numInlines; // which field on the text document this annotation will write to const inlineLayoutKey = "layout_" + inlineFieldKey; // the field holding the layout string that will render the inline annotation - const textDocInline = Docs.Create.TextDocument("", { layoutKey: inlineLayoutKey, _width: 75, _height: 35, annotationOn: textDoc, _autoHeight: true, _fontSize: 9, title: "inline comment" }); + const textDocInline = Docs.Create.TextDocument("", { layoutKey: inlineLayoutKey, _width: 75, _height: 35, annotationOn: textDoc, _autoHeight: true, _fontSize: "9pt", title: "inline comment" }); textDocInline.title = inlineFieldKey; // give the annotation its own title textDocInline.customTitle = true; // And make sure that it's 'custom' so that editing text doesn't change the title of the containing doc textDocInline.isTemplateForField = inlineFieldKey; // this is needed in case the containing text doc is converted to a template at some point diff --git a/src/client/views/nodes/formattedText/RichTextSchema.tsx b/src/client/views/nodes/formattedText/RichTextSchema.tsx index a989abd6a..33a080fe4 100644 --- a/src/client/views/nodes/formattedText/RichTextSchema.tsx +++ b/src/client/views/nodes/formattedText/RichTextSchema.tsx @@ -54,6 +54,8 @@ export class DashDocView { this._dashSpan.style.height = node.attrs.height; this._dashSpan.style.position = "absolute"; this._dashSpan.style.display = "inline-block"; + this._dashSpan.style.left = "0"; + this._dashSpan.style.top = "0"; this._dashSpan.style.whiteSpace = "normal"; this._dashSpan.onpointerleave = () => { @@ -160,9 +162,15 @@ export class DashDocView { 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" })); + if (getPos() !== undefined) { + const node = view.state.tr.doc.nodeAt(getPos()); + if (node.attrs.width !== dashDoc._width + "px" || + node.attrs.height !== dashDoc._height + "px") { + view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { ...node.attrs, width: dashDoc._width + "px", height: dashDoc._height + "px" })); + } + } } catch (e) { - console.log(e); + console.log("RichTextSchema: " + e); } } }; diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts index 3d7d71b14..f95f46104 100644 --- a/src/client/views/nodes/formattedText/marks_rts.ts +++ b/src/client/views/nodes/formattedText/marks_rts.ts @@ -258,9 +258,7 @@ export const marks: { [index: string]: MarkSpec } = { }, parseDOM: [{ style: 'background: yellow' }], toDOM(node: any) { - return ['span', { - style: `background: ${node.attrs.selected ? "orange" : "yellow"}` - }]; + return ['span', { style: `background: ${node.attrs.selected ? "orange" : "yellow"}` }]; } }, @@ -277,8 +275,8 @@ export const marks: { [index: string]: MarkSpec } = { const min = Math.round(node.attrs.modified / 12); const hr = Math.round(min / 60); const day = Math.round(hr / 60 / 24); - const remote = node.attrs.userid !== Doc.CurrentUserEmail ? " userMark-remote" : ""; - return ['span', { class: "userMark-" + uid + remote + " userMark-min-" + min + " userMark-hr-" + hr + " userMark-day-" + day }, 0]; + const remote = node.attrs.userid !== Doc.CurrentUserEmail ? " UM-remote" : ""; + return ['span', { class: "UM-" + uid + remote + " UM-min-" + min + " UM-hr-" + hr + " UM-day-" + day }, 0]; } }, // the id of the user who entered the text @@ -292,7 +290,7 @@ export const marks: { [index: string]: MarkSpec } = { inclusive: false, toDOM(node: any) { const uid = node.attrs.userid.replace(".", "").replace("@", ""); - return ['span', { class: "userTag-" + uid + " userTag-" + node.attrs.tag }, 0]; + return ['span', { class: "UT-" + uid + " UT-" + node.attrs.tag }, 0]; } }, diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx index 00c56d73e..6592c488b 100644 --- a/src/client/views/pdf/PDFMenu.tsx +++ b/src/client/views/pdf/PDFMenu.tsx @@ -47,7 +47,7 @@ export default class PDFMenu extends AntimodeMenu { public AddTag: (key: string, value: string) => boolean = returnFalse; public PinToPres: () => void = unimplementedFunction; public Marquee: { left: number; top: number; width: number; height: number; } | undefined; - public get Active() { return this._opacity ? true : false; } + public get Active() { return this._left > 0; } constructor(props: Readonly<{}>) { super(props); diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 4e5fdbfbb..30c51d9ca 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -262,7 +262,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu const eventBus = new PDFJSViewer.EventBus(true); eventBus._on("pagesinit", this.pagesinit); eventBus._on("pagerendered", action(() => this._showCover = this._showWaiting = false)); - const pdfLinkService = new PDFJSViewer.PDFLinkService(); + const pdfLinkService = new PDFJSViewer.PDFLinkService({ eventBus }); const pdfFindController = new PDFJSViewer.PDFFindController({ linkService: pdfLinkService, eventBus }); this._pdfViewer = new PDFJSViewer.PDFViewer({ container: this._mainCont.current, @@ -392,7 +392,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu @action search = (searchString: string, fwd: boolean, clear: boolean = false) => { if (clear) { - this._pdfViewer.findController.executeCommand('reset', {}); + this._pdfViewer.findController.executeCommand('reset', { query: "" }); } else if (!searchString) { fwd ? this.nextAnnotation() : this.prevAnnotation(); } else if (this._pdfViewer.pageViewsReady) { @@ -637,7 +637,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu @action onZoomWheel = (e: React.WheelEvent) => { - if (this.active()) { + if (this.active(true)) { e.stopPropagation(); if (e.ctrlKey) { const curScale = Number(this._pdfViewer.currentScaleValue); diff --git a/src/client/views/presentationview/PresElementBox.tsx b/src/client/views/presentationview/PresElementBox.tsx index 6fd3455b6..4c0972736 100644 --- a/src/client/views/presentationview/PresElementBox.tsx +++ b/src/client/views/presentationview/PresElementBox.tsx @@ -15,6 +15,7 @@ import { FieldView, FieldViewProps } from '../nodes/FieldView'; import "./PresElementBox.scss"; import React = require("react"); import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; +import { DocumentType } from "../../documents/DocumentTypes"; export const presSchema = createSchema({ presentationTargetDoc: Doc, @@ -100,7 +101,8 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc e.stopPropagation(); this.rootDoc.presProgressivize = !this.rootDoc.presProgressivize; const rootTarget = Cast(this.rootDoc.presentationTargetDoc, Doc, null); - const docs = DocListCast(rootTarget[Doc.LayoutFieldKey(rootTarget)]); + const docs = rootTarget.type === DocumentType.COL ? DocListCast(rootTarget[Doc.LayoutFieldKey(rootTarget)]) : + DocListCast(rootTarget[Doc.LayoutFieldKey(rootTarget) + "-annotations"]); if (this.rootDoc.presProgressivize) { rootTarget.currentFrame = 0; CollectionFreeFormDocumentView.setupKeyframes(docs, docs.length, true); diff --git a/src/client/views/search/FilterBox.tsx b/src/client/views/search/FilterBox.tsx index 4b53963a5..eb61f9a14 100644 --- a/src/client/views/search/FilterBox.tsx +++ b/src/client/views/search/FilterBox.tsx @@ -352,7 +352,6 @@ export class FilterBox extends React.Component { getDataStatus() { return this._deletedDocsStatus; } getActiveFilters() { - console.log(this._authorFieldStatus, this._titleFieldStatus, this._dataFieldStatus); return ( <div className="active-filters"> {!this._basicWordStatus ? <div className="active-icon container"> diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index c9d29e485..99fa6da21 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -125,7 +125,7 @@ export class SearchBox extends React.Component<SearchProps> { return returnedUri; } catch (e) { - console.log(e); + console.log("SearchBox:" + e); } } @@ -582,7 +582,6 @@ export class SearchBox extends React.Component<SearchProps> { } expandSection(thing: string) { - console.log("expand"); const element = document.getElementById(thing)!; // get the height of the element's inner content, regardless of its actual size const sectionHeight = element.scrollHeight; @@ -593,7 +592,6 @@ export class SearchBox extends React.Component<SearchProps> { // when the next css transition finishes (which should be the one we just triggered) element.addEventListener('transitionend', function handler(e) { // remove this event listener so it only gets triggered once - console.log("autoset"); element.removeEventListener('transitionend', handler); // remove "height" from the element's inline styles, so it can return to its initial value @@ -608,7 +606,6 @@ export class SearchBox extends React.Component<SearchProps> { autoset(thing: string) { const element = document.getElementById(thing)!; - console.log("autoset"); element.removeEventListener('transitionend', function (e) { }); // remove "height" from the element's inline styles, so it can return to its initial value diff --git a/src/client/views/search/ToggleBar.tsx b/src/client/views/search/ToggleBar.tsx index e4d7f2fd5..466822eba 100644 --- a/src/client/views/search/ToggleBar.tsx +++ b/src/client/views/search/ToggleBar.tsx @@ -58,7 +58,6 @@ export class ToggleBar extends React.Component<ToggleBarProps>{ this._forwardTimeline.play(); this._forwardTimeline.reverse(); this.props.handleChange(); - console.log(this.props.getStatus()); } @action.bound diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 7aa1d528d..7e91a7761 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -1,6 +1,6 @@ -import { action, computed, observable, ObservableMap, runInAction } from "mobx"; +import { action, computed, observable, ObservableMap, runInAction, untracked } from "mobx"; import { computedFn } from "mobx-utils"; -import { alias, map, serializable } from "serializr"; +import { alias, map, serializable, list } from "serializr"; import { DocServer } from "../client/DocServer"; import { DocumentType } from "../client/documents/DocumentTypes"; import { Scripting, scriptingGlobal } from "../client/util/Scripting"; @@ -14,11 +14,16 @@ import { ObjectField } from "./ObjectField"; import { PrefetchProxy, ProxyField } from "./Proxy"; import { FieldId, RefField } from "./RefField"; import { RichTextField } from "./RichTextField"; +import { ImageField, VideoField, WebField, AudioField, PdfField } from "./URLField"; +import { DateField } from "./DateField"; import { listSpec } from "./Schema"; import { ComputedField } from "./ScriptField"; import { Cast, FieldValue, NumCast, StrCast, ToConstructor } from "./Types"; -import { deleteProperty, getField, getter, makeEditable, makeReadOnly, setter, updateFunction } from "./util"; +import { deleteProperty, getField, getter, makeEditable, makeReadOnly, setter, updateFunction, GetEffectiveAcl } from "./util"; import { LinkManager } from "../client/util/LinkManager"; +import { SharingPermissions } from "../client/util/SharingManager"; +import JSZip = require("jszip"); +import { saveAs } from "file-saver"; export namespace Field { export function toKeyValueString(doc: Doc, key: string): string { @@ -92,32 +97,32 @@ export const WidthSym = Symbol("Width"); export const HeightSym = Symbol("Height"); export const DataSym = Symbol("Data"); export const LayoutSym = Symbol("Layout"); +export const FieldsSym = Symbol("Fields"); export const AclSym = Symbol("Acl"); export const AclPrivate = Symbol("AclOwnerOnly"); export const AclReadonly = Symbol("AclReadOnly"); export const AclAddonly = Symbol("AclAddonly"); -export const AclReadWrite = Symbol("AclReadWrite"); +export const AclEdit = Symbol("AclEdit"); export const UpdatingFromServer = Symbol("UpdatingFromServer"); const CachedUpdates = Symbol("Cached updates"); +const AclMap = new Map<string, symbol>([ + [SharingPermissions.None, AclPrivate], + [SharingPermissions.View, AclReadonly], + [SharingPermissions.Add, AclAddonly], + [SharingPermissions.Edit, AclEdit] +]); export function fetchProto(doc: Doc) { - if (doc.author !== Doc.CurrentUserEmail) { - const acl = Doc.Get(doc, "ACL", true); - switch (acl) { - case "ownerOnly": - doc[AclSym] = AclPrivate; - return undefined; - case "readOnly": - doc[AclSym] = AclReadonly; - break; - case "addOnly": - doc[AclSym] = AclAddonly; - break; - case "write": - doc[AclSym] = AclReadWrite; - } - } + // if (doc.author !== Doc.CurrentUserEmail) { + untracked(() => { + const permissions: { [key: string]: symbol } = {}; + + Object.keys(doc).filter(key => key.startsWith("ACL")).forEach(key => permissions[key] = AclMap.get(StrCast(doc[key]))!); + + if (Object.keys(permissions).length) doc[AclSym] = permissions; + }); + // } if (doc.proto instanceof Promise) { doc.proto.then(fetchProto); @@ -134,10 +139,10 @@ export class Doc extends RefField { set: setter, get: getter, // getPrototypeOf: (target) => Cast(target[SelfProxy].proto, Doc) || null, // TODO this might be able to replace the proto logic in getter - has: (target, key) => target[AclSym] !== AclPrivate && key in target.__fields, + has: (target, key) => GetEffectiveAcl(target) !== AclPrivate && key in target.__fields, ownKeys: target => { const obj = {} as any; - if (target[AclSym] !== AclPrivate) Object.assign(obj, target.___fields); + if (GetEffectiveAcl(target) !== AclPrivate) Object.assign(obj, target.___fields); runInAction(() => obj.__LAYOUT__ = target.__LAYOUT__); return Object.keys(obj); }, @@ -180,7 +185,6 @@ export class Doc extends RefField { } @observable - //{ [key: string]: Field | FieldWaiting | undefined } private ___fields: any = {}; private [UpdatingFromServer]: boolean = false; @@ -191,11 +195,12 @@ export class Doc extends RefField { private [Self] = this; private [SelfProxy]: any; - public [AclSym]: any = undefined; + public [FieldsSym] = () => this.___fields; + public [AclSym]: { [key: string]: symbol }; public [WidthSym] = () => NumCast(this[SelfProxy]._width); public [HeightSym] = () => NumCast(this[SelfProxy]._height); public [ToScriptString]() { return `DOC-"${this[Self][Id]}"-`; } - public [ToString]() { return `Doc(${this[AclSym] === AclPrivate ? "-inaccessible-" : this.title})`; } + public [ToString]() { return `Doc(${GetEffectiveAcl(this) === AclPrivate ? "-inaccessible-" : this.title})`; } public get [LayoutSym]() { return this[SelfProxy].__LAYOUT__; } public get [DataSym]() { const self = this[SelfProxy]; @@ -215,8 +220,8 @@ export class Doc extends RefField { return Cast(this[SelfProxy][renderFieldKey + "-layout[" + templateLayoutDoc[Id] + "]"], Doc, null) || templateLayoutDoc; } return undefined; - } + } private [CachedUpdates]: { [key: string]: () => void | Promise<any> } = {}; public static CurrentUserEmail: string = ""; @@ -483,27 +488,28 @@ export namespace Doc { return alias; } - - - export function makeClone(doc: Doc, cloneMap: Map<string, Doc>, rtfs: { copy: Doc, key: string, field: RichTextField }[]): Doc { + export async function makeClone(doc: Doc, cloneMap: Map<string, Doc>, rtfs: { copy: Doc, key: string, field: RichTextField }[], exclusions: string[], dontCreate: boolean): Promise<Doc> { if (Doc.IsBaseProto(doc)) return doc; if (cloneMap.get(doc[Id])) return cloneMap.get(doc[Id])!; - const copy = new Doc(undefined, true); + const copy = dontCreate ? doc : new Doc(undefined, true); cloneMap.set(doc[Id], copy); if (LinkManager.Instance.getAllLinks().includes(doc) && LinkManager.Instance.getAllLinks().indexOf(copy) === -1) LinkManager.Instance.addLink(copy); - const exclude = Cast(doc.excludeFields, listSpec("string"), []); - Object.keys(doc).forEach(key => { - if (exclude.includes(key)) return; + const filter = Cast(doc.cloneFieldFilter, listSpec("string"), exclusions); + await Promise.all(Object.keys(doc).map(async key => { + if (filter.includes(key)) return; + const assignKey = (val: any) => !dontCreate && (copy[key] = val); const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); const field = ProxyField.WithoutProxy(() => doc[key]); - const copyObjectField = (field: ObjectField) => { - const list = Cast(doc[key], listSpec(Doc)); - if (list !== undefined && !(list instanceof Promise)) { - copy[key] = new List<Doc>(list.filter(d => d instanceof Doc).map(d => Doc.makeClone(d as Doc, cloneMap, rtfs))); + const copyObjectField = async (field: ObjectField) => { + const list = await Cast(doc[key], listSpec(Doc)); + const docs = list && (await DocListCastAsync(list))?.filter(d => d instanceof Doc); + if (docs !== undefined && docs.length) { + const clones = await Promise.all(docs.map(async d => await Doc.makeClone(d as Doc, cloneMap, rtfs, exclusions, dontCreate))); + !dontCreate && assignKey(new List<Doc>(clones)); } else if (doc[key] instanceof Doc) { - copy[key] = key.includes("layout[") ? undefined : Doc.makeClone(doc[key] as Doc, cloneMap, rtfs); // reference documents except copy documents that are expanded teplate fields + assignKey(key.includes("layout[") ? undefined : key.startsWith("layout") ? doc[key] as Doc : await Doc.makeClone(doc[key] as Doc, cloneMap, rtfs, exclusions, dontCreate)); // reference documents except copy documents that are expanded teplate fields } else { - copy[key] = ObjectField.MakeCopy(field); + assignKey(ObjectField.MakeCopy(field)); if (field instanceof RichTextField) { if (field.Data.includes('"docid":') || field.Data.includes('"targetId":') || field.Data.includes('"linkId":')) { rtfs.push({ copy, key, field }); @@ -513,32 +519,34 @@ export namespace Doc { }; if (key === "proto") { if (doc[key] instanceof Doc) { - copy[key] = Doc.makeClone(doc[key]!, cloneMap, rtfs); + assignKey(await Doc.makeClone(doc[key]!, cloneMap, rtfs, exclusions, dontCreate)); } } else { if (field instanceof RefField) { - copy[key] = field; + assignKey(field); } else if (cfield instanceof ComputedField) { - copy[key] = ComputedField.MakeFunction(cfield.script.originalScript); - (key === "links" && field instanceof ObjectField) && copyObjectField(field); + !dontCreate && assignKey(ComputedField.MakeFunction(cfield.script.originalScript)); + (key === "links" && field instanceof ObjectField) && await copyObjectField(field); } else if (field instanceof ObjectField) { - copyObjectField(field); + await copyObjectField(field); } else if (field instanceof Promise) { debugger; //This shouldn't happend... } else { - copy[key] = field; + assignKey(field); } } - }); - Doc.SetInPlace(copy, "title", "CLONE: " + doc.title, true); - copy.cloneOf = doc; - cloneMap.set(doc[Id], copy); + })); + if (!dontCreate) { + Doc.SetInPlace(copy, "title", "CLONE: " + doc.title, true); + copy.cloneOf = doc; + cloneMap.set(doc[Id], copy); + } return copy; } - export function MakeClone(doc: Doc): Doc { + export async function MakeClone(doc: Doc, dontCreate: boolean = false) { const cloneMap = new Map<string, Doc>(); const rtfMap: { copy: Doc, key: string, field: RichTextField }[] = []; - const copy = Doc.makeClone(doc, cloneMap, rtfMap); + const copy = await Doc.makeClone(doc, cloneMap, rtfMap, ["context", "annotationOn", "cloneOf"], dontCreate); rtfMap.map(({ copy, key, field }) => { const replacer = (match: any, attr: string, id: string, offset: any, string: any) => { const mapped = cloneMap.get(id); @@ -552,9 +560,55 @@ export namespace Doc { const re = new RegExp(regex, "g"); copy[key] = new RichTextField(field.Data.replace(/("docid":|"targetId":|"linkId":)"([^"]+)"/g, replacer).replace(re, replacer2), field.Text); }); - return copy; + return { clone: copy, map: cloneMap }; } + export async function Zip(doc: Doc) { + const { clone, map } = await Doc.MakeClone(doc, true); + function replacer(key: any, value: any) { + if (["cloneOf", "context", "cursors"].includes(key)) return undefined; + else if (value instanceof Doc) { + if (key !== "field" && Number.isNaN(Number(key))) { + const __fields = value[FieldsSym](); + return { id: value[Id], __type: "Doc", fields: __fields }; + } else { + return { fieldId: value[Id], __type: "proxy" }; + } + } + else if (value instanceof RichTextField) return { Data: value.Data, Text: value.Text, __type: "RichTextField" }; + else if (value instanceof ImageField) return { url: value.url.href, __type: "image" }; + else if (value instanceof PdfField) return { url: value.url.href, __type: "pdf" }; + else if (value instanceof AudioField) return { url: value.url.href, __type: "audio" }; + else if (value instanceof VideoField) return { url: value.url.href, __type: "video" }; + else if (value instanceof WebField) return { url: value.url.href, __type: "web" }; + else if (value instanceof DateField) return { date: value.toString(), __type: "date" }; + else if (value instanceof ProxyField) return { fieldId: value.fieldId, __type: "proxy" }; + else if (value instanceof Array && key !== "fields") return { fields: value, __type: "list" }; + else if (value instanceof ComputedField) return { script: value.script, __type: "computed" }; + else return value; + } + + const docs: { [id: string]: any } = {}; + Array.from(map.entries()).forEach(f => docs[f[0]] = f[1]); + const docString = JSON.stringify({ id: doc[Id], docs }, replacer); + + var zip = new JSZip(); + + zip.file("doc.json", docString); + + // // Generate a directory within the Zip file structure + // var img = zip.folder("images"); + + // // Add a file to the directory, in this case an image with data URI as contents + // img.file("smile.gif", imgData, {base64: true}); + + // Generate the zip file asynchronously + zip.generateAsync({ type: "blob" }) + .then((content: any) => { + // Force down of the Zip file + saveAs(content, "download.zip"); + }); + } // // Determines whether the layout needs to be expanded (as a template). // template expansion is rquired when the layout is a template doc/field and there's a datadoc which isn't equal to the layout template @@ -657,7 +711,7 @@ export namespace Doc { export function MakeCopy(doc: Doc, copyProto: boolean = false, copyProtoId?: string): Doc { const copy = new Doc(copyProtoId, true); - const exclude = Cast(doc.excludeFields, listSpec("string"), []); + const exclude = Cast(doc.cloneFieldFilter, listSpec("string"), []); Object.keys(doc).forEach(key => { if (exclude.includes(key)) return; const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); @@ -823,7 +877,7 @@ export namespace Doc { } // don't bother memoizing (caching) the result if called from a non-reactive context. (plus this avoids a warning message) export function IsBrushedDegreeUnmemoized(doc: Doc) { - if (!doc || doc[AclSym] === AclPrivate || Doc.GetProto(doc)[AclSym] === AclPrivate) return 0; + if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return 0; return brushManager.BrushedDoc.has(doc) ? 2 : brushManager.BrushedDoc.has(Doc.GetProto(doc)) ? 1 : 0; } export function IsBrushedDegree(doc: Doc) { @@ -832,15 +886,14 @@ export namespace Doc { })(doc); } export function BrushDoc(doc: Doc) { - - if (!doc || doc[AclSym] === AclPrivate || Doc.GetProto(doc)[AclSym] === AclPrivate) return doc; + if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return doc; brushManager.BrushedDoc.set(doc, true); brushManager.BrushedDoc.set(Doc.GetProto(doc), true); return doc; } export function UnBrushDoc(doc: Doc) { - if (!doc || doc[AclSym] === AclPrivate || Doc.GetProto(doc)[AclSym] === AclPrivate) return doc; + if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return doc; brushManager.BrushedDoc.delete(doc); brushManager.BrushedDoc.delete(Doc.GetProto(doc)); return doc; @@ -870,7 +923,7 @@ export namespace Doc { } const highlightManager = new HighlightBrush(); export function IsHighlighted(doc: Doc) { - if (!doc || doc[AclSym] === AclPrivate || Doc.GetProto(doc)[AclSym] === AclPrivate) return false; + if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return false; return highlightManager.HighlightedDoc.get(doc) || highlightManager.HighlightedDoc.get(Doc.GetProto(doc)); } export function HighlightDoc(doc: Doc, dataAndDisplayDocs = true) { @@ -899,9 +952,12 @@ export namespace Doc { } export function getDocTemplate(doc?: Doc) { - return doc?.isTemplateDoc ? doc : - Cast(doc?.dragFactory, Doc, null)?.isTemplateDoc ? doc?.dragFactory : - Cast(doc?.layout, Doc, null)?.isTemplateDoc ? doc?.layout : undefined; + return !doc ? undefined : + doc.isTemplateDoc ? doc : + Cast(doc.dragFactory, Doc, null)?.isTemplateDoc ? doc.dragFactory : + Cast(Doc.Layout(doc), Doc, null)?.isTemplateDoc ? + (Cast(Doc.Layout(doc), Doc, null).resolvedDataDoc ? Doc.Layout(doc).proto : Doc.Layout(doc)) : + undefined; } export function matchFieldValue(doc: Doc, key: string, value: any): boolean { diff --git a/src/fields/Proxy.ts b/src/fields/Proxy.ts index 555faaad0..62734d3d2 100644 --- a/src/fields/Proxy.ts +++ b/src/fields/Proxy.ts @@ -9,7 +9,12 @@ import { Id, Copy, ToScriptString, ToString } from "./FieldSymbols"; import { scriptingGlobal } from "../client/util/Scripting"; import { Plugins } from "./util"; -@Deserializable("proxy") +function deserializeProxy(field: any) { + if (!field.cache) { + field.cache = DocServer.GetCachedRefField(field.fieldId) as any; + } +} +@Deserializable("proxy", deserializeProxy) export class ProxyField<T extends RefField> extends ObjectField { constructor(); constructor(value: T); @@ -17,6 +22,7 @@ export class ProxyField<T extends RefField> extends ObjectField { constructor(value?: T | string) { super(); if (typeof value === "string") { + this.cache = DocServer.GetCachedRefField(value) as any; this.fieldId = value; } else if (value) { this.cache = value; diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts index 11b3b0524..bd08b2f32 100644 --- a/src/fields/ScriptField.ts +++ b/src/fields/ScriptField.ts @@ -3,7 +3,7 @@ import { CompiledScript, CompileScript, scriptingGlobal, ScriptOptions, CompileE import { Copy, ToScriptString, ToString, Parent, SelfProxy } from "./FieldSymbols"; import { serializable, createSimpleSchema, map, primitive, object, deserialize, PropSchema, custom, SKIP } from "serializr"; import { Deserializable, autoObject } from "../client/util/SerializationHelper"; -import { Doc, Field } from "./Doc"; +import { Doc, Field, Opt } from "./Doc"; import { Plugins, setter } from "./util"; import { computedFn } from "mobx-utils"; import { ProxyField } from "./Proxy"; @@ -38,6 +38,21 @@ const scriptSchema = createSimpleSchema({ }); async function deserializeScript(script: ScriptField) { + if (script.script.originalScript === 'getCopy(this.dragFactory, true)') { + return (script as any).script = (ScriptField.GetCopyOfDragFactory ?? (ScriptField.GetCopyOfDragFactory = ScriptField.MakeFunction('getCopy(this.dragFactory, true)')))?.script; + } + if (script.script.originalScript === 'links(self)') { + return (script as any).script = (ScriptField.LinksSelf ?? (ScriptField.LinksSelf = ComputedField.MakeFunction('links(self)')))?.script; + } + if (script.script.originalScript === 'openOnRight(getCopy(this.dragFactory, true))') { + return (script as any).script = (ScriptField.OpenOnRight ?? (ScriptField.OpenOnRight = ComputedField.MakeFunction('openOnRight(getCopy(this.dragFactory, true))')))?.script; + } + if (script.script.originalScript === 'deiconifyView(self)') { + return (script as any).script = (ScriptField.DeiconifyView ?? (ScriptField.DeiconifyView = ComputedField.MakeFunction('deiconifyView(self)')))?.script; + } + if (script.script.originalScript === 'convertToButtons(dragData)') { + return (script as any).script = (ScriptField.ConvertToButtons ?? (ScriptField.ConvertToButtons = ComputedField.MakeFunction('convertToButtons(dragData)', { dragData: "DocumentDragData" })))?.script; + } const captures: ProxyField<Doc> = (script as any).captures; if (captures) { const doc = (await captures.value())!; @@ -65,6 +80,11 @@ export class ScriptField extends ObjectField { @serializable(autoObject()) private captures?: ProxyField<Doc>; + public static GetCopyOfDragFactory: Opt<ScriptField>; + public static LinksSelf: Opt<ScriptField>; + public static OpenOnRight: Opt<ScriptField>; + public static DeiconifyView: Opt<ScriptField>; + public static ConvertToButtons: Opt<ScriptField>; constructor(script: CompiledScript, setterscript?: CompiledScript) { super(); diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts index 97f62c9d4..ddffb56c3 100644 --- a/src/fields/documentSchemas.ts +++ b/src/fields/documentSchemas.ts @@ -3,7 +3,6 @@ import { ScriptField } from "./ScriptField"; import { Doc } from "./Doc"; import { DateField } from "./DateField"; import { SchemaHeaderField } from "./SchemaHeaderField"; -import { Schema } from "prosemirror-model"; export const documentSchema = createSchema({ // content properties @@ -55,7 +54,7 @@ export const documentSchema = createSchema({ _columnsHideIfEmpty: "boolean", // whether empty stacking view column headings should be hidden _columnHeaders: listSpec(SchemaHeaderField), // header descriptions for stacking/masonry _schemaHeaders: listSpec(SchemaHeaderField), // header descriptions for schema views - _fontSize: "number", + _fontSize: "string", _fontFamily: "string", _sidebarWidthPercent: "string", // percent of text window width taken up by sidebar @@ -66,6 +65,7 @@ export const documentSchema = createSchema({ color: "string", // foreground color of document fitToBox: "boolean", // whether freeform view contents should be zoomed/panned to fill the area of the document view fontSize: "string", + hidden: "boolean", // whether a document should not be displayed isInkMask: "boolean", // is the document a mask (ie, sits on top of other documents, has an unbounded width/height that is dark, and content uses 'hard-light' mix-blend-mode to let other documents pop through) layout: "string", // this is the native layout string for the document. templates can be added using other fields and setting layoutKey below layoutKey: "string", // holds the field key for the field that actually holds the current lyoat @@ -73,6 +73,9 @@ export const documentSchema = createSchema({ opacity: "number", // opacity of document strokeWidth: "number", strokeBezier: "number", + strokeStartMarker: "string", + strokeEndMarker: "string", + strokeDash: "string", textTransform: "string", treeViewOpen: "boolean", // flag denoting whether the documents sub-tree (contents) is visible or hidden treeViewExpandedView: "string", // name of field whose contents are being displayed as the document's subtree @@ -85,6 +88,8 @@ export const documentSchema = createSchema({ onPointerUp: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop) onDragStart: ScriptField, // script to run when document is dragged (without being selected). the script should return the Doc to be dropped. followLinkLocation: "string",// flag for where to place content when following a click interaction (e.g., onRight, inPlace, inTab, ) + hideLinkButton: "boolean", // whether the blue link counter button should be hidden + hideAllLinks: "boolean", // whether all individual blue anchor dots should be hidden isInPlaceContainer: "boolean",// whether the marked object will display addDocTab() calls that target "inPlace" destinations isLinkButton: "boolean", // whether document functions as a link follow button to follow the first link on the document when clicked isBackground: "boolean", // whether document is a background element and ignores input events (can only select with marquee) diff --git a/src/fields/util.ts b/src/fields/util.ts index 2dc21c987..ef66d9633 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -1,5 +1,5 @@ import { UndoManager } from "../client/util/UndoManager"; -import { Doc, Field, FieldResult, UpdatingFromServer, LayoutSym, AclSym, AclPrivate } from "./Doc"; +import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAddonly, AclSym, fetchProto, DataSym, DocListCast } from "./Doc"; import { SerializationHelper } from "../client/util/SerializationHelper"; import { ProxyField, PrefetchProxy } from "./Proxy"; import { RefField } from "./RefField"; @@ -8,7 +8,8 @@ import { action, trace } from "mobx"; import { Parent, OnUpdate, Update, Id, SelfProxy, Self } from "./FieldSymbols"; import { DocServer } from "../client/DocServer"; import { ComputedField } from "./ScriptField"; -import { ScriptCast } from "./Types"; +import { ScriptCast, StrCast } from "./Types"; +import { SharingPermissions } from "../client/util/SharingManager"; function _readOnlySetter(): never { @@ -34,7 +35,6 @@ export namespace Plugins { } const _setterImpl = action(function (target: any, prop: string | symbol | number, value: any, receiver: any): boolean { - //console.log("-set " + target[SelfProxy].title + "(" + target[SelfProxy][prop] + ")." + prop.toString() + " = " + value); if (SerializationHelper.IsSerializing()) { target[prop] = value; return true; @@ -70,8 +70,9 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number const writeMode = DocServer.getFieldWriteMode(prop as string); const fromServer = target[UpdatingFromServer]; const sameAuthor = fromServer || (receiver.author === Doc.CurrentUserEmail); - const writeToDoc = sameAuthor || (writeMode !== DocServer.WriteMode.LiveReadonly); - const writeToServer = sameAuthor || (writeMode === DocServer.WriteMode.Default); + const writeToDoc = sameAuthor || GetEffectiveAcl(target) === AclEdit || (writeMode !== DocServer.WriteMode.LiveReadonly); + const writeToServer = (sameAuthor || GetEffectiveAcl(target) === AclEdit || writeMode === DocServer.WriteMode.Default) && !playgroundMode; + if (writeToDoc) { if (value === undefined) { delete target.__fields[prop]; @@ -79,6 +80,7 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number target.__fields[prop] = value; } //if (typeof value === "object" && !(value instanceof ObjectField)) debugger; + if (writeToServer) { if (value === undefined) target[Update]({ '$unset': { ["fields." + prop]: "" } }); else target[Update]({ '$set': { ["fields." + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) : (value === undefined ? null : value) } }); @@ -89,8 +91,9 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number redo: () => receiver[prop] = value, undo: () => receiver[prop] = curValue }); + return true; } - return true; + return false; }); let _setter: (target: any, prop: string | symbol | number, value: any, receiver: any) => boolean = _setterImpl; @@ -107,11 +110,107 @@ export function OVERRIDE_ACL(val: boolean) { _overrideAcl = val; } +let playgroundMode = false; + +export function togglePlaygroundMode() { + playgroundMode = !playgroundMode; +} + +export function getPlaygroundMode() { + return playgroundMode; +} + +let currentUserGroups: string[] = []; + +export function setGroups(groups: string[]) { + currentUserGroups = groups; +} + +export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number): symbol { + if (in_prop === UpdatingFromServer || target[UpdatingFromServer]) return AclEdit; + + if (target[AclSym] && Object.keys(target[AclSym]).length) { + + if (target.__fields?.author === Doc.CurrentUserEmail || target.author === Doc.CurrentUserEmail || currentUserGroups.includes("admin")) return AclEdit; + + if (_overrideAcl || (in_prop && DocServer.PlaygroundFields?.includes(in_prop.toString()))) return AclEdit; + + let effectiveAcl = AclPrivate; + let aclPresent = false; + + const HierarchyMapping = new Map<symbol, number>([ + [AclPrivate, 0], + [AclReadonly, 1], + [AclAddonly, 2], + [AclEdit, 3] + ]); + + for (const [key, value] of Object.entries(target[AclSym])) { + if (currentUserGroups.includes(key.substring(4)) || Doc.CurrentUserEmail === key.substring(4).replace("_", ".")) { + if (HierarchyMapping.get(value as symbol)! >= HierarchyMapping.get(effectiveAcl)!) { + aclPresent = true; + effectiveAcl = value as symbol; + if (effectiveAcl === AclEdit) break; + } + } + } + return aclPresent ? effectiveAcl : AclEdit; + } + return AclEdit; +} + +export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, inheritingFromCollection?: boolean) { + + const HierarchyMapping = new Map<string, number>([ + ["Not Shared", 0], + ["Can View", 1], + ["Can Add", 2], + ["Can Edit", 3] + ]); + + const dataDoc = target[DataSym]; + + if (!inheritingFromCollection || !target[key] || HierarchyMapping.get(StrCast(target[key]))! > HierarchyMapping.get(acl)!) target[key] = acl; + + if (dataDoc && (!inheritingFromCollection || !dataDoc[key] || HierarchyMapping.get(StrCast(dataDoc[key]))! > HierarchyMapping.get(acl)!)) { + dataDoc[key] = acl; + + DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc)]).map(d => { + if (d.author === Doc.CurrentUserEmail && (!inheritingFromCollection || !d[key] || HierarchyMapping.get(StrCast(d[key]))! > HierarchyMapping.get(acl)!)) { + distributeAcls(key, acl, d); + d[key] = acl; + } + const data = d[DataSym]; + if (data && data.author === Doc.CurrentUserEmail && (!inheritingFromCollection || !data[key] || HierarchyMapping.get(StrCast(data[key]))! > HierarchyMapping.get(acl)!)) { + distributeAcls(key, acl, data); + data[key] = acl; + } + }); + + DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + "-annotations"]).map(d => { + if (d.author === Doc.CurrentUserEmail && (!inheritingFromCollection || !d[key] || HierarchyMapping.get(StrCast(d[key]))! > HierarchyMapping.get(acl)!)) { + distributeAcls(key, acl, d); + d[key] = acl; + } + const data = d[DataSym]; + if (data && data.author === Doc.CurrentUserEmail && (!inheritingFromCollection || !data[key] || HierarchyMapping.get(StrCast(data[key]))! > HierarchyMapping.get(acl)!)) { + distributeAcls(key, acl, data); + data[key] = acl; + } + }); + } +} + const layoutProps = ["panX", "panY", "width", "height", "nativeWidth", "nativeHeight", "fitWidth", "fitToBox", "chromeStatus", "viewType", "gridGap", "xMargin", "yMargin", "autoHeight"]; export function setter(target: any, in_prop: string | symbol | number, value: any, receiver: any): boolean { let prop = in_prop; - if (target[AclSym] && !_overrideAcl && !DocServer.PlaygroundFields.includes(in_prop.toString())) return true; + if (GetEffectiveAcl(target, in_prop) !== AclEdit) { + return true; + } + + if (typeof prop === "string" && prop.startsWith("ACL") && !["Can Edit", "Can Add", "Can View", "Not Shared", undefined].includes(value)) return true; + if (typeof prop === "string" && prop !== "__id" && prop !== "__fields" && (prop.startsWith("_") || layoutProps.includes(prop))) { if (!prop.startsWith("_")) { console.log(prop + " is deprecated - switch to _" + prop); @@ -131,7 +230,7 @@ export function setter(target: any, in_prop: string | symbol | number, value: an export function getter(target: any, in_prop: string | symbol | number, receiver: any): any { let prop = in_prop; if (in_prop === AclSym) return _overrideAcl ? undefined : target[AclSym]; - if (target[AclSym] === AclPrivate && !_overrideAcl) return undefined; + if (GetEffectiveAcl(target) === AclPrivate && !_overrideAcl) return undefined; if (prop === LayoutSym) { return target.__LAYOUT__; } @@ -168,7 +267,7 @@ function getFieldImpl(target: any, prop: string | number, receiver: any, ignoreP } if (field === undefined && !ignoreProto && prop !== "proto") { const proto = getFieldImpl(target, "proto", receiver, true);//TODO tfs: instead of receiver we could use target[SelfProxy]... I don't which semantics we want or if it really matters - if (proto instanceof Doc && proto[AclSym] !== AclPrivate) { + if (proto instanceof Doc && GetEffectiveAcl(proto) !== AclPrivate) { return getFieldImpl(proto[Self], prop, receiver, ignoreProto); } return undefined; diff --git a/src/mobile/AudioUpload.tsx b/src/mobile/AudioUpload.tsx index 590bf8f9d..b75102e43 100644 --- a/src/mobile/AudioUpload.tsx +++ b/src/mobile/AudioUpload.tsx @@ -143,6 +143,7 @@ export class AudioUpload extends React.Component { interactive={true} dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity} overlayDisplayedOpacity={this.overlayOpacity} + closeOnExternalClick={this.closeUpload} /> ); } diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx index 5ea626d52..c6420bab5 100644 --- a/src/mobile/ImageUpload.tsx +++ b/src/mobile/ImageUpload.tsx @@ -55,7 +55,7 @@ export class Uploader extends React.Component<ImageUploadProps> { doc = Docs.Create.VideoDocument(path, { _nativeWidth: 400, _width: 400, title: name }); // Case 2: File is a PDF document } else if (file.type === "application/pdf") { - doc = Docs.Create.PdfDocument(path, { _nativeWidth: 400, _width: 400, title: name }); + doc = Docs.Create.PdfDocument(path, { _nativeWidth: 400, _width: 400, _fitWidth: true, title: name }); // Case 3: File is another document type (most likely Image) } else { doc = Docs.Create.ImageDocument(path, { _nativeWidth: 400, _width: 400, title: name }); @@ -172,6 +172,7 @@ export class Uploader extends React.Component<ImageUploadProps> { interactive={true} dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity} overlayDisplayedOpacity={this.overlayOpacity} + closeOnExternalClick={this.closeUpload} /> ); } diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx index fafa94a11..3a889b0db 100644 --- a/src/mobile/MobileInterface.tsx +++ b/src/mobile/MobileInterface.tsx @@ -28,7 +28,6 @@ import { DockedFrameRenderer } from '../client/views/collections/CollectionDocki import { InkTool } from '../fields/InkField'; import GestureOverlay from "../client/views/GestureOverlay"; import { ScriptField } from "../fields/ScriptField"; -import InkOptionsMenu from "../client/views/collections/collectionFreeForm/InkOptionsMenu"; import { RadialMenu } from "../client/views/nodes/RadialMenu"; import { UndoManager } from "../client/util/UndoManager"; import { List } from "../fields/List"; @@ -38,6 +37,7 @@ import RichTextMenu from "../client/views/nodes/formattedText/RichTextMenu"; import { AudioBox } from "../client/views/nodes/AudioBox"; import { CollectionViewType } from "../client/views/collections/CollectionView"; import { DocumentType } from "../client/documents/DocumentTypes"; +import { CollectionFreeFormViewChrome } from "../client/views/collections/CollectionMenu"; library.add(faTasks, faReply, faQuoteLeft, faHandPointLeft, faFolderOpen, faAngleDoubleLeft, faExternalLinkSquareAlt, faMobile, faThLarge, faWindowClose, faEdit, faTrashAlt, faPalette, faAngleRight, faBell, faTrash, faCamera, faExpand, faCaretDown, faCaretLeft, faCaretRight, faCaretSquareDown, faCaretSquareRight, faArrowsAltH, faPlus, faMinus, faTerminal, faToggleOn, fileSolid, faExternalLinkAlt, faLocationArrow, faSearch, faFileDownload, faStop, faCalculator, faWindowMaximize, faAddressCard, @@ -429,7 +429,7 @@ export class MobileInterface extends React.Component { @computed get inkMenu() { return this._activeDoc._viewType !== CollectionViewType.Docking || !this._ink ? (null) : <div className="colorSelector"> - <InkOptionsMenu /> + {/* <CollectionFreeFormViewChrome /> */} </div>; } diff --git a/src/server/ApiManagers/DownloadManager.ts b/src/server/ApiManagers/DownloadManager.ts index c5f3ca717..0d4472fdc 100644 --- a/src/server/ApiManagers/DownloadManager.ts +++ b/src/server/ApiManagers/DownloadManager.ts @@ -80,20 +80,14 @@ async function getDocs(id: string) { } const ids: string[] = []; for (const key in doc.fields) { - if (!doc.fields.hasOwnProperty(key)) { - continue; - } + if (!doc.fields.hasOwnProperty(key)) { continue; } const field = doc.fields[key]; - if (field === undefined || field === null) { - continue; - } + if (field === undefined || field === null) { continue; } if (field.__type === "proxy" || field.__type === "prefetch_proxy") { ids.push(field.fieldId); } else if (field.__type === "script" || field.__type === "computed") { - if (field.captures) { - ids.push(field.captures.fieldId); - } + field.captures && ids.push(field.captures.fieldId); } else if (field.__type === "list") { ids.push(...fn(field)); } else if (typeof field === "string") { diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts index 55ceab9fb..4455d11eb 100644 --- a/src/server/ApiManagers/UploadManager.ts +++ b/src/server/ApiManagers/UploadManager.ts @@ -16,6 +16,7 @@ const imageDataUri = require('image-data-uri'); import { isWebUri } from "valid-url"; import { launch } from "puppeteer"; import { Opt } from "../../fields/Doc"; +import { SolrManager } from "./SearchManager"; export enum Directory { parsed_files = "parsed_files", @@ -139,13 +140,9 @@ export default class UploadManager extends ApiManager { doc.id = getId(doc.id); } for (const key in doc.fields) { - if (!doc.fields.hasOwnProperty(key)) { - continue; - } + if (!doc.fields.hasOwnProperty(key)) { continue; } const field = doc.fields[key]; - if (field === undefined || field === null) { - continue; - } + if (field === undefined || field === null) { continue; } if (field.__type === "proxy" || field.__type === "prefetch_proxy") { field.fieldId = getId(field.fieldId); @@ -208,11 +205,8 @@ export default class UploadManager extends ApiManager { } catch (e) { console.log(e); } unlink(path_2, () => { }); } - if (id) { - res.send(JSON.stringify(getId(id))); - } else { - res.send(JSON.stringify("error")); - } + SolrManager.update(); + res.send(JSON.stringify(id ? getId(id) : "error")); } catch (e) { console.log(e); } resolve(); }); diff --git a/src/server/Recommender.ts b/src/server/Recommender.ts index 423ce9b46..935ec3871 100644 --- a/src/server/Recommender.ts +++ b/src/server/Recommender.ts @@ -21,7 +21,6 @@ // private choice: string = ""; // Tensorflow or Word2Vec // constructor() { -// console.log("creating recommender..."); // Recommender.Instance = this; // } @@ -70,7 +69,6 @@ // if (this._model) { // if (this.choice === "WV") { // let similarity = this._model.similarity('father', 'mother'); -// console.log(similarity); // } // else if (this.choice === "TF") { // const model = this._model as use.UniversalSentenceEncoder; @@ -119,7 +117,6 @@ // } // // public async trainModel() { -// // console.log("phrasing..."); // // w2v.word2vec("./node_modules/word2vec/examples/eng_news-typical_2016_1M-sentences.txt", './node_modules/word2vec/examples/my_phrases.txt', { // // cbow: 1, // // size: 200, @@ -131,7 +128,6 @@ // // iter: 200, // // minCount: 2 // // }); -// // console.log("phrased!!!"); // // } // } diff --git a/src/server/downsize.ts b/src/server/downsize.ts index cd0d83812..5cd709fa3 100644 --- a/src/server/downsize.ts +++ b/src/server/downsize.ts @@ -7,7 +7,7 @@ const jpgTypes = ["jpg", "JPG", "jpeg", "JPEG"]; const smallResizer = sharp().resize(100); fs.readdir(folder, async (err, files) => { if (err) { - console.log(err); + console.log("readdir:" + err); return; } // files.forEach(file => { diff --git a/src/server/websocket.ts b/src/server/websocket.ts index d55c2e198..f63a35e43 100644 --- a/src/server/websocket.ts +++ b/src/server/websocket.ts @@ -180,7 +180,14 @@ export namespace WebSocket { function barReceived(socket: SocketIO.Socket, userEmail: string) { clients[userEmail] = new Client(userEmail.toString()); - console.log(green(`user ${userEmail} has connected to the web socket`)); + const currentdate = new Date(); + const datetime = currentdate.getDate() + "/" + + (currentdate.getMonth() + 1) + "/" + + currentdate.getFullYear() + " @ " + + currentdate.getHours() + ":" + + currentdate.getMinutes() + ":" + + currentdate.getSeconds(); + console.log(green(`user ${userEmail} has connected to the web socket at: ${datetime}`)); socketMap.set(socket, userEmail); } |