diff options
author | usodhi <61431818+usodhi@users.noreply.github.com> | 2020-05-15 22:41:58 +0530 |
---|---|---|
committer | usodhi <61431818+usodhi@users.noreply.github.com> | 2020-05-15 22:41:58 +0530 |
commit | 0cc83ef72b6b1b254d97bf07a97dd1fe936aa25a (patch) | |
tree | bce434d8545a953fd943743f0714609f23529220 | |
parent | 9c0b8ab5820232292e02fbf453e50261137a533c (diff) | |
parent | 98c7540fff67c232c1b04f2130ee624f9a70afbd (diff) |
resolved conflicts
200 files changed, 2880 insertions, 2144 deletions
diff --git a/package-lock.json b/package-lock.json index 8227f90ff..90cae1a32 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2903,7 +2903,8 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true + "bundled": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -2921,11 +2922,13 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true + "bundled": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2938,15 +2941,18 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "concat-map": { "version": "0.0.1", - "bundled": true + "bundled": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -3049,7 +3055,8 @@ }, "inherits": { "version": "2.0.4", - "bundled": true + "bundled": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -3059,6 +3066,7 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -3071,17 +3079,20 @@ "minimatch": { "version": "3.0.4", "bundled": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "1.2.5", - "bundled": true + "bundled": true, + "optional": true }, "minipass": { "version": "2.9.0", "bundled": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -3098,6 +3109,7 @@ "mkdirp": { "version": "0.5.3", "bundled": true, + "optional": true, "requires": { "minimist": "^1.2.5" } @@ -3153,7 +3165,8 @@ }, "npm-normalize-package-bin": { "version": "1.0.1", - "bundled": true + "bundled": true, + "optional": true }, "npm-packlist": { "version": "1.4.8", @@ -3178,7 +3191,8 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true + "bundled": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -3188,6 +3202,7 @@ "once": { "version": "1.4.0", "bundled": true, + "optional": true, "requires": { "wrappy": "1" } @@ -3256,7 +3271,8 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true + "bundled": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -3286,6 +3302,7 @@ "string-width": { "version": "1.0.2", "bundled": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -3303,6 +3320,7 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -3341,11 +3359,13 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true + "bundled": true, + "optional": true }, "yallist": { "version": "3.1.1", - "bundled": true + "bundled": true, + "optional": true } } } diff --git a/src/Utils.ts b/src/Utils.ts index 23b59ac9d..bcb215804 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -313,14 +313,18 @@ export namespace Utils { } } -export function OmitKeys(obj: any, keys: string[], addKeyFunc?: (dup: any) => void): { omit: any, extract: any } { +export function OmitKeys(obj: any, keys: string[], pattern?: string, addKeyFunc?: (dup: any) => void): { omit: any, extract: any } { const omit: any = { ...obj }; const extract: any = {}; keys.forEach(key => { extract[key] = omit[key]; delete omit[key]; }); - addKeyFunc && addKeyFunc(omit); + pattern && Array.from(Object.keys(omit)).filter(key => key.match(pattern)).forEach(key => { + extract[key] = omit[key]; + delete omit[key]; + }); + addKeyFunc?.(omit); return { omit, extract }; } diff --git a/src/client/ClientRecommender.tsx b/src/client/ClientRecommender.tsx index 537e331ab..d18669b02 100644 --- a/src/client/ClientRecommender.tsx +++ b/src/client/ClientRecommender.tsx @@ -1,6 +1,6 @@ -import { Doc, FieldResult } from "../new_fields/Doc"; -import { StrCast, Cast } from "../new_fields/Types"; -import { List } from "../new_fields/List"; +import { Doc, FieldResult } from "../fields/Doc"; +import { StrCast, Cast } from "../fields/Types"; +import { List } from "../fields/List"; import { CognitiveServices, Confidence, Tag, Service } from "./cognitive_services/CognitiveServices"; import React = require("react"); import { observer } from "mobx-react"; @@ -11,11 +11,11 @@ import { observable, action, computed, reaction } from "mobx"; // var https = require('https'); import "./ClientRecommender.scss"; import { JSXElement } from "babel-types"; -import { RichTextField } from "../new_fields/RichTextField"; -import { ToPlainText } from "../new_fields/FieldSymbols"; -import { listSpec } from "../new_fields/Schema"; -import { ComputedField } from "../new_fields/ScriptField"; -import { ImageField } from "../new_fields/URLField"; +import { RichTextField } from "../fields/RichTextField"; +import { ToPlainText } from "../fields/FieldSymbols"; +import { listSpec } from "../fields/Schema"; +import { ComputedField } from "../fields/ScriptField"; +import { ImageField } from "../fields/URLField"; import { KeyphraseQueryView } from "./views/KeyphraseQueryView"; import { Networking } from "./Network"; diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index 0c9d5f75c..ac5b7a218 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -1,12 +1,13 @@ import * as OpenSocket from 'socket.io-client'; import { MessageStore, YoutubeQueryTypes, GestureContent, MobileInkOverlayContent, UpdateMobileInkOverlayPositionContent, MobileDocumentUploadContent } from "./../server/Message"; -import { Opt, Doc } from '../new_fields/Doc'; +import { Opt, Doc } from '../fields/Doc'; import { Utils, emptyFunction } from '../Utils'; import { SerializationHelper } from './util/SerializationHelper'; -import { RefField } from '../new_fields/RefField'; -import { Id, HandleUpdate } from '../new_fields/FieldSymbols'; +import { RefField } from '../fields/RefField'; +import { Id, HandleUpdate } from '../fields/FieldSymbols'; import GestureOverlay from './views/GestureOverlay'; import MobileInkOverlay from '../mobile/MobileInkOverlay'; +import { runInAction } from 'mobx'; /** * This class encapsulates the transfer and cross-client synchronization of @@ -107,8 +108,9 @@ export namespace DocServer { export function init(protocol: string, hostname: string, port: number, identifier: string) { _cache = {}; GUID = identifier; - _socket = OpenSocket(`${protocol}//${hostname}:${port}`); + _socket = OpenSocket(`${protocol}//${hostname}:${port}`);// OpenSocket(`https://7f079dda.ngrok.io`);// if using ngrok, create a special address for the websocket + _GetCachedRefField = _GetCachedRefFieldImpl; _GetRefField = _GetRefFieldImpl; _GetRefFields = _GetRefFieldsImpl; _CreateField = _CreateFieldImpl; @@ -243,12 +245,22 @@ export namespace DocServer { return Promise.resolve(cached); } }; + const _GetCachedRefFieldImpl = (id: string): Opt<RefField> => { + const cached = _cache[id]; + if (cached !== undefined && !(cached instanceof Promise)) { + return cached; + } + }; let _GetRefField: (id: string) => Promise<Opt<RefField>> = errorFunc; + let _GetCachedRefField: (id: string) => Opt<RefField> = errorFunc; export function GetRefField(id: string): Promise<Opt<RefField>> { return _GetRefField(id); } + export function GetCachedRefField(id: string): Opt<RefField> { + return _GetCachedRefField(id); + } export async function getYoutubeChannels() { const apiKey = await Utils.EmitCallback(_socket, MessageStore.YoutubeApiQuery, { type: YoutubeQueryTypes.Channels }); @@ -308,32 +320,34 @@ export namespace DocServer { const deserializeFields = getSerializedFields.then(async fields => { const fieldMap: { [id: string]: RefField } = {}; const proms: Promise<void>[] = []; - for (const field of fields) { - if (field !== undefined && field !== null) { - // deserialize - const prom = SerializationHelper.Deserialize(field).then(deserialized => { - fieldMap[field.id] = deserialized; - - //overwrite or delete any promises (that we inserted as flags - // to indicate that the field was in the process of being fetched). Now everything - // should be an actual value within or entirely absent from the cache. - if (deserialized !== undefined) { - _cache[field.id] = deserialized; - } else { - delete _cache[field.id]; - } - return deserialized; - }); - // 4) here, for each of the documents we've requested *ourselves* (i.e. weren't promises or found in the cache) - // we set the value at the field's id to a promise that will resolve to the field. - // When we find that promises exist at keys in the cache, THIS is where they were set, just by some other caller (method). - // The mapping in the .then call ensures that when other callers await these promises, they'll - // get the resolved field - _cache[field.id] = prom; - // adds to a list of promises that will be awaited asynchronously - proms.push(prom); + runInAction(() => { + for (const field of fields) { + if (field !== undefined && field !== null) { + // deserialize + const prom = SerializationHelper.Deserialize(field).then(deserialized => { + fieldMap[field.id] = deserialized; + + //overwrite or delete any promises (that we inserted as flags + // to indicate that the field was in the process of being fetched). Now everything + // should be an actual value within or entirely absent from the cache. + if (deserialized !== undefined) { + _cache[field.id] = deserialized; + } else { + delete _cache[field.id]; + } + return deserialized; + }); + // 4) here, for each of the documents we've requested *ourselves* (i.e. weren't promises or found in the cache) + // we set the value at the field's id to a promise that will resolve to the field. + // When we find that promises exist at keys in the cache, THIS is where they were set, just by some other caller (method). + // The mapping in the .then call ensures that when other callers await these promises, they'll + // get the resolved field + _cache[field.id] = prom; + // adds to a list of promises that will be awaited asynchronously + proms.push(prom); + } } - } + }); await Promise.all(proms); return fieldMap; }); diff --git a/src/client/apis/GoogleAuthenticationManager.scss b/src/client/apis/GoogleAuthenticationManager.scss index 13bde822d..bd30dd94f 100644 --- a/src/client/apis/GoogleAuthenticationManager.scss +++ b/src/client/apis/GoogleAuthenticationManager.scss @@ -16,4 +16,11 @@ font-style: italic; margin-top: 15px; } + + .disconnect { + font-size: 10px; + margin-top: 20px; + color: red; + cursor: grab; + } }
\ No newline at end of file diff --git a/src/client/apis/GoogleAuthenticationManager.tsx b/src/client/apis/GoogleAuthenticationManager.tsx index 417dc3c3b..bf4469aeb 100644 --- a/src/client/apis/GoogleAuthenticationManager.tsx +++ b/src/client/apis/GoogleAuthenticationManager.tsx @@ -1,10 +1,11 @@ -import { observable, action, reaction, runInAction } from "mobx"; +import { observable, action, reaction, runInAction, IReactionDisposer } from "mobx"; import { observer } from "mobx-react"; import * as React from "react"; import MainViewModal from "../views/MainViewModal"; -import { Opt } from "../../new_fields/Doc"; +import { Opt } from "../../fields/Doc"; import { Networking } from "../Network"; import "./GoogleAuthenticationManager.scss"; +import { Scripting } from "../util/Scripting"; const AuthenticationUrl = "https://accounts.google.com/o/oauth2/v2/auth"; const prompt = "Paste authorization code here..."; @@ -15,64 +16,88 @@ export default class GoogleAuthenticationManager extends React.Component<{}> { private authenticationLink: Opt<string> = undefined; @observable private openState = false; @observable private authenticationCode: Opt<string> = undefined; - @observable private clickedState = false; + @observable private showPasteTargetState = false; @observable private success: Opt<boolean> = undefined; @observable private displayLauncher = true; - @observable private avatar: Opt<string> = undefined; - @observable private username: Opt<string> = undefined; + @observable private credentials: any; + private disposer: Opt<IReactionDisposer>; private set isOpen(value: boolean) { runInAction(() => this.openState = value); } - private set hasBeenClicked(value: boolean) { - runInAction(() => this.clickedState = value); + private set shouldShowPasteTarget(value: boolean) { + runInAction(() => this.showPasteTargetState = value); } - public fetchOrGenerateAccessToken = async () => { - const response = await Networking.FetchFromServer("/readGoogleAccessToken"); + public cancel() { + this.openState && this.resetState(0, 0); + } + + public fetchOrGenerateAccessToken = async (displayIfFound = false) => { + let response: any = await Networking.FetchFromServer("/readGoogleAccessToken"); // if this is an authentication url, activate the UI to register the new access token if (new RegExp(AuthenticationUrl).test(response)) { this.isOpen = true; this.authenticationLink = response; return new Promise<string>(async resolve => { - const disposer = reaction( + this.disposer?.(); + this.disposer = reaction( () => this.authenticationCode, async authenticationCode => { - if (authenticationCode) { - disposer(); - const { access_token, avatar, name } = await Networking.PostToServer("/writeGoogleAccessToken", { authenticationCode }); + if (authenticationCode && /\d{1}\/[\w-]{55}/.test(authenticationCode)) { + this.disposer?.(); + const response = await Networking.PostToServer("/writeGoogleAccessToken", { authenticationCode }); runInAction(() => { - this.avatar = avatar; - this.username = name; - this.hasBeenClicked = false; - this.success = false; + this.success = true; + this.credentials = response; }); - this.beginFadeout(); - resolve(access_token); + this.resetState(); + resolve(response.access_token); } } ); }); } - // otherwise, we already have a valid, stored access token - return response; + + // otherwise, we already have a valid, stored access token and user info + response = JSON.parse(response); + if (displayIfFound) { + runInAction(() => { + this.success = true; + this.credentials = response; + }); + this.resetState(-1, -1); + this.isOpen = true; + } + return response.access_token; } - beginFadeout = action(() => { - this.success = true; + resetState = action((visibleForMS: number = 3000, fadesOutInMS: number = 500) => { + if (!visibleForMS && !fadesOutInMS) { + runInAction(() => { + this.isOpen = false; + this.success = undefined; + this.displayLauncher = true; + this.credentials = undefined; + this.shouldShowPasteTarget = false; + this.authenticationCode = undefined; + }); + return; + } this.authenticationCode = undefined; this.displayLauncher = false; - this.hasBeenClicked = false; - setTimeout(action(() => { - this.isOpen = false; + this.shouldShowPasteTarget = false; + if (visibleForMS > 0 && fadesOutInMS > 0) { setTimeout(action(() => { - this.success = undefined; - this.displayLauncher = true; - this.avatar = undefined; - this.username = undefined; - }), 500); - }), 3000); + this.isOpen = false; + setTimeout(action(() => { + this.success = undefined; + this.displayLauncher = true; + this.credentials = undefined; + }), fadesOutInMS); + }), visibleForMS); + } }); constructor(props: {}) { @@ -83,27 +108,38 @@ export default class GoogleAuthenticationManager extends React.Component<{}> { private get renderPrompt() { return ( <div className={'authorize-container'}> + {this.displayLauncher ? <button className={"dispatch"} onClick={() => { window.open(this.authenticationLink); - setTimeout(() => this.hasBeenClicked = true, 500); + setTimeout(() => this.shouldShowPasteTarget = true, 500); }} - style={{ marginBottom: this.clickedState ? 15 : 0 }} + style={{ marginBottom: this.showPasteTargetState ? 15 : 0 }} >Authorize a Google account...</button> : (null)} - {this.clickedState ? <input + {this.showPasteTargetState ? <input className={'paste-target'} onChange={action(e => this.authenticationCode = e.currentTarget.value)} placeholder={prompt} /> : (null)} - {this.avatar ? <img - className={'avatar'} - src={this.avatar} - /> : (null)} - {this.username ? <span - className={'welcome'} - >Welcome to Dash, {this.username} - </span> : (null)} + {this.credentials ? + <> + <img + className={'avatar'} + src={this.credentials.userInfo.picture} + /> + <span + className={'welcome'} + >Welcome to Dash, {this.credentials.userInfo.name} + </span> + <div + className={'disconnect'} + onClick={async () => { + await Networking.FetchFromServer("/revokeGoogleAccessToken"); + this.resetState(0, 0); + }} + >Disconnect Account</div> + </> : (null)} </div> ); } @@ -125,4 +161,6 @@ export default class GoogleAuthenticationManager extends React.Component<{}> { ); } -}
\ No newline at end of file +} + +Scripting.addGlobal("GoogleAuthenticationManager", GoogleAuthenticationManager);
\ No newline at end of file diff --git a/src/client/apis/IBM_Recommender.ts b/src/client/apis/IBM_Recommender.ts index 4e1c541c8..480b9cb1c 100644 --- a/src/client/apis/IBM_Recommender.ts +++ b/src/client/apis/IBM_Recommender.ts @@ -1,4 +1,4 @@ -// import { Opt } from "../../new_fields/Doc"; +// import { Opt } from "../../fields/Doc"; // const NaturalLanguageUnderstandingV1 = require('ibm-watson/natural-language-understanding/v1'); // const { IamAuthenticator } = require('ibm-watson/auth'); diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts index fa67ddbef..551dca073 100644 --- a/src/client/apis/google_docs/GoogleApiClientUtils.ts +++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts @@ -1,5 +1,5 @@ import { docs_v1 } from "googleapis"; -import { Opt } from "../../../new_fields/Doc"; +import { Opt } from "../../../fields/Doc"; import { isArray } from "util"; import { EditorState } from "prosemirror-state"; import { Networking } from "../../Network"; @@ -95,7 +95,7 @@ export namespace GoogleApiClientUtils { export type ExtractResult = { text: string, paragraphs: DeconstructedParagraph[] }; export const extractText = (document: docs_v1.Schema$Document, removeNewlines = false): ExtractResult => { const paragraphs = extractParagraphs(document); - let text = paragraphs.map(paragraph => paragraph.contents.filter(content => !("inlineObjectId" in content)).map(run => run as docs_v1.Schema$TextRun).join("")).join(""); + let text = paragraphs.map(paragraph => paragraph.contents.filter(content => !("inlineObjectId" in content)).map(run => (run as docs_v1.Schema$TextRun).content).join("")).join(""); text = text.substring(0, text.length - 1); removeNewlines && text.replace(/\n/g, ""); return { text, paragraphs }; diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts index ff471853a..fef71ffeb 100644 --- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts +++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts @@ -1,11 +1,11 @@ import { AssertionError } from "assert"; import { EditorState } from "prosemirror-state"; -import { Doc, DocListCastAsync, Opt } from "../../../new_fields/Doc"; -import { Id } from "../../../new_fields/FieldSymbols"; -import { RichTextField } from "../../../new_fields/RichTextField"; -import { RichTextUtils } from "../../../new_fields/RichTextUtils"; -import { Cast, StrCast } from "../../../new_fields/Types"; -import { ImageField } from "../../../new_fields/URLField"; +import { Doc, DocListCastAsync, Opt } from "../../../fields/Doc"; +import { Id } from "../../../fields/FieldSymbols"; +import { RichTextField } from "../../../fields/RichTextField"; +import { RichTextUtils } from "../../../fields/RichTextUtils"; +import { Cast, StrCast } from "../../../fields/Types"; +import { ImageField } from "../../../fields/URLField"; import { MediaItem, NewMediaItemResult } from "../../../server/apis/google/SharedTypes"; import { Utils } from "../../../Utils"; import { Docs, DocumentOptions } from "../../documents/Documents"; @@ -153,21 +153,20 @@ export namespace GooglePhotos { } const tagMapping = new Map<string, string>(); const images = (await DocListCastAsync(collection.data))!.map(Doc.GetProto); - images && images.forEach(image => tagMapping.set(image[Id], ContentCategories.NONE)); - const values = Object.values(ContentCategories); + images?.forEach(image => tagMapping.set(image[Id], ContentCategories.NONE)); + const values = Object.values(ContentCategories).filter(value => value !== ContentCategories.NONE); for (const value of values) { - if (value === ContentCategories.NONE) { - continue; - } - for (const id of (await ContentSearch({ included: [value] }))?.mediaItems?.map(({ id }) => id)) { + 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) { - continue; + if (image) { + const key = image[Id]; + const tags = tagMapping.get(key); + !tags?.includes(value) && tagMapping.set(key, tags + delimiter + value); } - const key = image[Id]; - const tags = tagMapping.get(key); - !tags?.includes(value) && tagMapping.set(key, tags + delimiter + value); - } + }); } images?.forEach(image => { const concatenated = tagMapping.get(image[Id])!; diff --git a/src/client/apis/youtube/YoutubeBox.tsx b/src/client/apis/youtube/YoutubeBox.tsx index 1575e53fc..ce7f49e64 100644 --- a/src/client/apis/youtube/YoutubeBox.tsx +++ b/src/client/apis/youtube/YoutubeBox.tsx @@ -1,7 +1,7 @@ import { action, observable, runInAction } from 'mobx'; import { observer } from "mobx-react"; -import { Doc, DocListCastAsync } from "../../../new_fields/Doc"; -import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; +import { Doc, DocListCastAsync } from "../../../fields/Doc"; +import { Cast, NumCast, StrCast } from "../../../fields/Types"; import { Utils } from "../../../Utils"; import { DocServer } from "../../DocServer"; import { Docs } from "../../documents/Documents"; diff --git a/src/client/cognitive_services/CognitiveServices.ts b/src/client/cognitive_services/CognitiveServices.ts index 8c63ae906..d4df7ce57 100644 --- a/src/client/cognitive_services/CognitiveServices.ts +++ b/src/client/cognitive_services/CognitiveServices.ts @@ -1,12 +1,12 @@ import * as request from "request-promise"; -import { Doc, Field } from "../../new_fields/Doc"; -import { Cast } from "../../new_fields/Types"; +import { Doc, Field } from "../../fields/Doc"; +import { Cast } from "../../fields/Types"; import { Docs } from "../documents/Documents"; import { Utils } from "../../Utils"; -import { InkData } from "../../new_fields/InkField"; +import { InkData } from "../../fields/InkField"; import { UndoManager } from "../util/UndoManager"; import requestPromise = require("request-promise"); -import { List } from "../../new_fields/List"; +import { List } from "../../fields/List"; import { ClientRecommender } from "../ClientRecommender"; type APIManager<D> = { converter: BodyConverter<D>, requester: RequestExecutor }; diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 45687f597..18a0b43ff 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -9,14 +9,14 @@ import { ScriptingBox } from "../views/nodes/ScriptingBox"; import { VideoBox } from "../views/nodes/VideoBox"; import { WebBox } from "../views/nodes/WebBox"; import { OmitKeys, JSONUtils, Utils } from "../../Utils"; -import { Field, Doc, Opt, DocListCastAsync, FieldResult, DocListCast } from "../../new_fields/Doc"; -import { ImageField, VideoField, AudioField, PdfField, WebField, YoutubeField } from "../../new_fields/URLField"; -import { HtmlField } from "../../new_fields/HtmlField"; -import { List } from "../../new_fields/List"; -import { Cast, NumCast, StrCast } from "../../new_fields/Types"; +import { Field, Doc, Opt, DocListCastAsync, FieldResult, DocListCast } from "../../fields/Doc"; +import { ImageField, VideoField, AudioField, PdfField, WebField, YoutubeField } from "../../fields/URLField"; +import { HtmlField } from "../../fields/HtmlField"; +import { List } from "../../fields/List"; +import { Cast, NumCast, StrCast } from "../../fields/Types"; import { DocServer } from "../DocServer"; import { dropActionType } from "../util/DragManager"; -import { DateField } from "../../new_fields/DateField"; +import { DateField } from "../../fields/DateField"; import { YoutubeBox } from "../apis/youtube/YoutubeBox"; import { CollectionDockingView } from "../views/collections/CollectionDockingView"; import { LinkManager } from "../util/LinkManager"; @@ -26,10 +26,10 @@ import { Scripting } from "../util/Scripting"; import { LabelBox } from "../views/nodes/LabelBox"; import { SliderBox } from "../views/nodes/SliderBox"; import { FontIconBox } from "../views/nodes/FontIconBox"; -import { SchemaHeaderField } from "../../new_fields/SchemaHeaderField"; +import { SchemaHeaderField } from "../../fields/SchemaHeaderField"; import { PresBox } from "../views/nodes/PresBox"; -import { ComputedField, ScriptField } from "../../new_fields/ScriptField"; -import { ProxyField } from "../../new_fields/Proxy"; +import { ComputedField, ScriptField } from "../../fields/ScriptField"; +import { ProxyField } from "../../fields/Proxy"; import { DocumentType } from "./DocumentTypes"; import { RecommendationsBox } from "../views/RecommendationsBox"; import { PresElementBox } from "../views/presentationview/PresElementBox"; @@ -37,11 +37,11 @@ import { DashWebRTCVideo } from "../views/webcam/DashWebRTCVideo"; import { QueryBox } from "../views/nodes/QueryBox"; import { ColorBox } from "../views/nodes/ColorBox"; import { LinkAnchorBox } from "../views/nodes/LinkAnchorBox"; -import { DocHolderBox } from "../views/nodes/DocumentBox"; +import { DocHolderBox } from "../views/nodes/DocHolderBox"; import { InkingStroke } from "../views/InkingStroke"; -import { InkField } from "../../new_fields/InkField"; +import { InkField } from "../../fields/InkField"; import { InkingControl } from "../views/InkingControl"; -import { RichTextField } from "../../new_fields/RichTextField"; +import { RichTextField } from "../../fields/RichTextField"; import { extname } from "path"; import { MessageStore } from "../../server/Message"; import { ContextMenuProps } from "../views/ContextMenuItem"; @@ -58,6 +58,8 @@ export interface DocumentOptions { _height?: number; _nativeWidth?: number; _nativeHeight?: number; + _dimMagnitude?: number; // magnitude of collectionMulti{row,col} view element + _dimUnit?: string; // "px" or "*" (default = "*") _fitWidth?: boolean; _fitToBox?: boolean; // whether a freeformview should zoom/scale to create a shrinkwrapped view of its contents _LODdisable?: boolean; @@ -125,6 +127,7 @@ export interface DocumentOptions { borderRounding?: string; boxShadow?: string; 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 "onClick-rawScript"?: string; // onClick script in raw text form "onCheckedClick-rawScript"?: string; // onChecked script in raw text form @@ -148,6 +151,7 @@ export interface DocumentOptions { dragFactory?: Doc; // document to create when dragging with a suitable onDragStart script onDragStart?: ScriptField; //script to execute at start of drag operation -- e.g., when a "creator" button is dragged this script generates a different document to drop clipboard?: Doc; + UseCors?: boolean; icon?: string; sourcePanel?: Doc; // panel to display in 'targetContainer' as the result of a button onClick script targetContainer?: Doc; // document whose proto will be set to 'panel' as the result of a onClick click script @@ -455,10 +459,7 @@ export namespace Docs { Scripting.addGlobal(Buxton); - const delegateKeys = ["x", "y", "layoutKey", "_width", "_height", "_panX", "_panY", "_viewType", "_nativeWidth", "_nativeHeight", "dropAction", "childDropAction", "_annotationOn", - "_chromeStatus", "_autoHeight", "_fitWidth", "_LODdisable", "_itemIndex", "_showSidebar", "_showTitle", "_showCaption", "_showTitleHover", "_backgroundColor", - "_xMargin", "_yMargin", "_xPadding", "_yPadding", "_singleLine", "_scrollTop", - "_color", "isLinkButton", "isBackground", "removeDropProperties", "treeViewOpen"]; + const delegateKeys = ["x", "y", "layoutKey", "dropAction", "childDropAction", "isLinkButton", "isBackground", "removeDropProperties", "treeViewOpen"]; /** * This function receives the relevant document prototype and uses @@ -479,7 +480,7 @@ export namespace Docs { * main document. */ export function InstanceFromProto(proto: Doc, data: Field | undefined, options: DocumentOptions, delegId?: string, fieldKey: string = "data") { - const { omit: protoProps, extract: delegateProps } = OmitKeys(options, delegateKeys); + const { omit: protoProps, extract: delegateProps } = OmitKeys(options, delegateKeys, "^_"); if (!("author" in protoProps)) { protoProps.author = Doc.CurrentUserEmail; @@ -606,7 +607,7 @@ export namespace Docs { return doc; } - export function InkDocument(color: string, tool: number, strokeWidth: number, points: { X: number, Y: number }[], options: DocumentOptions = {}) { + export function InkDocument(color: string, tool: number, strokeWidth: string, points: { X: number, Y: number }[], options: DocumentOptions = {}) { const I = new Doc(); I.type = DocumentType.INK; I.layout = InkingStroke.LayoutString("data"); diff --git a/src/server/authentication/models/current_user_utils.ts b/src/client/util/CurrentUserUtils.ts index bf99f449f..1f25ed790 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -1,26 +1,27 @@ -import { action, computed, observable, reaction } from "mobx"; +import { computed, observable, reaction } from "mobx"; import * as rp from 'request-promise'; -import { Utils } from "../../../Utils"; -import { DocServer } from "../../../client/DocServer"; -import { Docs, DocumentOptions } from "../../../client/documents/Documents"; -import { UndoManager } from "../../../client/util/UndoManager"; -import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc"; -import { List } from "../../../new_fields/List"; -import { listSpec } from "../../../new_fields/Schema"; -import { ScriptField, ComputedField } from "../../../new_fields/ScriptField"; -import { Cast, PromiseValue, StrCast, NumCast } from "../../../new_fields/Types"; -import { nullAudio, ImageField } from "../../../new_fields/URLField"; -import { DragManager } from "../../../client/util/DragManager"; -import { InkingControl } from "../../../client/views/InkingControl"; -import { Scripting, CompileScript } from "../../../client/util/Scripting"; -import { CollectionViewType } from "../../../client/views/collections/CollectionView"; -import { makeTemplate } from "../../../client/util/DropConverter"; -import { RichTextField } from "../../../new_fields/RichTextField"; -import { PrefetchProxy } from "../../../new_fields/Proxy"; -import { FormattedTextBox } from "../../../client/views/nodes/formattedText/FormattedTextBox"; -import { MainView } from "../../../client/views/MainView"; -import { DocumentType } from "../../../client/documents/DocumentTypes"; -import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; +import { Utils } from "../../Utils"; +import { DocServer } from "../DocServer"; +import { Docs, DocumentOptions } from "../documents/Documents"; +import { UndoManager } from "./UndoManager"; +import { Doc, DocListCast, DocListCastAsync } from "../../fields/Doc"; +import { List } from "../../fields/List"; +import { listSpec } from "../../fields/Schema"; +import { ScriptField, ComputedField } from "../../fields/ScriptField"; +import { Cast, PromiseValue, StrCast, NumCast } from "../../fields/Types"; +import { nullAudio } from "../../fields/URLField"; +import { DragManager } from "./DragManager"; +import { InkingControl } from "../views/InkingControl"; +import { Scripting } from "./Scripting"; +import { CollectionViewType } from "../views/collections/CollectionView"; +import { makeTemplate } from "./DropConverter"; +import { RichTextField } from "../../fields/RichTextField"; +import { PrefetchProxy } from "../../fields/Proxy"; +import { FormattedTextBox } from "../views/nodes/formattedText/FormattedTextBox"; +import { MainView } from "../views/MainView"; +import { DocumentType } from "../documents/DocumentTypes"; +import { SchemaHeaderField } from "../../fields/SchemaHeaderField"; +import { DimUnit } from "../views/collections/collectionMulticolumn/CollectionMulticolumnView"; export class CurrentUserUtils { private static curr_id: string; @@ -49,7 +50,7 @@ export class CurrentUserUtils { ); queryTemplate.isTemplateDoc = makeTemplate(queryTemplate); doc["template-button-query"] = CurrentUserUtils.ficon({ - onDragStart: ScriptField.MakeFunction('makeDelegate(this.dragFactory, true)'), + onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'), dragFactory: new PrefetchProxy(queryTemplate) as any as Doc, removeDropProperties: new List<string>(["dropAction"]), title: "query view", icon: "question-circle" }); @@ -65,7 +66,7 @@ export class CurrentUserUtils { ); slideTemplate.isTemplateDoc = makeTemplate(slideTemplate); doc["template-button-slides"] = CurrentUserUtils.ficon({ - onDragStart: ScriptField.MakeFunction('makeDelegate(this.dragFactory, true)'), + onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'), dragFactory: new PrefetchProxy(slideTemplate) as any as Doc, removeDropProperties: new List<string>(["dropAction"]), title: "presentation slide", icon: "address-card" }); @@ -74,8 +75,10 @@ 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 = - "<div><FormattedTextBox {...props} height='{this._headerHeight||75}px' background='{this._headerColor||`orange`}' fieldKey={'header'}/>" + - "<FormattedTextBox {...props} height='calc(100% - {this._headerHeight||75}px)' fieldKey={'text'}/></div>"; + "<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"); doc["template-button-description"] = CurrentUserUtils.ficon({ @@ -85,6 +88,38 @@ export class CurrentUserUtils { }); } + if (doc["template-button-switch"] === undefined) { + const { FreeformDocument, MulticolumnDocument, TextDocument } = Docs.Create; + + const yes = FreeformDocument([], { title: "yes", _height: 35, _width: 50, _LODdisable: true, _dimUnit: DimUnit.Pixel, _dimMagnitude: 40 }); + const name = TextDocument("name", { title: "name", _height: 35, _width: 70, _dimMagnitude: 1 }); + const no = FreeformDocument([], { title: "no", _height: 100, _width: 100, _LODdisable: true }); + const labelTemplate = { + doc: { + type: "doc", content: [{ + type: "paragraph", + content: [{ type: "dashField", attrs: { fieldKey: "PARAMS", hideKey: true } }] + }] + }, + selection: { type: "text", anchor: 1, head: 1 }, + storedMarks: [] + }; + Doc.GetProto(name).text = new RichTextField(JSON.stringify(labelTemplate), "PARAMS"); + Doc.GetProto(yes).backgroundColor = ComputedField.MakeFunction("self[this.PARAMS] ? 'green':'red'"); + // Doc.GetProto(no).backgroundColor = ComputedField.MakeFunction("!self[this.PARAMS] ? 'red':'white'"); + // Doc.GetProto(yes).onClick = ScriptField.MakeScript("self[this.PARAMS] = true"); + Doc.GetProto(yes).onClick = ScriptField.MakeScript("self[this.PARAMS] = !self[this.PARAMS]"); + // Doc.GetProto(no).onClick = ScriptField.MakeScript("self[this.PARAMS] = false"); + const box = MulticolumnDocument([/*no, */ yes, name], { title: "value", _width: 120, _height: 35, }); + box.isTemplateDoc = makeTemplate(box, true, "switch"); + + doc["template-button-switch"] = CurrentUserUtils.ficon({ + onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'), + dragFactory: new PrefetchProxy(box) as any as Doc, + removeDropProperties: new List<string>(["dropAction"]), title: "data switch", icon: "toggle-on" + }); + } + if (doc["template-button-detail"] === undefined) { const { TextDocument, MasonryDocument, CarouselDocument } = Docs.Create; @@ -129,7 +164,7 @@ export class CurrentUserUtils { long.title = "Long Description"; doc["template-button-detail"] = CurrentUserUtils.ficon({ - onDragStart: ScriptField.MakeFunction('makeDelegate(this.dragFactory, true)'), + onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'), dragFactory: new PrefetchProxy(detailView) as any as Doc, removeDropProperties: new List<string>(["dropAction"]), title: "detail view", icon: "window-maximize" }); @@ -137,13 +172,19 @@ export class CurrentUserUtils { if (doc["template-buttons"] === undefined) { doc["template-buttons"] = new PrefetchProxy(Docs.Create.MasonryDocument([doc["template-button-slides"] as Doc, doc["template-button-description"] as Doc, - doc["template-button-query"] as Doc, doc["template-button-detail"] as Doc], { - title: "Document Prototypes", _xMargin: 0, _showTitle: "title", + doc["template-button-query"] as Doc, doc["template-button-detail"] as Doc, doc["template-button-switch"] as Doc], { + title: "Advanced Item Prototypes", _xMargin: 0, _showTitle: "title", _autoHeight: true, _width: 500, columnWidth: 35, ignoreClick: true, lockedPosition: true, _chromeStatus: "disabled", dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), })); } else { - DocListCast(Cast(doc["template-buttons"], Doc, null)?.data); // prefetch templates + const curButnTypes = Cast(doc["template-buttons"], Doc, null); + const requiredTypes = [doc["template-button-slides"] as Doc, doc["template-button-description"] as Doc, + doc["template-button-query"] as Doc, doc["template-button-detail"] as Doc, doc["template-button-switch"] as Doc]; + DocListCastAsync(curButnTypes.data).then(async curBtns => { + await Promise.all(curBtns!); + requiredTypes.map(btype => Doc.AddDocToList(curButnTypes, "data", btype)); + }); } return doc["template-buttons"] as Doc; } @@ -248,8 +289,12 @@ export class CurrentUserUtils { doc["template-icon-view-col"] as Doc, doc["template-icon-view-rtf"] as Doc], { title: "icon templates", _height: 75 })); } else { const templateIconsDoc = Cast(doc["template-icons"], Doc, null); - DocListCastAsync(templateIconsDoc).then(list => templateIconsDoc.data = new List<Doc>([doc["template-icon-view"] as Doc, doc["template-icon-view-img"] as Doc, - doc["template-icon-view-col"] as Doc, doc["template-icon-view-rtf"] as Doc])); + const requiredTypes = [doc["template-icon-view"] as Doc, doc["template-icon-view-img"] as Doc, + doc["template-icon-view-col"] as Doc, doc["template-icon-view-rtf"] as Doc]; + DocListCastAsync(templateIconsDoc.data).then(async curIcons => { + await Promise.all(curIcons!); + requiredTypes.map(ntype => Doc.AddDocToList(templateIconsDoc, "data", ntype)); + }); } return doc["template-icons"] as Doc; } @@ -272,7 +317,7 @@ export class CurrentUserUtils { { _width: 250, _height: 250, title: "container" }); } if (doc.emptyWebpage === undefined) { - doc.emptyWebpage = Docs.Create.WebDocument("", { title: "New Webpage", _width: 600 }) + doc.emptyWebpage = Docs.Create.WebDocument("", { title: "New Webpage", _width: 600, UseCors: true }); } return [ { title: "Drag a collection", label: "Col", icon: "folder", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyCollection as Doc }, @@ -294,7 +339,8 @@ export class CurrentUserUtils { // { title: "use eraser", icon: "eraser", click: 'activateEraser(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this);', ischecked: `sameDocs(this.activePen.inkPen, this)`, backgroundColor: "pink", activePen: doc }, // { title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activePen.inkPen = this;', ischecked: `sameDocs(this.activePen.inkPen, this)`, backgroundColor: "white", activePen: doc }, { title: "Drag a document previewer", label: "Prev", icon: "expand", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory,true)', dragFactory: doc.emptyDocHolder as Doc }, - { title: "Drag a Calculator REPL", label: "repl", icon: "calculator", click: 'addOverlayWindow("ScriptingRepl", { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" })' }, + { 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)' }, ]; } @@ -310,21 +356,22 @@ export class CurrentUserUtils { alreadyCreatedButtons = dragDocs.map(d => StrCast(d.title)); } } - const creatorBtns = CurrentUserUtils.creatorBtnDescriptors(doc).filter(d => !alreadyCreatedButtons?.includes(d.title)). - map(data => Docs.Create.FontIconDocument({ - _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, - icon: data.icon, - title: data.title, - label: data.label, - ignoreClick: data.ignoreClick, - dropAction: "copy", - onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined, - onClick: data.click ? ScriptField.MakeScript(data.click) : undefined, - ischecked: data.ischecked ? ComputedField.MakeFunction(data.ischecked) : undefined, - activePen: data.activePen, - backgroundColor: data.backgroundColor, removeDropProperties: new List<string>(["dropAction"]), - dragFactory: data.dragFactory, - })); + const buttons = CurrentUserUtils.creatorBtnDescriptors(doc).filter(d => !alreadyCreatedButtons?.includes(d.title)); + const creatorBtns = buttons.map(({ title, label, icon, ignoreClick, drag, click, ischecked, activePen, backgroundColor, dragFactory }) => Docs.Create.FontIconDocument({ + _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, + icon, + title, + label, + ignoreClick, + dropAction: "copy", + onDragStart: drag ? ScriptField.MakeFunction(drag) : undefined, + onClick: click ? ScriptField.MakeScript(click) : undefined, + ischecked: ischecked ? ComputedField.MakeFunction(ischecked) : undefined, + activePen, + backgroundColor, + removeDropProperties: new List<string>(["dropAction"]), + dragFactory, + })); if (dragCreatorSet === undefined) { doc.myItemCreators = new PrefetchProxy(Docs.Create.MasonryDocument(creatorBtns, { @@ -462,13 +509,13 @@ export class CurrentUserUtils { return doc.myWorkspaces as Doc; } - static setupDocumentCollection(doc: Doc) { - if (doc.myDocuments === undefined) { - doc.myDocuments = new PrefetchProxy(Docs.Create.TreeDocument([], { - title: "DOCUMENTS", _height: 42, forceActive: true, boxShadow: "0 0", treeViewPreventOpen: true, lockedPosition: true, + static setupCatalog(doc: Doc) { + if (doc.myCatalog === undefined) { + doc.myCatalog = new PrefetchProxy(Docs.Create.TreeDocument([], { + title: "CATALOG", _height: 42, forceActive: true, boxShadow: "0 0", treeViewPreventOpen: false, lockedPosition: true, })); } - return doc.myDocuments as Doc; + return doc.myCatalog as Doc; } static setupRecentlyClosed(doc: Doc) { // setup Recently Closed library item @@ -488,7 +535,7 @@ export class CurrentUserUtils { // setup the Library button which will display the library panel. This panel includes a collection of workspaces, documents, and recently closed views static setupLibraryPanel(doc: Doc, sidebarContainer: Doc) { const workspaces = CurrentUserUtils.setupWorkspaces(doc); - const documents = CurrentUserUtils.setupDocumentCollection(doc); + const documents = CurrentUserUtils.setupCatalog(doc); const recentlyClosed = CurrentUserUtils.setupRecentlyClosed(doc); if (doc["tabs-button-library"] === undefined) { @@ -496,7 +543,7 @@ export class CurrentUserUtils { _width: 50, _height: 25, title: "Library", _fontSize: 10, letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)", sourcePanel: new PrefetchProxy(Docs.Create.TreeDocument([workspaces, documents, recentlyClosed, doc], { - title: "Library", _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "move", lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true + title: "Library", _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "alias", lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true })) as any as Doc, targetContainer: new PrefetchProxy(sidebarContainer) as any as Doc, onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel;") @@ -564,7 +611,7 @@ export class CurrentUserUtils { static setupDockedButtons(doc: Doc) { if (doc["dockedBtn-pen"] === undefined) { doc["dockedBtn-pen"] = CurrentUserUtils.ficon({ - onClick: ScriptField.MakeScript("activatePen(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this,2, this.backgroundColor)"), + onClick: ScriptField.MakeScript("activatePen(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this, this.inkWidth, this.backgroundColor)"), author: "systemTemplates", title: "ink mode", icon: "pen-nib", ischecked: ComputedField.MakeFunction(`sameDocs(this.activePen.inkPen, this)`), activePen: doc }); } @@ -643,6 +690,8 @@ export class CurrentUserUtils { new InkingControl(); doc.title = Doc.CurrentUserEmail; doc.activePen = doc; + doc.inkColor = StrCast(doc.backgroundColor, "rgb(0, 0, 0)"); + doc.fontSize = NumCast(doc.fontSize, 12); doc["constants-snapThreshold"] = NumCast(doc["constants-snapThreshold"], 10); // doc["constants-dragThreshold"] = NumCast(doc["constants-dragThreshold"], 4); // Utils.DRAG_THRESHOLD = NumCast(doc["constants-dragThreshold"]); diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts index b3295ece0..e46225b4a 100644 --- a/src/client/util/DictationManager.ts +++ b/src/client/util/DictationManager.ts @@ -3,15 +3,15 @@ import { DocumentView } from "../views/nodes/DocumentView"; import { UndoManager } from "./UndoManager"; import * as interpreter from "words-to-numbers"; import { DocumentType } from "../documents/DocumentTypes"; -import { Doc, Opt } from "../../new_fields/Doc"; -import { List } from "../../new_fields/List"; +import { Doc, Opt } from "../../fields/Doc"; +import { List } from "../../fields/List"; import { Docs } from "../documents/Documents"; import { CollectionViewType } from "../views/collections/CollectionView"; -import { Cast, CastCtor } from "../../new_fields/Types"; -import { listSpec } from "../../new_fields/Schema"; -import { AudioField, ImageField } from "../../new_fields/URLField"; +import { Cast, CastCtor } from "../../fields/Types"; +import { listSpec } from "../../fields/Schema"; +import { AudioField, ImageField } from "../../fields/URLField"; import { Utils } from "../../Utils"; -import { RichTextField } from "../../new_fields/RichTextField"; +import { RichTextField } from "../../fields/RichTextField"; import { DictationOverlay } from "../views/DictationOverlay"; /** diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 1ba6f0248..ab087335e 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -1,7 +1,7 @@ import { action, computed, observable } from 'mobx'; -import { Doc, DocListCastAsync, DocListCast, Opt } from '../../new_fields/Doc'; -import { Id } from '../../new_fields/FieldSymbols'; -import { Cast, NumCast, StrCast } from '../../new_fields/Types'; +import { Doc, DocListCastAsync, DocListCast, Opt } from '../../fields/Doc'; +import { Id } from '../../fields/FieldSymbols'; +import { Cast, NumCast, StrCast } from '../../fields/Types'; import { CollectionDockingView } from '../views/collections/CollectionDockingView'; import { CollectionView } from '../views/collections/CollectionView'; import { DocumentView, DocFocusFunc } from '../views/nodes/DocumentView'; @@ -94,37 +94,29 @@ export class DocumentManager { // heuristic to return the "best" documents first: // choose an exact match over an alias match // choose documents that have a PanelWidth() over those that don't (the treeview documents have no panelWidth) - docViews.map(view => !view.props.Document.presBox && view.props.PanelWidth() > 1 && view.props.Document === toFind && toReturn.push(view)); - docViews.map(view => !view.props.Document.presBox && view.props.PanelWidth() <= 1 && view.props.Document === toFind && toReturn.push(view)); - docViews.map(view => !view.props.Document.presBox && view.props.PanelWidth() > 1 && view.props.Document !== toFind && Doc.AreProtosEqual(view.props.Document, toFind) && toReturn.push(view)); - docViews.map(view => !view.props.Document.presBox && view.props.PanelWidth() <= 1 && view.props.Document !== toFind && Doc.AreProtosEqual(view.props.Document, toFind) && toReturn.push(view)); + docViews.map(view => view.props.PanelWidth() > 1 && view.props.Document === toFind && toReturn.push(view)); + docViews.map(view => view.props.PanelWidth() <= 1 && view.props.Document === toFind && toReturn.push(view)); + docViews.map(view => view.props.PanelWidth() > 1 && view.props.Document !== toFind && Doc.AreProtosEqual(view.props.Document, toFind) && toReturn.push(view)); + docViews.map(view => view.props.PanelWidth() <= 1 && view.props.Document !== toFind && Doc.AreProtosEqual(view.props.Document, toFind) && toReturn.push(view)); return toReturn; } @computed public get LinkedDocumentViews() { - const pairs = DocumentManager.Instance.DocumentViews - //.filter(dv => (dv.isSelected() || Doc.IsBrushed(dv.props.Document))) // draw links from DocumentViews that are selected or brushed OR - // || DocumentManager.Instance.DocumentViews.some(dv2 => { // Documentviews which - // const rest = DocListCast(dv2.props.Document.links).some(l => Doc.AreProtosEqual(l, dv.props.Document));// are link doc anchors - // const init = (dv2.isSelected() || Doc.IsBrushed(dv2.props.Document)) && dv2.Document.type !== DocumentType.AUDIO; // on a view that is selected or brushed - // return init && rest; - // } - // ) - .reduce((pairs, dv) => { - const linksList = LinkManager.Instance.getAllRelatedLinks(dv.props.Document); - pairs.push(...linksList.reduce((pairs, link) => { - const linkToDoc = link && LinkManager.Instance.getOppositeAnchor(link, dv.props.Document); - linkToDoc && DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 => { - if (dv.props.Document.type !== DocumentType.LINK || dv.props.LayoutTemplateString !== docView1.props.LayoutTemplateString) { - pairs.push({ a: dv, b: docView1, l: link }); - } - }); - return pairs; - }, [] as { a: DocumentView, b: DocumentView, l: Doc }[])); + const pairs = DocumentManager.Instance.DocumentViews.reduce((pairs, dv) => { + const linksList = LinkManager.Instance.getAllRelatedLinks(dv.props.Document); + pairs.push(...linksList.reduce((pairs, link) => { + const linkToDoc = link && LinkManager.Instance.getOppositeAnchor(link, dv.props.Document); + linkToDoc && DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 => { + if (dv.props.Document.type !== DocumentType.LINK || dv.props.LayoutTemplateString !== docView1.props.LayoutTemplateString) { + pairs.push({ a: dv, b: docView1, l: link }); + } + }); return pairs; - }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]); + }, [] as { a: DocumentView, b: DocumentView, l: Doc }[])); + return pairs; + }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]); return pairs; } diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index d91eb60ef..2a9c1633a 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -1,16 +1,17 @@ import { action, observable, runInAction } from "mobx"; -import { DateField } from "../../new_fields/DateField"; -import { Doc, Field, Opt } from "../../new_fields/Doc"; -import { List } from "../../new_fields/List"; -import { PrefetchProxy } from "../../new_fields/Proxy"; -import { listSpec } from "../../new_fields/Schema"; -import { SchemaHeaderField } from "../../new_fields/SchemaHeaderField"; -import { ScriptField } from "../../new_fields/ScriptField"; -import { Cast, NumCast, ScriptCast, StrCast } from "../../new_fields/Types"; +import { DateField } from "../../fields/DateField"; +import { Doc, Field, Opt } from "../../fields/Doc"; +import { List } from "../../fields/List"; +import { PrefetchProxy } from "../../fields/Proxy"; +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 { Docs, DocUtils } from "../documents/Documents"; import * as globalCssVariables from "../views/globalCssVariables.scss"; import { UndoManager } from "./UndoManager"; +import { SnappingManager } from "./SnappingManager"; export type dropActionType = "alias" | "copy" | "move" | undefined; // undefined = move export function SetupDrag( @@ -49,7 +50,7 @@ export function SetupDrag( if (e.shiftKey) { e.persist(); const dragDoc = await docFunc(); - dragDoc && DragManager.Vals.Instance.StartWindowDrag?.({ + dragDoc && DragManager.StartWindowDrag?.({ pageX: e.pageX, pageY: e.pageY, preventDefault: emptyFunction, @@ -66,19 +67,7 @@ export function SetupDrag( export namespace DragManager { let dragDiv: HTMLDivElement; - export class Vals { - static Instance: Vals = new Vals(); - @observable public IsDragging: boolean = false; - @observable public horizSnapLines: number[] = []; - @observable public vertSnapLines: number[] = []; - public StartWindowDrag: Opt<((e: any, dragDocs: Doc[]) => void)> = undefined; - public SetIsDragging(dragging: boolean) { runInAction(() => this.IsDragging = dragging); } - public GetIsDragging() { return this.IsDragging; } - public clearSnapLines() { - this.vertSnapLines.length = 0; - this.horizSnapLines.length = 0; - } - } + export let StartWindowDrag: Opt<((e: any, dragDocs: Doc[]) => void)> = undefined; export function Root() { const root = document.getElementById("root"); @@ -88,8 +77,8 @@ export namespace DragManager { return root; } export let AbortDrag: () => void = emptyFunction; - export type MoveFunction = (document: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean) => boolean; - export type RemoveFunction = (document: Doc) => boolean; + export type MoveFunction = (document: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean; + export type RemoveFunction = (document: Doc | Doc[]) => boolean; export interface DragDropDisposer { (): void; } export interface DragOptions { @@ -187,7 +176,7 @@ export namespace DragManager { element: HTMLElement, dropFunc: (e: Event, de: DropEvent) => void, doc?: Doc, - preDropFunc?: (e: Event, de: DropEvent) => void, + preDropFunc?: (e: Event, de: DropEvent, targetAction: dropActionType) => void, ): DragDropDisposer { if ("canDrop" in element.dataset) { throw new Error( @@ -198,10 +187,7 @@ export namespace DragManager { const handler = (e: Event) => dropFunc(e, (e as CustomEvent<DropEvent>).detail); const preDropHandler = (e: Event) => { const de = (e as CustomEvent<DropEvent>).detail; - if (de.complete.docDragData && doc?.targetDropAction) { - de.complete.docDragData.dropAction = StrCast(doc.targetDropAction) as dropActionType; - } - preDropFunc?.(e, de); + preDropFunc?.(e, de, StrCast(doc?.targetDropAction) as dropActionType); }; element.addEventListener("dashOnDrop", handler); doc && element.addEventListener("dashPreDrop", preDropHandler); @@ -267,10 +253,43 @@ export namespace DragManager { } export function SetSnapLines(horizLines: number[], vertLines: number[]) { - DragManager.Vals.Instance.horizSnapLines.push(...horizLines); - DragManager.Vals.Instance.vertSnapLines.push(...vertLines); + SnappingManager.setSnapLines(horizLines, vertLines); } + export function snapDragAspect(dragPt: number[], snapAspect: number) { + let closest = NumCast(Doc.UserDoc()["constants-snapThreshold"], 10); + let near = dragPt; + const intersect = (x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, x4: number, y4: number, dragx: number, dragy: number) => { + if ((x1 === x2 && y1 === y2) || (x3 === x4 && y3 === y4)) return undefined; // Check if none of the lines are of length 0 + const denominator = ((y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)); + if (denominator === 0) return undefined; // Lines are parallel + const ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denominator; + // let ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denominator; + //if (ua < 0 || ua > 1 || ub < 0 || ub > 1) return undefined; // is the intersection along the segments + + // Return a object with the x and y coordinates of the intersection + const x = x1 + ua * (x2 - x1); + const y = y1 + ua * (y2 - y1); + const dist = Math.sqrt((dragx - x) * (dragx - x) + (dragy - y) * (dragy - y)); + return { pt: [x, y], dist }; + }; + SnappingManager.vertSnapLines().forEach((xCoord, i) => { + const pt = intersect(dragPt[0], dragPt[1], dragPt[0] + snapAspect, dragPt[1] + 1, xCoord, -1, xCoord, 1, dragPt[0], dragPt[1]); + if (pt && pt.dist < closest) { + closest = pt.dist; + near = pt.pt; + } + }); + SnappingManager.horizSnapLines().forEach((yCoord, i) => { + const pt = intersect(dragPt[0], dragPt[1], dragPt[0] + snapAspect, dragPt[1] + 1, -1, yCoord, 1, yCoord, dragPt[0], dragPt[1]); + if (pt && pt.dist < closest) { + closest = pt.dist; + near = pt.pt; + } + }); + return { thisX: near[0], thisY: near[1] }; + } + // snap to the active snap lines - if oneAxis is set (eg, for maintaining aspect ratios), then it only snaps to the nearest horizontal/vertical line export function snapDrag(e: PointerEvent, xFromLeft: number, yFromTop: number, xFromRight: number, yFromBottom: number) { const snapThreshold = NumCast(Doc.UserDoc()["constants-snapThreshold"], 10); const snapVal = (pts: number[], drag: number, snapLines: number[]) => { @@ -284,10 +303,9 @@ export namespace DragManager { } return drag; }; - return { - thisX: snapVal([xFromLeft, xFromRight], e.pageX, DragManager.Vals.Instance.vertSnapLines), - thisY: snapVal([yFromTop, yFromBottom], e.pageY, DragManager.Vals.Instance.horizSnapLines) + thisX: snapVal([xFromLeft, xFromRight], e.pageX, SnappingManager.vertSnapLines()), + thisY: snapVal([yFromTop, yFromBottom], e.pageY, SnappingManager.horizSnapLines()) }; } export let docsBeingDragged: Doc[] = []; @@ -299,7 +317,7 @@ export namespace DragManager { dragDiv.style.pointerEvents = "none"; DragManager.Root().appendChild(dragDiv); } - DragManager.Vals.Instance.SetIsDragging(true); + SnappingManager.SetIsDragging(true); const scaleXs: number[] = []; const scaleYs: number[] = []; const xs: number[] = []; @@ -388,7 +406,7 @@ export namespace DragManager { } AbortDrag(); finishDrag?.(new DragCompleteEvent(true, dragData)); - DragManager.Vals.Instance.StartWindowDrag?.({ + DragManager.StartWindowDrag?.({ pageX: e.pageX, pageY: e.pageY, preventDefault: emptyFunction, @@ -415,19 +433,19 @@ export namespace DragManager { const endDrag = action(() => { document.removeEventListener("pointermove", moveHandler, true); document.removeEventListener("pointerup", upHandler); - DragManager.Vals.Instance.clearSnapLines(); + SnappingManager.clearSnapLines(); }); AbortDrag = () => { hideDragShowOriginalElements(); - DragManager.Vals.Instance.SetIsDragging(false); + SnappingManager.SetIsDragging(false); options?.dragComplete?.(new DragCompleteEvent(true, dragData)); endDrag(); }; const upHandler = (e: PointerEvent) => { hideDragShowOriginalElements(); dispatchDrag(eles, e, dragData, xFromLeft, yFromTop, xFromRight, yFromBottom, options, finishDrag); - DragManager.Vals.Instance.SetIsDragging(false); + SnappingManager.SetIsDragging(false); endDrag(); options?.dragComplete?.(new DragCompleteEvent(false, dragData)); }; diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts index d6db882b8..752c1cfc5 100644 --- a/src/client/util/DropConverter.ts +++ b/src/client/util/DropConverter.ts @@ -1,12 +1,12 @@ import { DragManager } from "./DragManager"; -import { Doc, DocListCast, Opt } from "../../new_fields/Doc"; +import { Doc, DocListCast, Opt } from "../../fields/Doc"; import { DocumentType } from "../documents/DocumentTypes"; -import { ObjectField } from "../../new_fields/ObjectField"; -import { StrCast } from "../../new_fields/Types"; +import { ObjectField } from "../../fields/ObjectField"; +import { StrCast } from "../../fields/Types"; import { Docs } from "../documents/Documents"; -import { ScriptField, ComputedField } from "../../new_fields/ScriptField"; -import { RichTextField } from "../../new_fields/RichTextField"; -import { ImageField } from "../../new_fields/URLField"; +import { ScriptField, ComputedField } from "../../fields/ScriptField"; +import { RichTextField } from "../../fields/RichTextField"; +import { ImageField } from "../../fields/URLField"; import { Scripting } from "./Scripting"; // diff --git a/src/client/util/History.ts b/src/client/util/History.ts index 2c53d7e52..7b7d4b835 100644 --- a/src/client/util/History.ts +++ b/src/client/util/History.ts @@ -1,4 +1,4 @@ -import { Doc } from "../../new_fields/Doc"; +import { Doc } from "../../fields/Doc"; import { DocServer } from "../DocServer"; import { MainView } from "../views/MainView"; import * as qs from 'query-string'; diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx index 438904688..1e8f07049 100644 --- a/src/client/util/Import & Export/DirectoryImportBox.tsx +++ b/src/client/util/Import & Export/DirectoryImportBox.tsx @@ -1,6 +1,6 @@ import "fs"; import React = require("react"); -import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../new_fields/Doc"; +import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../fields/Doc"; import { action, observable, runInAction, computed, reaction, IReactionDisposer } from "mobx"; import { FieldViewProps, FieldView } from "../../views/nodes/FieldView"; import Measure, { ContentRect } from "react-measure"; @@ -12,12 +12,12 @@ import { observer } from "mobx-react"; import ImportMetadataEntry, { keyPlaceholder, valuePlaceholder } from "./ImportMetadataEntry"; import { Utils } from "../../../Utils"; import { DocumentManager } from "../DocumentManager"; -import { Id } from "../../../new_fields/FieldSymbols"; -import { List } from "../../../new_fields/List"; -import { Cast, BoolCast, NumCast } from "../../../new_fields/Types"; -import { listSpec } from "../../../new_fields/Schema"; +import { Id } from "../../../fields/FieldSymbols"; +import { List } from "../../../fields/List"; +import { Cast, BoolCast, NumCast } from "../../../fields/Types"; +import { listSpec } from "../../../fields/Schema"; import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils"; -import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; +import { SchemaHeaderField } from "../../../fields/SchemaHeaderField"; import "./DirectoryImportBox.scss"; import { Networking } from "../../Network"; import { BatchedArray } from "array-batcher"; diff --git a/src/client/util/Import & Export/ImageUtils.ts b/src/client/util/Import & Export/ImageUtils.ts index c8d1530b3..072e5f58a 100644 --- a/src/client/util/Import & Export/ImageUtils.ts +++ b/src/client/util/Import & Export/ImageUtils.ts @@ -1,9 +1,9 @@ -import { Doc } from "../../../new_fields/Doc"; -import { ImageField } from "../../../new_fields/URLField"; -import { Cast, StrCast } from "../../../new_fields/Types"; +import { Doc } from "../../../fields/Doc"; +import { ImageField } from "../../../fields/URLField"; +import { Cast, StrCast } from "../../../fields/Types"; import { Docs } from "../../documents/Documents"; import { Networking } from "../../Network"; -import { Id } from "../../../new_fields/FieldSymbols"; +import { Id } from "../../../fields/FieldSymbols"; import { Utils } from "../../../Utils"; export namespace ImageUtils { diff --git a/src/client/util/Import & Export/ImportMetadataEntry.tsx b/src/client/util/Import & Export/ImportMetadataEntry.tsx index 8e1c50bea..dcb94e2e0 100644 --- a/src/client/util/Import & Export/ImportMetadataEntry.tsx +++ b/src/client/util/Import & Export/ImportMetadataEntry.tsx @@ -5,8 +5,8 @@ import { action, computed } from "mobx"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faPlus } from "@fortawesome/free-solid-svg-icons"; import { library } from '@fortawesome/fontawesome-svg-core'; -import { Doc } from "../../../new_fields/Doc"; -import { StrCast, BoolCast } from "../../../new_fields/Types"; +import { Doc } from "../../../fields/Doc"; +import { StrCast, BoolCast } from "../../../fields/Types"; interface KeyValueProps { Document: Doc; diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx index b1f136430..3a5345c80 100644 --- a/src/client/util/InteractionUtils.tsx +++ b/src/client/util/InteractionUtils.tsx @@ -87,15 +87,17 @@ export namespace InteractionUtils { return myTouches; } - export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number, color: string, width: number) { + export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number, color: string, width: string) { const pts = points.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X - left},${pt.Y - top} `, ""); return ( <polyline points={pts} style={{ fill: "none", - stroke: color, - strokeWidth: width + stroke: color ?? "rgb(0, 0, 0)", + strokeWidth: parseInt(width), + strokeLinejoin: "round", + strokeLinecap: "round" }} /> ); diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index e236c7f47..8e6ccf098 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -1,7 +1,7 @@ -import { Doc, DocListCast } from "../../new_fields/Doc"; -import { List } from "../../new_fields/List"; -import { listSpec } from "../../new_fields/Schema"; -import { Cast, StrCast } from "../../new_fields/Types"; +import { Doc, DocListCast } from "../../fields/Doc"; +import { List } from "../../fields/List"; +import { listSpec } from "../../fields/Schema"; +import { Cast, StrCast } from "../../fields/Types"; import { Docs } from "../documents/Documents"; import { Scripting } from "./Scripting"; diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index 12628273b..ab577315c 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -9,7 +9,7 @@ export { ts }; // @ts-ignore import * as typescriptlib from '!!raw-loader!./type_decls.d'; -import { Doc, Field } from '../../new_fields/Doc'; +import { Doc, Field } from '../../fields/Doc'; export interface ScriptSucccess { success: true; @@ -136,7 +136,7 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an if (batch) { batch.end(); } - onError && onError(error); + onError?.(error); return { success: false, error, result: errorVal }; } }; diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts index 6501da34a..5679c0a14 100644 --- a/src/client/util/SearchUtil.ts +++ b/src/client/util/SearchUtil.ts @@ -1,7 +1,7 @@ import * as rp from 'request-promise'; import { DocServer } from '../DocServer'; -import { Doc } from '../../new_fields/Doc'; -import { Id } from '../../new_fields/FieldSymbols'; +import { Doc } from '../../fields/Doc'; +import { Id } from '../../fields/FieldSymbols'; import { Utils } from '../../Utils'; import { DocumentType } from '../documents/DocumentTypes'; diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index d509168b6..bd743c28e 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -1,13 +1,14 @@ import { observable, action, runInAction, ObservableMap } from "mobx"; -import { Doc } from "../../new_fields/Doc"; +import { Doc } from "../../fields/Doc"; import { DocumentView } from "../views/nodes/DocumentView"; import { computedFn } from "mobx-utils"; -import { List } from "../../new_fields/List"; +import { List } from "../../fields/List"; export namespace SelectionManager { class Manager { + @observable IsDragging: boolean = false; SelectedDocuments: ObservableMap<DocumentView, boolean> = new ObservableMap(); @action @@ -53,6 +54,8 @@ export namespace SelectionManager { manager.SelectDoc(docView, ctrlPressed); } + export function SetIsDragging(dragging: boolean) { runInAction(() => manager.IsDragging = dragging); } + export function GetIsDragging() { return manager.IsDragging; } // computed functions, such as used in IsSelected generate errors if they're called outside of a // reaction context. Specifying the context with 'outsideReaction' allows an efficiency feature // to avoid unnecessary mobx invalidations when running inside a reaction. diff --git a/src/client/util/SerializationHelper.ts b/src/client/util/SerializationHelper.ts index 1f6b939d3..ad0309fa7 100644 --- a/src/client/util/SerializationHelper.ts +++ b/src/client/util/SerializationHelper.ts @@ -1,5 +1,5 @@ import { PropSchema, serialize, deserialize, custom, setDefaultModelSchema, getDefaultModelSchema } from "serializr"; -import { Field } from "../../new_fields/Doc"; +import { Field } from "../../fields/Doc"; import { ClientUtils } from "./ClientUtils"; let serializing = 0; diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index ff0b22381..0e15197c4 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -8,6 +8,8 @@ import { SelectionManager } from "./SelectionManager"; import "./SettingsManager.scss"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Networking } from "../Network"; +import { CurrentUserUtils } from "./CurrentUserUtils"; +import { Utils } from "../../Utils"; library.add(fa.faWindowClose); @@ -90,6 +92,9 @@ export default class SettingsManager extends React.Component<{}> { <div className="settings-type"> <button onClick={this.onClick} value="password">reset password</button> <button onClick={this.onClick} value="data">reset data</button> + <button onClick={() => window.location.assign(Utils.prepend("/logout"))}> + {CurrentUserUtils.GuestWorkspace ? "Exit" : "Log Out"} + </button> </div> {this.settingsContent === "password" ? <div className="settings-content"> diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index 3ce6de80d..dc67145fc 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -1,13 +1,13 @@ import { observable, runInAction, action } from "mobx"; import * as React from "react"; import MainViewModal from "../views/MainViewModal"; -import { Doc, Opt, DocCastAsync } from "../../new_fields/Doc"; +import { Doc, Opt, DocCastAsync } from "../../fields/Doc"; import { DocServer } from "../DocServer"; -import { Cast, StrCast } from "../../new_fields/Types"; +import { Cast, StrCast } from "../../fields/Types"; import * as RequestPromise from "request-promise"; import { Utils } from "../../Utils"; import "./SharingManager.scss"; -import { Id } from "../../new_fields/FieldSymbols"; +import { Id } from "../../fields/FieldSymbols"; import { observer } from "mobx-react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { library } from '@fortawesome/fontawesome-svg-core'; diff --git a/src/client/util/SnappingManager.ts b/src/client/util/SnappingManager.ts new file mode 100644 index 000000000..fc07e8ab4 --- /dev/null +++ b/src/client/util/SnappingManager.ts @@ -0,0 +1,29 @@ +import { observable, action, runInAction } from "mobx"; + +export namespace SnappingManager { + + class Manager { + @observable IsDragging: boolean = false; + @observable public horizSnapLines: number[] = []; + @observable public vertSnapLines: number[] = []; + @action public clearSnapLines() { + this.vertSnapLines = []; + this.horizSnapLines = []; + } + @action public setSnapLines(horizLines: number[], vertLines: number[]) { + this.horizSnapLines = horizLines; + this.vertSnapLines = vertLines; + } + } + + const manager = new Manager(); + + export function clearSnapLines() { manager.clearSnapLines(); } + export function setSnapLines(horizLines: number[], vertLines: number[]) { manager.setSnapLines(horizLines, vertLines); } + export function horizSnapLines() { return manager.horizSnapLines; } + export function vertSnapLines() { return manager.vertSnapLines; } + + export function SetIsDragging(dragging: boolean) { runInAction(() => manager.IsDragging = dragging); } + export function GetIsDragging() { return manager.IsDragging; } +} + diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index 11866aebe..1ba9fcc32 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -1,11 +1,14 @@ -import { Doc, Opt, DataSym } from '../../new_fields/Doc'; +import { Doc, Opt, DataSym, DocListCast } from '../../fields/Doc'; import { Touchable } from './Touchable'; import { computed, action, observable } from 'mobx'; -import { Cast, BoolCast, ScriptCast } from '../../new_fields/Types'; -import { listSpec } from '../../new_fields/Schema'; +import { Cast, BoolCast, ScriptCast } from '../../fields/Types'; +import { listSpec } from '../../fields/Schema'; import { InkingControl } from './InkingControl'; -import { InkTool } from '../../new_fields/InkField'; +import { InkTool } from '../../fields/InkField'; import { InteractionUtils } from '../util/InteractionUtils'; +import { List } from '../../fields/List'; +import { DateField } from '../../fields/DateField'; +import { ScriptField } from '../../fields/ScriptField'; /// DocComponent returns a generic React base class used by views that don't have 'fieldKey' props (e.g.,CollectionFreeFormDocumentView, DocumentView) @@ -92,29 +95,56 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T lookupField = (field: string) => ScriptCast((this.layoutDoc as any).lookupField)?.script.run({ self: this.layoutDoc, data: this.rootDoc, field: field }).result; + styleFromLayoutString = (scale: number) => { + const style: { [key: string]: any } = {}; + const divKeys = ["width", "height", "background", "top", "position"]; + const replacer = (match: any, expr: string, offset: any, string: any) => { // bcz: this executes a script to convert a property expression string: { script } into a value + return ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name, scale: "number" })?.script.run({ self: this.rootDoc, this: this.layoutDoc, scale }).result as string || ""; + }; + divKeys.map((prop: string) => { + const p = (this.props as any)[prop] as string; + p && (style[prop] = p?.replace(/{([^.'][^}']+)}/g, replacer)); + }); + return style; + } + protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; _annotationKey: string = "annotations"; - public set annotationKey(val: string) { this._annotationKey = val; } - public get annotationKey() { return this._annotationKey; } + public get annotationKey() { return this.fieldKey + "-" + this._annotationKey; } @action.bound - removeDocument(doc: Doc): boolean { - Doc.GetProto(doc).annotationOn = undefined; - const value = Cast(this.dataDoc[this.props.fieldKey + "-" + this._annotationKey], listSpec(Doc), []); - const index = value ? Doc.IndexOf(doc, value.map(d => d as Doc), true) : -1; - return index !== -1 && value && value.splice(index, 1) ? true : false; + removeDocument(doc: Doc | Doc[]): boolean { + const docs = doc instanceof Doc ? [doc] : doc; + 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); + return true; + } + return false; } // if the moved document is already in this overlay collection nothing needs to be done. // otherwise, if the document can be removed from where it was, it will then be added to this document's overlay collection. @action.bound - moveDocument(doc: Doc, targetCollection: Doc | undefined, addDocument: (doc: Doc) => boolean): boolean { + moveDocument(doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean { return Doc.AreProtosEqual(this.props.Document, targetCollection) ? true : this.removeDocument(doc) ? addDocument(doc) : false; } @action.bound - addDocument(doc: Doc): boolean { - doc.context = Doc.GetProto(doc).annotationOn = this.props.Document; - return Doc.AddDocToList(this.dataDoc, this.props.fieldKey + "-" + this._annotationKey, doc) ? true : false; + addDocument(doc: Doc | Doc[]): boolean { + const docs = doc instanceof Doc ? [doc] : doc; + docs.map(doc => doc.context = Doc.GetProto(doc).annotationOn = this.props.Document); + const targetDataDoc = this.props.Document[DataSym]; + const docList = DocListCast(targetDataDoc[this.annotationKey]); + const added = docs.filter(d => !docList.includes(d)); + if (added.length) { + 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; } whenActiveChanged = action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive)); diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index 3624cdb6d..2db5cd3ba 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -1,11 +1,11 @@ import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; -import { faArrowAltCircleDown, faPhotoVideo, faArrowAltCircleUp, faCheckCircle, faCloudUploadAlt, faLink, faShare, faStopCircle, faSyncAlt, faTag, faTimes } from '@fortawesome/free-solid-svg-icons'; +import { faArrowAltCircleDown, faPhotoVideo, faArrowAltCircleUp, faArrowAltCircleRight, faCheckCircle, faCloudUploadAlt, faLink, faShare, faStopCircle, faSyncAlt, faTag, faTimes } 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 { Doc, DocListCast } from "../../new_fields/Doc"; -import { RichTextField } from '../../new_fields/RichTextField'; -import { NumCast, StrCast } from "../../new_fields/Types"; +import { Doc, DocListCast } from "../../fields/Doc"; +import { RichTextField } from '../../fields/RichTextField'; +import { NumCast, StrCast, Cast } from "../../fields/Types"; import { emptyFunction, setupMoveUpEvents } from "../../Utils"; import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils'; import { UndoManager } from "../util/UndoManager"; @@ -22,6 +22,7 @@ import React = require("react"); import { DragManager } from '../util/DragManager'; import { MetadataEntryMenu } from './MetadataEntryMenu'; import GoogleAuthenticationManager from '../apis/GoogleAuthenticationManager'; +import { Docs } from '../documents/Documents'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -31,6 +32,7 @@ library.add(faTag); library.add(faTimes); library.add(faArrowAltCircleDown); library.add(faArrowAltCircleUp); +library.add(faArrowAltCircleRight); library.add(faStopCircle); library.add(faCheckCircle); library.add(faCloudUploadAlt); @@ -41,12 +43,16 @@ library.add(faPhotoVideo); const cloud: IconProp = "cloud-upload-alt"; const fetch: IconProp = "sync-alt"; +enum UtilityButtonState { + Default, + OpenRight, + OpenExternally +} + @observer -export class DocumentButtonBar extends React.Component<{ views: (DocumentView | undefined)[], stack?: any }, {}> { +export class DocumentButtonBar extends React.Component<{ views: () => (DocumentView | undefined)[], stack?: any }, {}> { private _linkButton = React.createRef<HTMLDivElement>(); private _dragRef = React.createRef<HTMLDivElement>(); - private _downX = 0; - private _downY = 0; private _pullAnimating = false; private _pushAnimating = false; private _pullColorAnimating = false; @@ -57,13 +63,13 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView | @observable public isAnimatingFetch = false; @observable public isAnimatingPulse = false; - @observable private openHover = false; + @observable private openHover: UtilityButtonState = UtilityButtonState.Default; @observable public static Instance: DocumentButtonBar; public static hasPushedHack = false; public static hasPulledHack = false; - constructor(props: { views: (DocumentView | undefined)[] }) { + constructor(props: { views: () => (DocumentView | undefined)[] }) { super(props); runInAction(() => DocumentButtonBar.Instance = this); } @@ -105,7 +111,7 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView | this._pullColorAnimating = false; }); - get view0() { return this.props.views?.[0]; } + get view0() { return this.props.views()?.[0]; } @action onLinkButtonMoved = (e: PointerEvent) => { @@ -165,14 +171,37 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView | 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={`${!dataDoc.unchanged ? "Pull from" : "Fetch"} Google Docs`} + 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={e => e.altKey && runInAction(() => this.openHover = true)} - onPointerLeave={action(() => this.openHover = false)} - onClick={e => { + 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(); - window.open(`https://docs.google.com/document/d/${dataDoc[GoogleRef]}/edit`); + 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; @@ -182,7 +211,14 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView | }}> <FontAwesomeIcon className="documentdecorations-icon" size="sm" style={{ WebkitAnimation: animation, MozAnimation: animation }} - icon={this.openHover ? "share" : dataDoc.unchanged === false ? (this.pullIcon as any) : fetch} + 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>; } @@ -193,16 +229,7 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView | 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 => { - if (isPinned) { - DockedFrameRenderer.UnpinDoc(targetDoc); - } - else { - targetDoc.sourceContext = this.view0?.props.ContainingCollectionDoc; // bcz: !! Shouldn't need this ... use search to lookup contexts dynamically - DockedFrameRenderer.PinDoc(targetDoc); - } - }}> + onClick={e => DockedFrameRenderer.PinDoc(targetDoc, isPinned)}> <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="map-pin" /> </div>; @@ -227,7 +254,7 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView | 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? */}> + 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> @@ -251,7 +278,7 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView | } onAliasButtonMoved = () => { if (this._dragRef.current) { - const dragDocView = this.props.views[0]!; + const dragDocView = this.view0!; const dragData = new DragManager.DocumentDragData([dragDocView.props.Document]); const [left, top] = dragDocView.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); dragData.embedDoc = true; @@ -270,16 +297,18 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView | get templateButton() { const view0 = this.view0; const templates: Map<Template, boolean> = new Map(); + const views = this.props.views(); Array.from(Object.values(Templates.TemplateList)).map(template => - templates.set(template, this.props.views.reduce((checked, doc) => checked || doc?.props.Document["_show" + template.Name] ? true : false, false as boolean))); - return !view0 ? (null) : <div title="Tap: Customize layout. Drag: Create alias" className="documentButtonBar-linkFlyout" ref={this._dragRef}> - <Flyout anchorPoint={anchorPoints.LEFT_TOP} onOpen={action(() => this._aliasDown = true)} onClose={action(() => this._aliasDown = false)} - content={!this._aliasDown ? (null) : <TemplateMenu docViews={this.props.views.filter(v => v).map(v => v as DocumentView)} templates={templates} />}> - <div className={"documentButtonBar-linkButton-empty"} ref={this._dragRef} onPointerDown={this.onAliasButtonDown} > - {<FontAwesomeIcon className="documentdecorations-icon" icon="edit" size="sm" />} - </div> - </Flyout> - </div>; + 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>; } render() { diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss index 4f34eb9e3..a4d4af2f0 100644 --- a/src/client/views/DocumentDecorations.scss +++ b/src/client/views/DocumentDecorations.scss @@ -72,6 +72,7 @@ $linkGap : 3px; grid-column-start: 5; grid-column-end: 7; border-radius: 100%; + background: dimgray; .borderRadiusTooltip { width: 10px; @@ -137,9 +138,10 @@ $linkGap : 3px; .documentDecorations-contextMenu { width: 25px; height: calc(100% + 8px); // 8px for the height of the top resizer bar - grid-column-start: 1; + grid-column-start: 2; grid-column-end : 2; pointer-events: all; + padding-left: 5px; } .documentDecorations-title { opacity: 1; @@ -184,9 +186,12 @@ $linkGap : 3px; position: absolute; left: 0px; top: 0px; - width: $MINIMIZED_ICON_SIZE; + width: 8px; height: $MINIMIZED_ICON_SIZE; max-height: 20px; + > svg { + margin:0; + } } .documentDecorations-background { diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index f8e77d90a..fc7c16296 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -3,10 +3,10 @@ import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote, faT import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, computed, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DataSym, Field } from "../../new_fields/Doc"; -import { Document } from '../../new_fields/documentSchemas'; -import { ScriptField } from '../../new_fields/ScriptField'; -import { Cast, StrCast, NumCast } from "../../new_fields/Types"; +import { Doc, DataSym, Field, WidthSym, HeightSym } from "../../fields/Doc"; +import { Document } from '../../fields/documentSchemas'; +import { ScriptField } from '../../fields/ScriptField'; +import { Cast, StrCast, NumCast } from "../../fields/Types"; import { Utils, setupMoveUpEvents, emptyFunction, returnFalse, simulateMouseClick } from "../../Utils"; import { DocUtils } from "../documents/Documents"; import { DocumentType } from '../documents/DocumentTypes'; @@ -17,10 +17,10 @@ import { DocumentButtonBar } from './DocumentButtonBar'; import './DocumentDecorations.scss'; import { DocumentView } from "./nodes/DocumentView"; import React = require("react"); -import { Id } from '../../new_fields/FieldSymbols'; +import { Id } from '../../fields/FieldSymbols'; import e = require('express'); import { CollectionDockingView } from './collections/CollectionDockingView'; -import { MainView } from './MainView'; +import { SnappingManager } from '../util/SnappingManager'; library.add(faCaretUp); library.add(faObjectGroup); @@ -173,8 +173,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> } @undoBatch @action - onCloseClick = async (e: PointerEvent) => { - if (e.button === 0) { + onCloseClick = async (e: PointerEvent | undefined) => { + if (!e?.button) { const recent = Cast(Doc.UserDoc().myRecentlyClosed, Doc) as Doc; const selected = SelectionManager.SelectedDocuments().slice(); SelectionManager.DeselectAll(); @@ -236,6 +236,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> return false; } + _initialAutoHeight = false; + _dragHeights = new Map<Doc, number>(); @action onPointerDown = (e: React.PointerEvent): void => { setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, (e) => { }); @@ -250,17 +252,44 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> } this._snapX = e.pageX; this._snapY = e.pageY; + this._initialAutoHeight = true; + DragManager.docsBeingDragged = SelectionManager.SelectedDocuments().map(dv => dv.rootDoc); + SelectionManager.SelectedDocuments().map(dv => { + this._dragHeights.set(dv.layoutDoc, NumCast(dv.layoutDoc._height)); + dv.layoutDoc._delayAutoHeight = dv.layoutDoc._height; + }); } onPointerMove = (e: PointerEvent, down: number[], move: number[]): boolean => { - const { thisX, thisY } = DragManager.snapDrag(e, -this._offX, -this._offY, this._offX, this._offY); - move[0] = thisX - this._snapX; - move[1] = thisY - this._snapY; - this._snapX = thisX; - this._snapY = thisY; + const first = SelectionManager.SelectedDocuments()[0]; + let thisPt = { thisX: e.clientX - this._offX, thisY: e.clientY - this._offY }; + const fixedAspect = first.layoutDoc._nativeWidth ? NumCast(first.layoutDoc._nativeWidth) / NumCast(first.layoutDoc._nativeHeight) : 0; + if (fixedAspect && (this._resizeHdlId === "documentDecorations-bottomRightResizer" || this._resizeHdlId === "documentDecorations-topLeftResizer")) { // need to generalize for bl and tr drag handles + const project = (p: number[], a: number[], b: number[]) => { + const atob = [b[0] - a[0], b[1] - a[1]]; + const atop = [p[0] - a[0], p[1] - a[1]]; + const len = atob[0] * atob[0] + atob[1] * atob[1]; + let dot = atop[0] * atob[0] + atop[1] * atob[1]; + const t = dot / len; + dot = (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]); + return [a[0] + atob[0] * t, a[1] + atob[1] * t]; + }; + const tl = first.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); + const drag = project([e.clientX + this._offX, e.clientY + this._offY], tl, [tl[0] + fixedAspect, tl[1] + 1]); + thisPt = DragManager.snapDragAspect(drag, fixedAspect); + } else { + thisPt = DragManager.snapDrag(e, -this._offX, -this._offY, this._offX, this._offY); + } - let dX = 0, dY = 0, dW = 0, dH = 0; + move[0] = thisPt.thisX - this._snapX; + move[1] = thisPt.thisY - this._snapY; + this._snapX = thisPt.thisX; + this._snapY = thisPt.thisY; + let dX = 0, dY = 0, dW = 0, dH = 0; + const unfreeze = () => + SelectionManager.SelectedDocuments().forEach(action((element: DocumentView) => + (element.rootDoc.type === DocumentType.RTF && element.layoutDoc._nativeHeight) && element.toggleNativeDimensions())); switch (this._resizeHdlId) { case "": break; case "documentDecorations-topLeftResizer": @@ -275,6 +304,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> dH = -move[1]; break; case "documentDecorations-topResizer": + unfreeze(); dY = -1; dH = -move[1]; break; @@ -288,26 +318,33 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> dH = move[1]; break; case "documentDecorations-bottomResizer": + unfreeze(); dH = move[1]; break; case "documentDecorations-leftResizer": + unfreeze(); dX = -1; dW = -move[0]; break; case "documentDecorations-rightResizer": + unfreeze(); dW = move[0]; break; } SelectionManager.SelectedDocuments().forEach(action((element: DocumentView) => { + if (e.ctrlKey && !element.props.Document._nativeHeight) element.toggleNativeDimensions(); if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) { const doc = Document(element.rootDoc); let nwidth = doc._nativeWidth || 0; let nheight = doc._nativeHeight || 0; const width = (doc._width || 0); - const height = (doc._height || (nheight / nwidth * width)); + let height = (doc._height || (nheight / nwidth * width)); const scale = element.props.ScreenToLocalTransform().Scale * element.props.ContentScaling(); if (nwidth && nheight) { + if (nwidth / nheight !== width / height) { + height = nheight / nwidth * width; + } if (Math.abs(dW) > Math.abs(dH)) dH = dW * nheight / nwidth; else dW = dH * nwidth / nheight; } @@ -351,7 +388,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> } else { dW && (doc._width = actualdW); dH && (doc._height = actualdH); - dH && doc._autoHeight && (doc._autoHeight = false); + dH && this._initialAutoHeight && (doc._autoHeight = this._initialAutoHeight = false); } } })); @@ -360,11 +397,18 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> @action onPointerUp = (e: PointerEvent): void => { + SelectionManager.SelectedDocuments().map(dv => { + if (NumCast(dv.layoutDoc._delayAutoHeight) < this._dragHeights.get(dv.layoutDoc)!) { + dv.nativeWidth > 0 && Doc.toggleNativeDimensions(dv.layoutDoc, dv.props.ContentScaling(), dv.panelWidth(), dv.panelHeight()); + dv.layoutDoc._autoHeight = true; + } + dv.layoutDoc._delayAutoHeight = undefined; + }); this._resizeHdlId = ""; this.Interacting = false; (e.button === 0) && this._resizeUndo?.end(); this._resizeUndo = undefined; - DragManager.Vals.Instance.clearSnapLines(); + SnappingManager.clearSnapLines(); } @computed @@ -403,7 +447,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> const darkScheme = Cast(Doc.UserDoc().activeWorkspace, Doc, null)?.darkScheme ? "dimgray" : undefined; const bounds = this.Bounds; const seldoc = SelectionManager.SelectedDocuments().length ? SelectionManager.SelectedDocuments()[0] : undefined; - if (DragManager.Vals.Instance.GetIsDragging() || bounds.r - bounds.x < 2 || bounds.x === Number.MAX_VALUE || !seldoc || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) { + if (SnappingManager.GetIsDragging() || bounds.r - bounds.x < 2 || bounds.x === Number.MAX_VALUE || !seldoc || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) { return (null); } const minimal = bounds.r - bounds.x < 100 ? true : false; @@ -432,12 +476,14 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> <FontAwesomeIcon size="lg" color={SelectionManager.SelectedDocuments()[0].props.Document.title === SelectionManager.SelectedDocuments()[0].props.Document[Id] ? "green" : undefined} icon="sticky-note"></FontAwesomeIcon> </div>} </> : - <div className="documentDecorations-title" onPointerDown={this.onTitleDown} > - {minimal ? (null) : <div className="documentDecorations-contextMenu" title="Show context menu" onPointerDown={this.onSettingsDown}> + <> + {minimal ? (null) : <div className="documentDecorations-contextMenu" key="menu" title="Show context menu" onPointerDown={this.onSettingsDown}> <FontAwesomeIcon size="lg" icon="cog" /> </div>} - <span style={{ width: "calc(100% - 25px)", display: "inline-block" }}>{`${this.selectionTitle}`}</span> - </div>; + <div className="documentDecorations-title" key="title" onPointerDown={this.onTitleDown} > + <span style={{ width: "calc(100% - 25px)", display: "inline-block" }}>{`${this.selectionTitle}`}</span> + </div> + </> bounds.x = Math.max(0, bounds.x - this._resizeBorderWidth / 2) + this._resizeBorderWidth / 2; bounds.y = Math.max(0, bounds.y - this._resizeBorderWidth / 2 - this._titleHeight) + this._resizeBorderWidth / 2 + this._titleHeight; @@ -498,7 +544,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> </div > <div className="link-button-container" style={{ left: bounds.x - this._resizeBorderWidth / 2, top: bounds.b + this._resizeBorderWidth / 2 }}> - <DocumentButtonBar views={SelectionManager.SelectedDocuments()} /> + <DocumentButtonBar views={SelectionManager.SelectedDocuments} /> </div> </div > ); diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index c51173ad3..e0e205df9 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -2,8 +2,8 @@ import React = require('react'); import { action, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as Autosuggest from 'react-autosuggest'; -import { ObjectField } from '../../new_fields/ObjectField'; -import { SchemaHeaderField } from '../../new_fields/SchemaHeaderField'; +import { ObjectField } from '../../fields/ObjectField'; +import { SchemaHeaderField } from '../../fields/SchemaHeaderField'; import "./EditableView.scss"; export interface EditableProps { diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index 4f8f9ed69..4352ac52c 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -6,14 +6,14 @@ import { computed, observable, action, runInAction, IReactionDisposer, reaction, import { GestureUtils } from "../../pen-gestures/GestureUtils"; import { InteractionUtils } from "../util/InteractionUtils"; import { InkingControl } from "./InkingControl"; -import { InkTool, InkData } from "../../new_fields/InkField"; -import { Doc } from "../../new_fields/Doc"; +import { InkTool, InkData } from "../../fields/InkField"; +import { Doc } from "../../fields/Doc"; import { LinkManager } from "../util/LinkManager"; import { DocUtils, Docs } from "../documents/Documents"; import { undoBatch } from "../util/UndoManager"; import { Scripting } from "../util/Scripting"; -import { FieldValue, Cast, NumCast, BoolCast } from "../../new_fields/Types"; -import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils"; +import { FieldValue, Cast, NumCast, BoolCast } from "../../fields/Types"; +import { CurrentUserUtils } from "../util/CurrentUserUtils"; import HorizontalPalette from "./Palette"; import { Utils, emptyPath, emptyFunction, returnFalse, returnOne, returnEmptyString, returnTrue, numberRange, returnZero } from "../../Utils"; import { DocumentView } from "./nodes/DocumentView"; @@ -22,9 +22,9 @@ import { DocumentContentsView } from "./nodes/DocumentContentsView"; import { CognitiveServices } from "../cognitive_services/CognitiveServices"; import { DocServer } from "../DocServer"; import htmlToImage from "html-to-image"; -import { ScriptField } from "../../new_fields/ScriptField"; -import { listSpec } from "../../new_fields/Schema"; -import { List } from "../../new_fields/List"; +import { ScriptField } from "../../fields/ScriptField"; +import { listSpec } from "../../fields/Schema"; +import { List } from "../../fields/List"; import { CollectionViewType } from "./collections/CollectionView"; import TouchScrollableMenu, { TouchScrollableMenuItem } from "./TouchScrollableMenu"; import MobileInterface from "../../mobile/MobileInterface"; @@ -38,10 +38,8 @@ import { SelectionManager } from "../util/SelectionManager"; export default class GestureOverlay extends Touchable { static Instance: GestureOverlay; - @observable public Color: string = "rgb(0, 0, 0)"; - @observable public Width: number = 2; @observable public SavedColor?: string; - @observable public SavedWidth?: number; + @observable public SavedWidth?: string; @observable public Tool: ToolglassTools = ToolglassTools.None; @observable private _thumbX?: number; @@ -711,12 +709,12 @@ export default class GestureOverlay extends Touchable { this._palette, [this._strokes.map(l => { const b = this.getBounds(l); - return <svg key={b.left} width={b.width} height={b.height} style={{ transform: `translate(${b.left}px, ${b.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000 }}> - {InteractionUtils.CreatePolyline(l, b.left, b.top, GestureOverlay.Instance.Color, GestureOverlay.Instance.Width)} + return <svg key={b.left} width={b.width} height={b.height} style={{ transform: `translate(${b.left}px, ${b.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000, overflow: "visible" }}> + {InteractionUtils.CreatePolyline(l, b.left, b.top, InkingControl.Instance.selectedColor, InkingControl.Instance.selectedWidth)} </svg>; }), - this._points.length <= 1 ? (null) : <svg width={B.width} height={B.height} style={{ transform: `translate(${B.left}px, ${B.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000 }}> - {InteractionUtils.CreatePolyline(this._points, B.left, B.top, GestureOverlay.Instance.Color, GestureOverlay.Instance.Width)} + this._points.length <= 1 ? (null) : <svg width={B.width} height={B.height} style={{ transform: `translate(${B.left}px, ${B.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000, overflow: "visible" }}> + {InteractionUtils.CreatePolyline(this._points, B.left, B.top, InkingControl.Instance.selectedColor, InkingControl.Instance.selectedWidth)} </svg>] ]; } @@ -806,16 +804,16 @@ Scripting.addGlobal(function setToolglass(tool: any) { }); Scripting.addGlobal(function setPen(width: any, color: any) { runInAction(() => { - GestureOverlay.Instance.SavedColor = GestureOverlay.Instance.Color; - GestureOverlay.Instance.Color = color; - GestureOverlay.Instance.SavedWidth = GestureOverlay.Instance.Width; - GestureOverlay.Instance.Width = width; + GestureOverlay.Instance.SavedColor = InkingControl.Instance.selectedColor; + InkingControl.Instance.updateSelectedColor(color); + GestureOverlay.Instance.SavedWidth = InkingControl.Instance.selectedWidth; + InkingControl.Instance.switchWidth(width); }); }); Scripting.addGlobal(function resetPen() { runInAction(() => { - GestureOverlay.Instance.Color = GestureOverlay.Instance.SavedColor ?? "rgb(0, 0, 0)"; - GestureOverlay.Instance.Width = GestureOverlay.Instance.SavedWidth ?? 2; + InkingControl.Instance.updateSelectedColor(GestureOverlay.Instance.SavedColor ?? "rgb(0, 0, 0)"); + InkingControl.Instance.switchWidth(GestureOverlay.Instance.SavedWidth ?? "2"); }); }); Scripting.addGlobal(function createText(text: any, x: any, y: any) { diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index 185222541..255142771 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -1,17 +1,26 @@ -import { UndoManager } from "../util/UndoManager"; +import { UndoManager, undoBatch } from "../util/UndoManager"; import { SelectionManager } from "../util/SelectionManager"; import { CollectionDockingView } from "./collections/CollectionDockingView"; import { MainView } from "./MainView"; import { DragManager } from "../util/DragManager"; import { action, runInAction } from "mobx"; -import { Doc } from "../../new_fields/Doc"; +import { Doc, DocListCast } from "../../fields/Doc"; import { DictationManager } from "../util/DictationManager"; import SharingManager from "../util/SharingManager"; -import { Cast, PromiseValue, NumCast } from "../../new_fields/Types"; -import { ScriptField } from "../../new_fields/ScriptField"; +import { Cast, PromiseValue, NumCast } from "../../fields/Types"; +import { ScriptField } from "../../fields/ScriptField"; import { InkingControl } from "./InkingControl"; -import { InkTool } from "../../new_fields/InkField"; +import { InkTool } from "../../fields/InkField"; import { DocumentView } from "./nodes/DocumentView"; +import GoogleAuthenticationManager from "../apis/GoogleAuthenticationManager"; +import { CollectionFreeFormView } from "./collections/collectionFreeForm/CollectionFreeFormView"; +import { MarqueeView } from "./collections/collectionFreeForm/MarqueeView"; +import { Id } from "../../fields/FieldSymbols"; +import { DocumentDecorations } from "./DocumentDecorations"; +import { DocumentType } from "../documents/DocumentTypes"; +import { DocServer } from "../DocServer"; +import { List } from "../../fields/List"; +import { DateField } from "../../fields/DateField"; const modifiers = ["control", "meta", "shift", "alt"]; type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo | Promise<KeyControlInfo>; @@ -64,6 +73,9 @@ export default class KeyManager { private unmodified = action((keyname: string, e: KeyboardEvent) => { switch (keyname) { + case " ": + MarqueeView.DragMarquee = !MarqueeView.DragMarquee; + break; case "escape": const main = MainView.Instance; InkingControl.Instance.switchTool(InkTool.None); @@ -79,6 +91,7 @@ export default class KeyManager { SelectionManager.DeselectAll(); DictationManager.Controls.stop(); // RecommendationsBox.Instance.closeMenu(); + GoogleAuthenticationManager.Instance.cancel(); SharingManager.Instance.close(); break; case "delete": @@ -233,11 +246,30 @@ export default class KeyManager { break; case "a": case "v": - case "x": - case "c": stopPropagation = false; preventDefault = false; break; + case "x": + if (SelectionManager.SelectedDocuments().length) { + const bds = DocumentDecorations.Instance.Bounds; + const pt = [bds.x + (bds.r - bds.x) / 2, bds.y + (bds.b - bds.y) / 2]; + const text = `__DashDocId(${pt[0]},${pt[1]}):` + SelectionManager.SelectedDocuments().map(dv => dv.Document[Id]).join(":"); + SelectionManager.SelectedDocuments().length && navigator.clipboard.writeText(text); + DocumentDecorations.Instance.onCloseClick(undefined); + stopPropagation = false; + preventDefault = false; + } + break; + case "c": + if (SelectionManager.SelectedDocuments().length) { + const bds = DocumentDecorations.Instance.Bounds; + const pt = SelectionManager.SelectedDocuments()[0].props.ScreenToLocalTransform().transformPoint(bds.x + (bds.r - bds.x) / 2, bds.y + (bds.b - bds.y) / 2); + const text = `__DashDocId(${pt?.[0] || 0},${pt?.[1] || 0}):` + SelectionManager.SelectedDocuments().map(dv => dv.Document[Id]).join(":"); + SelectionManager.SelectedDocuments().length && navigator.clipboard.writeText(text); + stopPropagation = false; + preventDefault = false; + } + break; } return { @@ -246,6 +278,36 @@ export default class KeyManager { }; }); + public paste(e: ClipboardEvent) { + if (e.clipboardData?.getData("text/plain") !== "" && e.clipboardData?.getData("text/plain").startsWith("__DashDocId(")) { + const first = SelectionManager.SelectedDocuments().length ? SelectionManager.SelectedDocuments()[0] : undefined; + if (first?.props.Document.type === DocumentType.COL) { + const docids = e.clipboardData.getData("text/plain").split(":"); + let count = 1; + const list: Doc[] = []; + 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 => { + count++; + if (doc instanceof Doc) { + list.push(doc); + } + if (count === docids.length) { + const added = list.filter(d => !docList.includes(d)); + if (added.length) { + added.map(doc => doc.context = targetDataDoc); + undoBatch(() => { + targetDataDoc[fieldKey] = new List<Doc>([...docList, ...added]); + targetDataDoc[fieldKey + "-lastModified"] = new DateField(new Date(Date.now())); + })(); + } + } + })); + } + } + } + async printClipboard() { const text: string = await navigator.clipboard.readText(); } diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx index 70ea955e1..41ee36d05 100644 --- a/src/client/views/InkingControl.tsx +++ b/src/client/views/InkingControl.tsx @@ -1,9 +1,9 @@ import { action, computed, observable } from "mobx"; import { ColorState } from 'react-color'; -import { Doc } from "../../new_fields/Doc"; -import { InkTool } from "../../new_fields/InkField"; -import { FieldValue, NumCast, StrCast } from "../../new_fields/Types"; -import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils"; +import { Doc } from "../../fields/Doc"; +import { InkTool } from "../../fields/InkField"; +import { FieldValue, NumCast, StrCast } from "../../fields/Types"; +import { CurrentUserUtils } from "../util/CurrentUserUtils"; import { Scripting } from "../util/Scripting"; import { SelectionManager } from "../util/SelectionManager"; import { undoBatch } from "../util/UndoManager"; @@ -13,8 +13,8 @@ import { FormattedTextBox } from "./nodes/formattedText/FormattedTextBox"; export class InkingControl { @observable static Instance: InkingControl; @computed private get _selectedTool(): InkTool { return FieldValue(NumCast(Doc.UserDoc().inkTool)) ?? InkTool.None; } - @computed private get _selectedColor(): string { return GestureOverlay.Instance.Color ?? FieldValue(StrCast(Doc.UserDoc().inkColor)) ?? "rgb(244, 67, 54)"; } - @computed private get _selectedWidth(): string { return GestureOverlay.Instance.Width?.toString() ?? FieldValue(StrCast(Doc.UserDoc().inkWidth)) ?? "5"; } + @computed private get _selectedColor(): string { return CurrentUserUtils.ActivePen ? FieldValue(StrCast(CurrentUserUtils.ActivePen.backgroundColor)) ?? "rgb(0, 0, 0)" : "rgb(0, 0, 0)"; } + @computed private get _selectedWidth(): string { return FieldValue(StrCast(Doc.UserDoc().inkWidth)) ?? "2"; } @observable public _open: boolean = false; constructor() { @@ -34,7 +34,9 @@ export class InkingControl { @undoBatch switchColor = action((color: ColorState): void => { - Doc.UserDoc().inkColor = color.hex + (color.rgb.a !== undefined ? this.decimalToHexString(Math.round(color.rgb.a * 255)) : "ff"); + Doc.UserDoc().backgroundColor = color.hex.startsWith("#") ? + color.hex + (color.rgb.a ? this.decimalToHexString(Math.round(color.rgb.a * 255)) : "ff") : color.hex; + CurrentUserUtils.ActivePen && (CurrentUserUtils.ActivePen.backgroundColor = color.hex); if (InkingControl.Instance.selectedTool === InkTool.None) { const selected = SelectionManager.SelectedDocuments(); @@ -44,20 +46,20 @@ export class InkingControl { view.props.Document.isTemplateForField ? view.props.Document : Doc.GetProto(view.props.Document); if (targetDoc) { if (StrCast(Doc.Layout(view.props.Document).layout).indexOf("FormattedTextBox") !== -1 && FormattedTextBox.HadSelection) { - Doc.Layout(view.props.Document).color = Doc.UserDoc().inkColor; + Doc.Layout(view.props.Document).color = Doc.UserDoc().bacgroundColor; } else { - Doc.Layout(view.props.Document)._backgroundColor = Doc.UserDoc().inkColor; // '_backgroundColor' is template specific. 'backgroundColor' would apply to all templates, but has no UI at the moment + Doc.Layout(view.props.Document)._backgroundColor = Doc.UserDoc().backgroundColor; // '_backgroundColor' is template specific. 'backgroundColor' would apply to all templates, but has no UI at the moment } } }); - } else { - CurrentUserUtils.ActivePen && (CurrentUserUtils.ActivePen.backgroundColor = this._selectedColor); } }); @action switchWidth = (width: string): void => { // this._selectedWidth = width; - Doc.UserDoc().inkWidth = width; + if (!isNaN(parseInt(width))) { + Doc.UserDoc().inkWidth = width; + } } @computed diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 7a318d5c2..8938e8b6c 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -1,14 +1,14 @@ import { observer } from "mobx-react"; -import { documentSchema } from "../../new_fields/documentSchemas"; -import { InkData, InkField, InkTool } from "../../new_fields/InkField"; -import { makeInterface } from "../../new_fields/Schema"; -import { Cast, StrCast, NumCast } from "../../new_fields/Types"; +import { documentSchema } from "../../fields/documentSchemas"; +import { InkData, InkField, InkTool } from "../../fields/InkField"; +import { makeInterface } from "../../fields/Schema"; +import { Cast, StrCast, NumCast } from "../../fields/Types"; import { ViewBoxBaseComponent } from "./DocComponent"; import { InkingControl } from "./InkingControl"; import "./InkingStroke.scss"; import { FieldView, FieldViewProps } from "./nodes/FieldView"; import React = require("react"); -import { TraceMobx } from "../../new_fields/util"; +import { TraceMobx } from "../../fields/util"; import { InteractionUtils } from "../util/InteractionUtils"; import { ContextMenu } from "./ContextMenu"; import { CognitiveServices } from "../cognitive_services/CognitiveServices"; @@ -40,7 +40,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume const bottom = Math.max(...ys); const points = InteractionUtils.CreatePolyline(data, left, top, StrCast(this.layoutDoc.color, InkingControl.Instance.selectedColor), - NumCast(this.layoutDoc.strokeWidth, parseInt(InkingControl.Instance.selectedWidth))); + StrCast(this.layoutDoc.strokeWidth, InkingControl.Instance.selectedWidth)); const width = right - left; const height = bottom - top; const scaleX = this.props.PanelWidth() / width; diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index b21eb9c8f..17c001971 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -1,6 +1,6 @@ import { MainView } from "./MainView"; import { Docs } from "../documents/Documents"; -import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils"; +import { CurrentUserUtils } from "../util/CurrentUserUtils"; import * as ReactDOM from 'react-dom'; import * as React from 'react'; import { DocServer } from "../DocServer"; diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss index 04288a9e1..dca2a1e3e 100644 --- a/src/client/views/MainView.scss +++ b/src/client/views/MainView.scss @@ -22,6 +22,15 @@ z-index: 1; } +.mainView-snapLines { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events:none; +} + .mainView-container, .mainView-container-dark { input { color: unset !important; @@ -98,7 +107,9 @@ position: absolute; left: 0; bottom: 0; - font-size: 8px; + border-radius: 25%; + margin-left: -5px; + background: darkblue; } .mainView-settings:hover { diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 71c2bf245..978cf7868 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -1,18 +1,25 @@ import { library } from '@fortawesome/fontawesome-svg-core'; -import { faTerminal, faFile as fileSolid, faLocationArrow, faSearch, faFileDownload, faStop, faCalculator, faWindowMaximize, faAddressCard, faQuestionCircle, faArrowLeft, faArrowRight, faArrowDown, faArrowUp, faBolt, faBullseye, faCaretUp, faCat, faCheck, faChevronRight, faClipboard, faClone, faCloudUploadAlt, faCommentAlt, faCompressArrowsAlt, faCut, faEllipsisV, faEraser, faExclamation, faFileAlt, faFileAudio, faFilePdf, faFilm, faFilter, faFont, faGlobeAsia, faHighlighter, faLongArrowAltRight, faMicrophone, faMousePointer, faMusic, faObjectGroup, faPause, faPen, faPenNib, faPhone, faPlay, faPortrait, faRedoAlt, faStamp, faStickyNote, faThumbtack, faTree, faTv, faUndoAlt, faVideo } from '@fortawesome/free-solid-svg-icons'; +import { + faTrashAlt, faAngleRight, faBell, faTrash, faCamera, faExpand, faCaretDown, faCaretRight, faCaretSquareDown, faCaretSquareRight, faArrowsAltH, faPlus, faMinus, + faTerminal, faToggleOn, faFile as fileSolid, faExternalLinkAlt, faLocationArrow, faSearch, faFileDownload, faStop, faCalculator, faWindowMaximize, faAddressCard, + faQuestionCircle, faArrowLeft, faArrowRight, faArrowDown, faArrowUp, faBolt, faBullseye, faCaretUp, faCat, faCheck, faChevronRight, faClipboard, faClone, faCloudUploadAlt, + faCommentAlt, faCompressArrowsAlt, faCut, faEllipsisV, faEraser, faExclamation, faFileAlt, faFileAudio, faFilePdf, faFilm, faFilter, faFont, faGlobeAsia, faHighlighter, + faLongArrowAltRight, faMicrophone, faMousePointer, faMusic, faObjectGroup, faPause, faPen, faPenNib, faPhone, faPlay, faPortrait, faRedoAlt, faStamp, faStickyNote, + faThumbtack, faTree, faTv, faUndoAlt, faVideo +} from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, configure, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import "normalize.css"; import * as React from 'react'; import Measure from 'react-measure'; -import { Doc, DocListCast, Field, Opt } from '../../new_fields/Doc'; -import { Id } from '../../new_fields/FieldSymbols'; -import { List } from '../../new_fields/List'; -import { listSpec } from '../../new_fields/Schema'; -import { BoolCast, Cast, FieldValue, StrCast } from '../../new_fields/Types'; -import { TraceMobx } from '../../new_fields/util'; -import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils'; +import { Doc, DocListCast, Field, Opt } from '../../fields/Doc'; +import { Id } from '../../fields/FieldSymbols'; +import { List } from '../../fields/List'; +import { listSpec } from '../../fields/Schema'; +import { BoolCast, Cast, FieldValue, StrCast } from '../../fields/Types'; +import { TraceMobx } from '../../fields/util'; +import { CurrentUserUtils } from '../util/CurrentUserUtils'; import { emptyFunction, emptyPath, returnFalse, returnOne, returnZero, returnTrue, Utils } from '../../Utils'; import GoogleAuthenticationManager from '../apis/GoogleAuthenticationManager'; import { DocServer } from '../DocServer'; @@ -41,9 +48,10 @@ import { RadialMenu } from './nodes/RadialMenu'; import { OverlayView } from './OverlayView'; import PDFMenu from './pdf/PDFMenu'; import { PreviewCursor } from './PreviewCursor'; -import { ScriptField } from '../../new_fields/ScriptField'; +import { ScriptField } from '../../fields/ScriptField'; import { TimelineMenu } from './animationtimeline/TimelineMenu'; import { DragManager } from '../util/DragManager'; +import { SnappingManager } from '../util/SnappingManager'; @observer export class MainView extends React.Component { @@ -75,12 +83,14 @@ export class MainView extends React.Component { firstScriptTag.parentNode!.insertBefore(tag, firstScriptTag); window.removeEventListener("keydown", KeyManager.Instance.handle); window.addEventListener("keydown", KeyManager.Instance.handle); + window.addEventListener("paste", KeyManager.Instance.paste as any); } componentWillUnMount() { window.removeEventListener("keydown", KeyManager.Instance.handle); window.removeEventListener("pointerdown", this.globalPointerDown); window.removeEventListener("pointerup", this.globalPointerUp); + window.removeEventListener("paste", KeyManager.Instance.paste as any); } constructor(props: Readonly<{}>) { @@ -104,7 +114,20 @@ export class MainView extends React.Component { } } + library.add(faTrashAlt); + library.add(faAngleRight); + library.add(faBell); + library.add(faTrash); + library.add(faCamera); + library.add(faExpand); + library.add(faCaretDown); + library.add(faCaretRight); + library.add(faCaretSquareDown); + library.add(faCaretSquareRight); + library.add(faArrowsAltH); + library.add(faPlus, faMinus); library.add(faTerminal); + library.add(faToggleOn); library.add(faLocationArrow); library.add(faSearch); library.add(fileSolid); @@ -161,6 +184,7 @@ export class MainView extends React.Component { library.add(faPhone); library.add(faClipboard); library.add(faStamp); + library.add(faExternalLinkAlt); this.initEventListeners(); this.initAuthenticationRouters(); } @@ -225,10 +249,10 @@ export class MainView extends React.Component { _width: this._panelWidth * .7, _height: this._panelHeight, title: "Collection " + workspaceCount, + _LODdisable: true }; const freeformDoc = CurrentUserUtils.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions); - Doc.AddDocToList(Doc.GetProto(Doc.UserDoc().myDocuments as Doc), "data", freeformDoc); - const mainDoc = Docs.Create.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600, path: [Doc.UserDoc().myDocuments as Doc] }], { title: `Workspace ${workspaceCount}` }, id, "row"); + const mainDoc = Docs.Create.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600, path: [Doc.UserDoc().myCatalog as Doc] }], { title: `Workspace ${workspaceCount}` }, id, "row"); const toggleTheme = ScriptField.MakeScript(`self.darkScheme = !self.darkScheme`); mainDoc.contextMenuScripts = new List<ScriptField>([toggleTheme!]); @@ -297,7 +321,7 @@ export class MainView extends React.Component { defaultBackgroundColors = (doc: Doc) => { if (this.darkScheme) { - switch (doc.type) { + switch (doc?.type) { case DocumentType.RTF || DocumentType.LABEL || DocumentType.BUTTON: return "#2d2d2d"; case DocumentType.LINK: case DocumentType.COL: { @@ -306,7 +330,7 @@ export class MainView extends React.Component { default: return "black"; } } else { - switch (doc.type) { + switch (doc?.type) { case DocumentType.RTF: return "#f1efeb"; case DocumentType.BUTTON: case DocumentType.LABEL: return "lightgray"; @@ -469,10 +493,7 @@ export class MainView extends React.Component { ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} /> <button className="mainView-settings" key="settings" onClick={() => SettingsManager.Instance.open()}> - Settings - </button> - <button className="mainView-logout" key="logout" onClick={() => window.location.assign(Utils.prepend("/logout"))}> - {CurrentUserUtils.GuestWorkspace ? "Exit" : "Log Out"} + <FontAwesomeIcon icon="cog" size="lg" /> </button> </div> {this.docButtons} @@ -517,9 +538,9 @@ export class MainView extends React.Component { return !this._flyoutTranslate ? (<div className="mainView-expandFlyoutButton" title="Re-attach sidebar" onPointerDown={MainView.expandFlyout}><FontAwesomeIcon icon="chevron-right" color="grey" size="lg" /></div>) : (null); } - addButtonDoc = (doc: Doc) => Doc.AddDocToList(Doc.UserDoc().dockedBtns as Doc, "data", doc); - remButtonDoc = (doc: Doc) => Doc.RemoveDocFromList(Doc.UserDoc().dockedBtns as Doc, "data", doc); - moveButtonDoc = (doc: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean) => this.remButtonDoc(doc) && addDocument(doc); + addButtonDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg: boolean, doc) => flg && Doc.AddDocToList(Doc.UserDoc().dockedBtns as Doc, "data", doc), true); + remButtonDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg: boolean, doc) => flg && Doc.RemoveDocFromList(Doc.UserDoc().dockedBtns as Doc, "data", doc), true); + moveButtonDoc = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => this.remButtonDoc(doc) && addDocument(doc); buttonBarXf = () => { if (!this._docBtnRef.current) return Transform.Identity(); @@ -575,6 +596,15 @@ export class MainView extends React.Component { return this._mainViewRef; } + @computed get snapLines() { + return <div className="mainView-snapLines"> + <svg style={{ width: "100%", height: "100%" }}> + {SnappingManager.horizSnapLines().map(l => <line x1="0" y1={l} x2="2000" y2={l} stroke="black" opacity={0.3} strokeWidth={0.5} strokeDasharray={"1 1"} />)} + {SnappingManager.vertSnapLines().map(l => <line y1="0" x1={l} y2="2000" x2={l} stroke="black" opacity={0.3} strokeWidth={0.5} strokeDasharray={"1 1"} />)} + </svg> + </div>; + } + render() { return (<div className={"mainView-container" + (this.darkScheme ? "-dark" : "")} ref={this._mainViewRef}> <DictationOverlay /> @@ -592,14 +622,8 @@ export class MainView extends React.Component { <MarqueeOptionsMenu /> <RichTextMenu /> <OverlayView /> - {// TO VIEW SNAP LINES - <div className="snapLines" style={{ position: "absolute", top: 0, left: 0, width: "100%", height: "100%", pointerEvents: "none" }}> - <svg style={{ width: "100%", height: "100%" }}> - {DragManager.Vals.Instance.horizSnapLines.map((l: any) => <line x1="0" y1={l} x2="2000" y2={l} stroke="black" opacity={0.3} strokeWidth={0.5} strokeDasharray={"1 1"} />)} - {DragManager.Vals.Instance.vertSnapLines.map((l: any) => <line y1="0" x1={l} y2="2000" x2={l} stroke="black" opacity={0.3} strokeWidth={0.5} strokeDasharray={"1 1"} />)} - </svg> - </div>} <TimelineMenu /> + {this.snapLines} </div >); } } diff --git a/src/client/views/MainViewModal.tsx b/src/client/views/MainViewModal.tsx index 9198fe3e3..a7bd5882d 100644 --- a/src/client/views/MainViewModal.tsx +++ b/src/client/views/MainViewModal.tsx @@ -4,7 +4,7 @@ import "./MainViewModal.scss"; export interface MainViewOverlayProps { isDisplayed: boolean; interactive: boolean; - contents: string | JSX.Element; + contents: string | JSX.Element | null; dialogueBoxStyle?: React.CSSProperties; overlayStyle?: React.CSSProperties; dialogueBoxDisplayedOpacity?: number; diff --git a/src/client/views/MainViewNotifs.tsx b/src/client/views/MainViewNotifs.tsx index 82e07c449..05f890485 100644 --- a/src/client/views/MainViewNotifs.tsx +++ b/src/client/views/MainViewNotifs.tsx @@ -2,7 +2,7 @@ import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import "normalize.css"; import * as React from 'react'; -import { Doc, DocListCast, Opt } from '../../new_fields/Doc'; +import { Doc, DocListCast, Opt } from '../../fields/Doc'; import { emptyFunction } from '../../Utils'; import { SetupDrag } from '../util/DragManager'; import "./MainViewNotifs.scss"; diff --git a/src/client/views/MetadataEntryMenu.tsx b/src/client/views/MetadataEntryMenu.tsx index 8bc80ed06..e100d3f52 100644 --- a/src/client/views/MetadataEntryMenu.tsx +++ b/src/client/views/MetadataEntryMenu.tsx @@ -3,7 +3,7 @@ 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 '../../new_fields/Doc'; +import { Doc, Field, DocListCastAsync } from '../../fields/Doc'; import * as Autosuggest from 'react-autosuggest'; import { undoBatch } from '../util/UndoManager'; import { emptyFunction, emptyPath } from '../../Utils'; diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx index 20aa14f84..bfa44fe47 100644 --- a/src/client/views/OverlayView.tsx +++ b/src/client/views/OverlayView.tsx @@ -1,9 +1,9 @@ import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; import * as React from "react"; -import { Doc, DocListCast } from "../../new_fields/Doc"; -import { Id } from "../../new_fields/FieldSymbols"; -import { NumCast } from "../../new_fields/Types"; +import { Doc, DocListCast, Opt } from "../../fields/Doc"; +import { Id } from "../../fields/FieldSymbols"; +import { NumCast } from "../../fields/Types"; import { emptyFunction, emptyPath, returnEmptyString, returnFalse, returnOne, returnTrue, returnZero, Utils } from "../../Utils"; import { Transform } from "../util/Transform"; import { CollectionFreeFormLinksView } from "./collections/collectionFreeForm/CollectionFreeFormLinksView"; @@ -214,4 +214,6 @@ export class OverlayView extends React.Component { } } // bcz: ugh ... want to be able to pass ScriptingRepl as tag argument, but that doesn't seem to work.. runtime error -Scripting.addGlobal(function addOverlayWindow(Tag: string, options: OverlayElementOptions) { const x = <ScriptingRepl />; OverlayView.Instance.addWindow(x, options); });
\ No newline at end of file +Scripting.addGlobal(function addOverlayWindow(type: string, options: OverlayElementOptions) { + OverlayView.Instance.addWindow(<ScriptingRepl />, options); +});
\ No newline at end of file diff --git a/src/client/views/Palette.tsx b/src/client/views/Palette.tsx index 63744cb50..108eb83d6 100644 --- a/src/client/views/Palette.tsx +++ b/src/client/views/Palette.tsx @@ -1,8 +1,8 @@ import { IReactionDisposer, observable, reaction } from "mobx"; import { observer } from "mobx-react"; import * as React from "react"; -import { Doc } from "../../new_fields/Doc"; -import { NumCast } from "../../new_fields/Types"; +import { Doc } from "../../fields/Doc"; +import { NumCast } from "../../fields/Types"; import { emptyFunction, emptyPath, returnEmptyString, returnZero, returnFalse, returnOne, returnTrue } from "../../Utils"; import { Transform } from "../util/Transform"; import { DocumentView } from "./nodes/DocumentView"; diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx index df30c1215..dd65681d4 100644 --- a/src/client/views/PreviewCursor.tsx +++ b/src/client/views/PreviewCursor.tsx @@ -4,15 +4,18 @@ import "normalize.css"; import * as React from 'react'; import "./PreviewCursor.scss"; import { Docs } from '../documents/Documents'; -import { Doc } from '../../new_fields/Doc'; +import { Doc } from '../../fields/Doc'; import { Transform } from "../util/Transform"; +import { DocServer } from '../DocServer'; +import { undoBatch } from '../util/UndoManager'; +import { NumCast } from '../../fields/Types'; @observer export class PreviewCursor extends React.Component<{}> { static _onKeyPress?: (e: KeyboardEvent) => void; static _getTransform: () => Transform; + static _addDocument: (doc: Doc | Doc[]) => void; static _addLiveTextDoc: (doc: Doc) => void; - static _addDocument: (doc: Doc) => boolean; static _nudge: (x: number, y: number) => boolean; @observable static _clickPoint = [0, 0]; @observable public static Visible = false; @@ -27,50 +30,77 @@ export class PreviewCursor extends React.Component<{}> { const newPoint = PreviewCursor._getTransform().transformPoint(PreviewCursor._clickPoint[0], PreviewCursor._clickPoint[1]); runInAction(() => PreviewCursor.Visible = false); + // tests for URL and makes web document + const re: any = /^https?:\/\//g; if (e.clipboardData.getData("text/plain") !== "") { // tests for youtube and makes video document if (e.clipboardData.getData("text/plain").indexOf("www.youtube.com/watch") !== -1) { const url = e.clipboardData.getData("text/plain").replace("youtube.com/watch?v=", "youtube.com/embed/"); - return PreviewCursor._addDocument(Docs.Create.VideoDocument(url, { + undoBatch(() => PreviewCursor._addDocument(Docs.Create.VideoDocument(url, { title: url, _width: 400, _height: 315, _nativeWidth: 600, _nativeHeight: 472.5, x: newPoint[0], y: newPoint[1] - })); + })))(); } - // tests for URL and makes web document - const re: any = /^https?:\/\//g; - if (re.test(e.clipboardData.getData("text/plain"))) { + else if (re.test(e.clipboardData.getData("text/plain"))) { const url = e.clipboardData.getData("text/plain"); - return PreviewCursor._addDocument(Docs.Create.WebDocument(url, { - title: url, _width: 500, _height: 300, + undoBatch(() => PreviewCursor._addDocument(Docs.Create.WebDocument(url, { + title: url, _width: 500, _height: 300, UseCors: true, // nativeWidth: 300, nativeHeight: 472.5, x: newPoint[0], y: newPoint[1] - })); + })))(); } - // creates text document - return PreviewCursor._addLiveTextDoc(Docs.Create.TextDocument("", { - _width: 500, - limitHeight: 400, - _autoHeight: true, - x: newPoint[0], - y: newPoint[1], - title: "-pasted text-" - })); - } - //pasting in images - if (e.clipboardData.getData("text/html") !== "" && e.clipboardData.getData("text/html").includes("<img src=")) { - const re: any = /<img src="(.*?)"/g; - const arr: any[] = re.exec(e.clipboardData.getData("text/html")); + else if (e.clipboardData.getData("text/plain").startsWith("__DashDocId(")) { + const docids = e.clipboardData.getData("text/plain").split(":"); + const strs = docids[0].split(","); + const ptx = Number(strs[0].substring("__DashDocId(".length)); + const pty = Number(strs[1].substring(0, strs[1].length - 1)); + let count = 1; + const list: Doc[] = []; - return PreviewCursor._addDocument(Docs.Create.ImageDocument( - arr[1], { - _width: 300, title: arr[1], - x: newPoint[0], - y: newPoint[1], - })); - } + let first: Doc | undefined; + docids.map((did, i) => i && DocServer.GetRefField(did).then(doc => { + count++; + if (doc instanceof Doc) { + i === 1 && (first = doc); + const alias = Doc.MakeClone(doc); + const deltaX = NumCast(doc.x) - NumCast(first!.x) - ptx; + const deltaY = NumCast(doc.y) - NumCast(first!.y) - pty; + alias.x = newPoint[0] + deltaX; + alias.y = newPoint[1] + deltaY; + list.push(alias); + } + if (count === docids.length) { + undoBatch(() => PreviewCursor._addDocument(list))(); + } + })); + e.stopPropagation(); + } else { + // creates text document + undoBatch(() => PreviewCursor._addLiveTextDoc(Docs.Create.TextDocument("", { + _width: 500, + limitHeight: 400, + _autoHeight: true, + x: newPoint[0], + y: newPoint[1], + title: "-pasted text-" + })))(); + } + } else + //pasting in images + if (e.clipboardData.getData("text/html") !== "" && e.clipboardData.getData("text/html").includes("<img src=")) { + const re: any = /<img src="(.*?)"/g; + const arr: any[] = re.exec(e.clipboardData.getData("text/html")); + + undoBatch(() => PreviewCursor._addDocument(Docs.Create.ImageDocument( + arr[1], { + _width: 300, title: arr[1], + x: newPoint[0], + y: newPoint[1], + })))(); + } } } @@ -81,7 +111,7 @@ export class PreviewCursor extends React.Component<{}> { if (e.key !== "Escape" && e.key !== "Backspace" && e.key !== "Delete" && e.key !== "CapsLock" && e.key !== "Alt" && e.key !== "Shift" && e.key !== "Meta" && e.key !== "Control" && e.key !== "Insert" && e.key !== "Home" && e.key !== "End" && e.key !== "PageUp" && e.key !== "PageDown" && - e.key !== "NumLock" && + e.key !== "NumLock" && e.key !== " " && (e.keyCode < 112 || e.keyCode > 123) && // F1 thru F12 keys !e.key.startsWith("Arrow") && !e.defaultPrevented) { @@ -112,7 +142,7 @@ export class PreviewCursor extends React.Component<{}> { onKeyPress: (e: KeyboardEvent) => void, addLiveText: (doc: Doc) => void, getTransform: () => Transform, - addDocument: (doc: Doc) => boolean, + addDocument: (doc: Doc | Doc[]) => boolean, nudge: (nudgeX: number, nudgeY: number) => boolean) { this._clickPoint = [x, y]; this._onKeyPress = onKeyPress; diff --git a/src/client/views/RecommendationsBox.tsx b/src/client/views/RecommendationsBox.tsx index e66fd3eb4..8ca81c070 100644 --- a/src/client/views/RecommendationsBox.tsx +++ b/src/client/views/RecommendationsBox.tsx @@ -3,17 +3,17 @@ import React = require("react"); import { observable, action, computed, runInAction } from "mobx"; import Measure from "react-measure"; import "./RecommendationsBox.scss"; -import { Doc, DocListCast, WidthSym, HeightSym } from "../../new_fields/Doc"; +import { Doc, DocListCast, WidthSym, HeightSym } from "../../fields/Doc"; import { DocumentIcon } from "./nodes/DocumentIcon"; -import { StrCast, NumCast } from "../../new_fields/Types"; +import { StrCast, NumCast } from "../../fields/Types"; import { returnFalse, emptyFunction, returnEmptyString, returnOne, emptyPath, returnZero } from "../../Utils"; import { Transform } from "../util/Transform"; -import { ObjectField } from "../../new_fields/ObjectField"; +import { ObjectField } from "../../fields/ObjectField"; import { DocumentView } from "./nodes/DocumentView"; import { DocumentType } from '../documents/DocumentTypes'; import { ClientRecommender } from "../ClientRecommender"; import { DocServer } from "../DocServer"; -import { Id } from "../../new_fields/FieldSymbols"; +import { Id } from "../../fields/FieldSymbols"; import { FieldView, FieldViewProps } from "./nodes/FieldView"; import { DocumentManager } from "../util/DocumentManager"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; diff --git a/src/client/views/ScriptBox.tsx b/src/client/views/ScriptBox.tsx index 66d3b937e..888f84dfa 100644 --- a/src/client/views/ScriptBox.tsx +++ b/src/client/views/ScriptBox.tsx @@ -5,11 +5,11 @@ import { observable, action } from "mobx"; import "./ScriptBox.scss"; import { OverlayView } from "./OverlayView"; import { DocumentIconContainer } from "./nodes/DocumentIcon"; -import { Opt, Doc } from "../../new_fields/Doc"; +import { Opt, Doc } from "../../fields/Doc"; import { emptyFunction } from "../../Utils"; -import { ScriptCast } from "../../new_fields/Types"; +import { ScriptCast } from "../../fields/Types"; import { CompileScript } from "../util/Scripting"; -import { ScriptField } from "../../new_fields/ScriptField"; +import { ScriptField } from "../../fields/ScriptField"; import { DragManager } from "../util/DragManager"; import { EditableView } from "./EditableView"; import { getEffectiveTypeRoots } from "typescript"; diff --git a/src/client/views/SearchDocBox.tsx b/src/client/views/SearchDocBox.tsx index 7bd689b19..e038d8213 100644 --- a/src/client/views/SearchDocBox.tsx +++ b/src/client/views/SearchDocBox.tsx @@ -3,9 +3,9 @@ import { faBullseye, faLink } from "@fortawesome/free-solid-svg-icons"; import { action, computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; //import "./SearchBoxDoc.scss"; -import { Doc, DocListCast } from "../../new_fields/Doc"; -import { Id } from "../../new_fields/FieldSymbols"; -import { BoolCast, Cast, NumCast, StrCast } from "../../new_fields/Types"; +import { Doc, DocListCast } from "../../fields/Doc"; +import { Id } from "../../fields/FieldSymbols"; +import { BoolCast, Cast, NumCast, StrCast } from "../../fields/Types"; import { returnFalse, returnZero } from "../../Utils"; import { Docs } from "../documents/Documents"; import { SearchUtil } from "../util/SearchUtil"; diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx index 665ab4e41..f5e95e4fd 100644 --- a/src/client/views/TemplateMenu.tsx +++ b/src/client/views/TemplateMenu.tsx @@ -6,15 +6,15 @@ import './TemplateMenu.scss'; import { DocumentView } from "./nodes/DocumentView"; import { Template } from "./Templates"; import React = require("react"); -import { Doc, DocListCast } from "../../new_fields/Doc"; +import { Doc, DocListCast } from "../../fields/Doc"; import { Docs, } from "../documents/Documents"; -import { StrCast, Cast } from "../../new_fields/Types"; +import { StrCast, Cast } from "../../fields/Types"; import { CollectionTreeView } from "./collections/CollectionTreeView"; import { returnTrue, emptyFunction, returnFalse, returnOne, emptyPath, returnZero } from "../../Utils"; import { Transform } from "../util/Transform"; -import { ScriptField, ComputedField } from "../../new_fields/ScriptField"; +import { ScriptField, ComputedField } from "../../fields/ScriptField"; import { Scripting } from "../util/Scripting"; -import { List } from "../../new_fields/List"; +import { List } from "../../fields/List"; @observer class TemplateToggle extends React.Component<{ template: Template, checked: boolean, toggle: (event: React.ChangeEvent<HTMLInputElement>, template: Template) => void }> { @@ -113,8 +113,8 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { render() { const firstDoc = this.props.docViews[0].props.Document; const templateName = StrCast(firstDoc.layoutKey, "layout").replace("layout_", ""); - const noteTypes = DocListCast(Cast(Doc.UserDoc()["template-notes"], Doc, null)); - const addedTypes = DocListCast(Cast(Doc.UserDoc().templateButtons, Doc, null)?.data); + const noteTypes = DocListCast(Cast(Doc.UserDoc()["template-notes"], Doc, null)?.data); + const addedTypes = DocListCast(Cast(Doc.UserDoc()["template-buttons"], Doc, null)?.data); const layout = Doc.Layout(firstDoc); const templateMenu: Array<JSX.Element> = []; this.props.templates.forEach((checked, template) => @@ -158,9 +158,9 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { annotationsKey={""} dontRegisterView={true} fieldKey={"data"} - moveDocument={(doc: Doc) => false} - removeDocument={(doc: Doc) => false} - addDocument={(doc: Doc) => false} /> + moveDocument={returnFalse} + removeDocument={returnFalse} + addDocument={returnFalse} /> </ul>; } } diff --git a/src/client/views/animationtimeline/Keyframe.tsx b/src/client/views/animationtimeline/Keyframe.tsx index bbd7b2676..92b0f05b3 100644 --- a/src/client/views/animationtimeline/Keyframe.tsx +++ b/src/client/views/animationtimeline/Keyframe.tsx @@ -4,10 +4,10 @@ import "./Timeline.scss"; import "../globalCssVariables.scss"; import { observer } from "mobx-react"; import { observable, reaction, action, IReactionDisposer, observe, computed, runInAction, trace } from "mobx"; -import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../new_fields/Doc"; -import { Cast, NumCast } from "../../../new_fields/Types"; -import { List } from "../../../new_fields/List"; -import { createSchema, defaultSpec, makeInterface, listSpec } from "../../../new_fields/Schema"; +import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../fields/Doc"; +import { Cast, NumCast } from "../../../fields/Types"; +import { List } from "../../../fields/List"; +import { createSchema, defaultSpec, makeInterface, listSpec } from "../../../fields/Schema"; import { Transform } from "../../util/Transform"; import { TimelineMenu } from "./TimelineMenu"; import { Docs } from "../../documents/Documents"; diff --git a/src/client/views/animationtimeline/Timeline.tsx b/src/client/views/animationtimeline/Timeline.tsx index 466cbb867..30692944d 100644 --- a/src/client/views/animationtimeline/Timeline.tsx +++ b/src/client/views/animationtimeline/Timeline.tsx @@ -1,12 +1,12 @@ import * as React from "react"; import "./Timeline.scss"; -import { listSpec } from "../../../new_fields/Schema"; +import { listSpec } from "../../../fields/Schema"; import { observer } from "mobx-react"; import { Track } from "./Track"; import { observable, action, computed, runInAction, IReactionDisposer, reaction, trace } from "mobx"; -import { Cast, NumCast, StrCast, BoolCast } from "../../../new_fields/Types"; -import { List } from "../../../new_fields/List"; -import { Doc, DocListCast } from "../../../new_fields/Doc"; +import { Cast, NumCast, StrCast, BoolCast } from "../../../fields/Types"; +import { List } from "../../../fields/List"; +import { Doc, DocListCast } from "../../../fields/Doc"; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faPlayCircle, faBackward, faForward, faGripLines, faPauseCircle, faEyeSlash, faEye, faCheckCircle, faTimesCircle } from "@fortawesome/free-solid-svg-icons"; import { ContextMenu } from "../ContextMenu"; diff --git a/src/client/views/animationtimeline/Track.tsx b/src/client/views/animationtimeline/Track.tsx index 461db4858..fc96c320a 100644 --- a/src/client/views/animationtimeline/Track.tsx +++ b/src/client/views/animationtimeline/Track.tsx @@ -1,12 +1,12 @@ import { action, computed, intercept, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import * as React from "react"; -import { Doc, DocListCast, Opt, DocListCastAsync } from "../../../new_fields/Doc"; -import { Copy } from "../../../new_fields/FieldSymbols"; -import { List } from "../../../new_fields/List"; -import { ObjectField } from "../../../new_fields/ObjectField"; -import { listSpec } from "../../../new_fields/Schema"; -import { Cast, NumCast } from "../../../new_fields/Types"; +import { Doc, DocListCast, Opt, DocListCastAsync } from "../../../fields/Doc"; +import { Copy } from "../../../fields/FieldSymbols"; +import { List } from "../../../fields/List"; +import { ObjectField } from "../../../fields/ObjectField"; +import { listSpec } from "../../../fields/Schema"; +import { Cast, NumCast } from "../../../fields/Types"; import { Transform } from "../../util/Transform"; import { Keyframe, KeyframeFunc, RegionData } from "./Keyframe"; import "./Track.scss"; diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx index a04136e51..39bb9bc23 100644 --- a/src/client/views/collections/CollectionCarouselView.tsx +++ b/src/client/views/collections/CollectionCarouselView.tsx @@ -2,18 +2,18 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { observable, computed } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { documentSchema, collectionSchema } from '../../../new_fields/documentSchemas'; -import { makeInterface } from '../../../new_fields/Schema'; -import { NumCast, StrCast, ScriptCast, Cast } from '../../../new_fields/Types'; +import { documentSchema, collectionSchema } from '../../../fields/documentSchemas'; +import { makeInterface } from '../../../fields/Schema'; +import { NumCast, StrCast, ScriptCast, Cast } from '../../../fields/Types'; import { DragManager } from '../../util/DragManager'; import { ContentFittingDocumentView } from '../nodes/ContentFittingDocumentView'; import "./CollectionCarouselView.scss"; import { CollectionSubView } from './CollectionSubView'; import { faCaretLeft, faCaretRight } from '@fortawesome/free-solid-svg-icons'; -import { Doc } from '../../../new_fields/Doc'; +import { Doc } from '../../../fields/Doc'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; import { ContextMenu } from '../ContextMenu'; -import { ObjectField } from '../../../new_fields/ObjectField'; +import { ObjectField } from '../../../fields/ObjectField'; import { returnFalse } from '../../../Utils'; type CarouselDocument = makeInterface<[typeof documentSchema, typeof collectionSchema]>; diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 33ece13cc..745476ef7 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -7,13 +7,13 @@ import { observer } from "mobx-react"; import * as ReactDOM from 'react-dom'; import Measure from "react-measure"; import * as GoldenLayout from "../../../client/goldenLayout"; -import { DateField } from '../../../new_fields/DateField'; -import { Doc, DocListCast, Field, Opt, DataSym } from "../../../new_fields/Doc"; -import { Id } from '../../../new_fields/FieldSymbols'; -import { List } from '../../../new_fields/List'; -import { FieldId } from "../../../new_fields/RefField"; -import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; -import { TraceMobx } from '../../../new_fields/util'; +import { DateField } from '../../../fields/DateField'; +import { Doc, DocListCast, Field, Opt, DataSym } from "../../../fields/Doc"; +import { Id } from '../../../fields/FieldSymbols'; +import { List } from '../../../fields/List'; +import { FieldId } from "../../../fields/RefField"; +import { Cast, NumCast, StrCast } from "../../../fields/Types"; +import { TraceMobx } from '../../../fields/util'; import { emptyFunction, returnOne, returnTrue, Utils, returnZero } from "../../../Utils"; import { DocServer } from "../../DocServer"; import { Docs } from '../../documents/Documents'; @@ -30,6 +30,7 @@ import { SubCollectionViewProps } from "./CollectionSubView"; import { DockingViewButtonSelector } from './ParentDocumentSelector'; import React = require("react"); import { CollectionViewType } from './CollectionView'; +import { SnappingManager } from '../../util/SnappingManager'; library.add(faFile); const _global = (window /* browser */ || global /* node */) as any; @@ -68,10 +69,8 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp //Why is this here? (window as any).React = React; (window as any).ReactDOM = ReactDOM; - DragManager.Vals.Instance.StartWindowDrag = this.StartOtherDrag; + DragManager.StartWindowDrag = this.StartOtherDrag; } - hack: boolean = false; - undohack: any = null; public StartOtherDrag = (e: any, dragDocs: Doc[]) => { let config: any; if (dragDocs.length === 1) { @@ -191,6 +190,30 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp return retVal; } + @undoBatch + @action + public static ReplaceTab(document: Doc, stack: any): Opt<Doc> { + if (!CollectionDockingView.Instance) return undefined; + const instance = CollectionDockingView.Instance; + const replaceTab = (doc: Doc, child: any): Opt<Doc> => { + for (const contentItem of child.contentItems) { + const { config, isStack, isRow, isColumn } = contentItem; + if (isRow || isColumn || isStack) { + const val = replaceTab(doc, contentItem); + if (val) return val; + } else if (config.component === "DocumentFrameRenderer" && + config.props.documentId === doc[Id]) { + const alias = Doc.MakeAlias(doc); + config.props.documentId = alias[Id]; + config.title = alias.title; + instance.stateChanged(); + return alias; + } + } + return undefined; + }; + return replaceTab(document, instance._goldenLayout.root); + } // // Creates a vertical split on the right side of the docking view, and then adds the Document to the right of that split @@ -454,12 +477,6 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp const json = JSON.stringify(this._goldenLayout.toConfig()); this.props.Document.dockingConfig = json; this.updateDataField(json); - - if (this.undohack && !this.hack) { - this.undohack.end(); - this.undohack = undefined; - } - this.hack = false; } itemDropped = () => { @@ -500,7 +517,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp const stack = tab.contentItem.parent; // shifts the focus to this tab when another tab is dragged over it tab.element[0].onmouseenter = (e: any) => { - if (!this._isPointerDown || !DragManager.Vals.Instance.GetIsDragging()) return; + if (!this._isPointerDown || !SnappingManager.GetIsDragging()) return; const activeContentItem = tab.header.parent.getActiveContentItem(); if (tab.contentItem !== activeContentItem) { tab.header.parent.setActiveContentItem(tab.contentItem); @@ -516,15 +533,17 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp DragManager.StartDocumentDrag([gearSpan], dragData, e.clientX, e.clientY); } }; - let rendered = false; + tab.buttonDisposer = reaction(() => ((view: Opt<DocumentView>) => view ? [view] : [])(DocumentManager.Instance.getDocumentView(doc)), (views) => { - !rendered && ReactDOM.render(<span title="Drag as document" className="collectionDockingView-dragAsDocument" onPointerDown={onDown} > - <DockingViewButtonSelector views={views} Stack={stack} /> - </span>, - gearSpan); - rendered = true; - }); + if (views.length) { + ReactDOM.render(<span title="Drag as document" className="collectionDockingView-dragAsDocument" onPointerDown={onDown} > + <DockingViewButtonSelector views={() => views} Stack={stack} /> + </span>, + gearSpan); + tab.buttonDisposer?.(); + } + }, { fireImmediately: true }); tab.reactComponents = [gearSpan]; tab.element.append(gearSpan); @@ -680,16 +699,20 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { **/ @undoBatch @action - public static PinDoc(doc: Doc) { - //add this new doc to props.Document - const curPres = Cast(Doc.UserDoc().activePresentation, Doc) as Doc; - if (curPres) { - const pinDoc = Doc.MakeAlias(doc); - pinDoc.presentationTargetDoc = doc; - pinDoc.presZoomButton = true; - Doc.AddDocToList(curPres, "data", pinDoc); - if (!DocumentManager.Instance.getDocumentView(curPres)) { - CollectionDockingView.AddRightSplit(curPres); + public static PinDoc(doc: Doc, unpin = false) { + if (unpin) DockedFrameRenderer.UnpinDoc(doc); + else { + //add this new doc to props.Document + const curPres = Cast(Doc.UserDoc().activePresentation, Doc) as Doc; + if (curPres) { + const pinDoc = Doc.MakeAlias(doc); + pinDoc.presentationTargetDoc = doc; + pinDoc.presZoomButton = true; + pinDoc.context = curPres; + Doc.AddDocToList(curPres, "data", pinDoc); + if (!DocumentManager.Instance.getDocumentView(curPres)) { + CollectionDockingView.AddRightSplit(curPres); + } } } } @@ -734,8 +757,10 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { } get layoutDoc() { return this._document && Doc.Layout(this._document); } - panelWidth = () => this.layoutDoc && this.layoutDoc.maxWidth ? Math.min(Math.max(NumCast(this.layoutDoc._width), NumCast(this.layoutDoc._nativeWidth)), this._panelWidth) : this._panelWidth; - panelHeight = () => this._panelHeight; + nativeAspect = () => this.nativeWidth() ? this.nativeWidth() / this.nativeHeight() : 0; + panelWidth = () => this.layoutDoc?.maxWidth ? Math.min(Math.max(NumCast(this.layoutDoc._width), NumCast(this.layoutDoc._nativeWidth)), this._panelWidth) : + (this.nativeAspect() && this.nativeAspect() < this._panelWidth / this._panelHeight ? this._panelHeight * this.nativeAspect() : this._panelWidth) + panelHeight = () => this.nativeAspect() && this.nativeAspect() > this._panelWidth / this._panelHeight ? this._panelWidth / this.nativeAspect() : this._panelHeight; nativeWidth = () => !this.layoutDoc!._fitWidth ? NumCast(this.layoutDoc!._nativeWidth) || this._panelWidth : 0; nativeHeight = () => !this.layoutDoc!._fitWidth ? NumCast(this.layoutDoc!._nativeHeight) || this._panelHeight : 0; @@ -773,7 +798,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { return Transform.Identity(); } get previewPanelCenteringOffset() { return this.nativeWidth() ? (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2 : 0; } - get widthpercent() { return this.nativeWidth() ? `${(this.nativeWidth() * this.contentScaling()) / this.panelWidth() * 100}%` : undefined; } + get widthpercent() { return this.nativeWidth() ? `${(this.nativeWidth() * this.contentScaling()) / this._panelWidth * 100}%` : undefined; } addDocTab = (doc: Doc, location: string, libraryPath?: Doc[]) => { SelectionManager.DeselectAll(); @@ -783,6 +808,13 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { return CollectionDockingView.AddRightSplit(doc, libraryPath); } else if (location === "close") { return CollectionDockingView.CloseRightSplit(doc); + } else if (location === "replace") { + const alias = CollectionDockingView.ReplaceTab(doc, this._stack); + if (alias) { + runInAction(() => this._document = alias); + return true; + } + return false; } else {// if (location === "inPlace") { return CollectionDockingView.Instance.AddTab(this._stack, doc, libraryPath); } @@ -804,8 +836,8 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { ContentScaling={this.contentScaling} PanelWidth={this.panelWidth} PanelHeight={this.panelHeight} - NativeHeight={returnZero} - NativeWidth={returnZero} + NativeHeight={this.nativeHeight} + NativeWidth={this.nativeWidth} ScreenToLocalTransform={this.ScreenToLocalTransform} renderDepth={0} parentActive={returnTrue} diff --git a/src/client/views/collections/CollectionLinearView.tsx b/src/client/views/collections/CollectionLinearView.tsx index 344dca23a..f1002044a 100644 --- a/src/client/views/collections/CollectionLinearView.tsx +++ b/src/client/views/collections/CollectionLinearView.tsx @@ -1,9 +1,9 @@ import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, HeightSym, WidthSym } from '../../../new_fields/Doc'; -import { makeInterface } from '../../../new_fields/Schema'; -import { BoolCast, NumCast, StrCast, Cast, ScriptCast } from '../../../new_fields/Types'; +import { Doc, HeightSym, WidthSym } from '../../../fields/Doc'; +import { makeInterface } from '../../../fields/Schema'; +import { BoolCast, NumCast, StrCast, Cast, ScriptCast } from '../../../fields/Types'; import { emptyFunction, returnEmptyString, returnOne, returnTrue, Utils, returnFalse, returnZero } from '../../../Utils'; import { DragManager } from '../../util/DragManager'; import { Transform } from '../../util/Transform'; @@ -11,8 +11,8 @@ import "./CollectionLinearView.scss"; import { CollectionViewType } from './CollectionView'; import { CollectionSubView } from './CollectionSubView'; import { DocumentView } from '../nodes/DocumentView'; -import { documentSchema } from '../../../new_fields/documentSchemas'; -import { Id } from '../../../new_fields/FieldSymbols'; +import { documentSchema } from '../../../fields/documentSchemas'; +import { Id } from '../../../fields/FieldSymbols'; type LinearDocument = makeInterface<[typeof documentSchema,]>; diff --git a/src/client/views/collections/CollectionMapView.tsx b/src/client/views/collections/CollectionMapView.tsx index 971224482..d91337ce9 100644 --- a/src/client/views/collections/CollectionMapView.tsx +++ b/src/client/views/collections/CollectionMapView.tsx @@ -1,10 +1,10 @@ import { GoogleApiWrapper, Map as GeoMap, IMapProps, Marker } from "google-maps-react"; import { observer } from "mobx-react"; -import { Doc, Opt, DocListCast, FieldResult, Field } from "../../../new_fields/Doc"; -import { documentSchema } from "../../../new_fields/documentSchemas"; -import { Id } from "../../../new_fields/FieldSymbols"; -import { makeInterface } from "../../../new_fields/Schema"; -import { Cast, NumCast, ScriptCast, StrCast } from "../../../new_fields/Types"; +import { Doc, Opt, DocListCast, FieldResult, Field } from "../../../fields/Doc"; +import { documentSchema } from "../../../fields/documentSchemas"; +import { Id } from "../../../fields/FieldSymbols"; +import { makeInterface } from "../../../fields/Schema"; +import { Cast, NumCast, ScriptCast, StrCast } from "../../../fields/Types"; import "./CollectionMapView.scss"; import { CollectionSubView } from "./CollectionSubView"; import React = require("react"); @@ -47,7 +47,7 @@ class CollectionMapView extends CollectionSubView<MapSchema, Partial<IMapProps> private _cancelAddrReq = new Map<string, boolean>(); private _cancelLocReq = new Map<string, boolean>(); private _initialLookupPending = new Map<string, boolean>(); - private responders: { location: Lambda, address: Lambda }[] = []; + private responders: { locationDisposer: Lambda, addressDisposer: Lambda }[] = []; /** * Note that all the uses of runInAction below are not included @@ -176,13 +176,16 @@ class CollectionMapView extends CollectionSubView<MapSchema, Partial<IMapProps> } @computed get reactiveContents() { - this.responders.forEach(({ location, address }) => { location(); address(); }); + this.responders.forEach(({ locationDisposer, addressDisposer }) => { + locationDisposer(); + addressDisposer(); + }); this.responders = []; return this.childLayoutPairs.map(({ layout }) => { const fieldKey = Doc.LayoutFieldKey(layout); const id = layout[Id]; this.responders.push({ - location: computed(() => ({ lat: layout[`${fieldKey}-lat`], lng: layout[`${fieldKey}-lng`] })) + locationDisposer: computed(() => ({ lat: layout[`${fieldKey}-lat`], lng: layout[`${fieldKey}-lng`] })) .observe(({ oldValue, newValue }) => { if (this._cancelLocReq.get(id)) { this._cancelLocReq.set(id, false); @@ -190,7 +193,7 @@ class CollectionMapView extends CollectionSubView<MapSchema, Partial<IMapProps> this.respondToLocationChange(layout, fieldKey, newValue, oldValue); } }), - address: computed(() => Cast(layout[`${fieldKey}-address`], "string", null)) + addressDisposer: computed(() => Cast(layout[`${fieldKey}-address`], "string", null)) .observe(({ oldValue, newValue }) => { if (this._cancelAddrReq.get(id)) { this._cancelAddrReq.set(id, false); @@ -206,7 +209,8 @@ class CollectionMapView extends CollectionSubView<MapSchema, Partial<IMapProps> render() { const { childLayoutPairs } = this; const { Document, fieldKey, active, google } = this.props; - let center = this.getLocation(Document, `${fieldKey}-mapCenter`, false); + const mapLoc = this.getLocation(this.rootDoc, `${fieldKey}-mapCenter`, false); + let center = mapLoc; if (center === undefined) { const childLocations = childLayoutPairs.map(({ layout }) => this.getLocation(layout, Doc.LayoutFieldKey(layout), false)); center = childLocations.find(location => location) || defaultLocation; @@ -246,6 +250,7 @@ class CollectionMapView extends CollectionSubView<MapSchema, Partial<IMapProps> }} > {this.reactiveContents} + {mapLoc ? this.renderMarker(this.rootDoc) : undefined} </GeoMap> </div> </div>; diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx index c74cfbcf4..d6cb174cc 100644 --- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx +++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx @@ -4,20 +4,20 @@ import { faPalette } 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 { Doc } from "../../../new_fields/Doc"; -import { PastelSchemaPalette, SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; -import { ScriptField } from "../../../new_fields/ScriptField"; -import { StrCast, NumCast } from "../../../new_fields/Types"; +import { Doc } from "../../../fields/Doc"; +import { PastelSchemaPalette, SchemaHeaderField } from "../../../fields/SchemaHeaderField"; +import { ScriptField } from "../../../fields/ScriptField"; +import { StrCast, NumCast } from "../../../fields/Types"; import { numberRange, setupMoveUpEvents, emptyFunction } from "../../../Utils"; import { Docs } from "../../documents/Documents"; import { DragManager } from "../../util/DragManager"; import { CompileScript } from "../../util/Scripting"; -import { SelectionManager } from "../../util/SelectionManager"; import { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; import { EditableView } from "../EditableView"; import { CollectionStackingView } from "./CollectionStackingView"; import "./CollectionStackingView.scss"; +import { SnappingManager } from "../../util/SnappingManager"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -132,7 +132,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr this._color = color; } - pointerEnteredRow = action(() => DragManager.Vals.Instance.GetIsDragging() && (this._background = "#b4b4b4")); + pointerEnteredRow = action(() => SnappingManager.GetIsDragging() && (this._background = "#b4b4b4")); @action pointerLeaveRow = () => { diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx index 0a10c24b3..e3bcf2a21 100644 --- a/src/client/views/collections/CollectionPileView.tsx +++ b/src/client/views/collections/CollectionPileView.tsx @@ -1,8 +1,8 @@ import { action, computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { HeightSym, Opt, WidthSym } from "../../../new_fields/Doc"; -import { ScriptField } from "../../../new_fields/ScriptField"; -import { BoolCast, NumCast, StrCast } from "../../../new_fields/Types"; +import { HeightSym, Opt, WidthSym } from "../../../fields/Doc"; +import { ScriptField } from "../../../fields/ScriptField"; +import { BoolCast, NumCast, StrCast } from "../../../fields/Types"; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView"; @@ -12,7 +12,7 @@ import React = require("react"); import { setupMoveUpEvents, emptyFunction, returnFalse } from "../../../Utils"; import { SelectionManager } from "../../util/SelectionManager"; import { UndoManager } from "../../util/UndoManager"; -import { DragManager } from "../../util/DragManager"; +import { SnappingManager } from "../../util/SnappingManager"; @observer export class CollectionPileView extends CollectionSubView(doc => doc) { @@ -79,7 +79,7 @@ export class CollectionPileView extends CollectionSubView(doc => doc) { _undoBatch: UndoManager.Batch | undefined; pointerDown = (e: React.PointerEvent) => { let dist = 0; - DragManager.Vals.Instance.SetIsDragging(true); + SnappingManager.SetIsDragging(true); // this._lastTap should be set to 0, and this._doubleTap should be set to false in the class header setupMoveUpEvents(this, e, (e: PointerEvent, down: number[], delta: number[]) => { if (this.layoutEngine() === "pass" && this.childDocs.length && this.props.isSelected(true)) { @@ -99,7 +99,7 @@ export class CollectionPileView extends CollectionSubView(doc => doc) { }, () => { this._undoBatch?.end(); this._undoBatch = undefined; - DragManager.Vals.Instance.SetIsDragging(false); + SnappingManager.SetIsDragging(false); if (!this.childDocs.length) { this.props.ContainingCollectionView?.removeDocument(this.props.Document); } diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx index 0e6489947..62aed67ed 100644 --- a/src/client/views/collections/CollectionSchemaCells.tsx +++ b/src/client/views/collections/CollectionSchemaCells.tsx @@ -4,8 +4,8 @@ import { observer } from "mobx-react"; import { CellInfo } from "react-table"; import "react-table/react-table.css"; import { emptyFunction, returnFalse, returnZero, returnOne } from "../../../Utils"; -import { Doc, DocListCast, Field, Opt } from "../../../new_fields/Doc"; -import { Id } from "../../../new_fields/FieldSymbols"; +import { Doc, DocListCast, Field, Opt } from "../../../fields/Doc"; +import { Id } from "../../../fields/FieldSymbols"; import { KeyCodes } from "../../util/KeyCodes"; import { SetupDrag, DragManager } from "../../util/DragManager"; import { CompileScript } from "../../util/Scripting"; @@ -16,13 +16,13 @@ import { EditableView } from "../EditableView"; import { FieldView, FieldViewProps } from "../nodes/FieldView"; import "./CollectionSchemaView.scss"; import { CollectionView } from "./CollectionView"; -import { NumCast, StrCast, BoolCast, FieldValue, Cast } from "../../../new_fields/Types"; +import { NumCast, StrCast, BoolCast, FieldValue, Cast } from "../../../fields/Types"; import { Docs } from "../../documents/Documents"; -import { SelectionManager } from "../../util/SelectionManager"; import { library } from '@fortawesome/fontawesome-svg-core'; import { faExpand } from '@fortawesome/free-solid-svg-icons'; -import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; +import { SchemaHeaderField } from "../../../fields/SchemaHeaderField"; import { undoBatch } from "../../util/UndoManager"; +import { SnappingManager } from "../../util/SnappingManager"; library.add(faExpand); @@ -37,8 +37,8 @@ export interface CellProps { renderDepth: number; addDocTab: (document: Doc, where: string) => boolean; pinToPres: (document: Doc) => void; - moveDocument: (document: Doc, targetCollection: Doc | undefined, - addDocument: (document: Doc) => boolean) => boolean; + moveDocument: (document: Doc | Doc[], targetCollection: Doc | undefined, + addDocument: (document: Doc | Doc[]) => boolean) => boolean; isFocused: boolean; changeFocusedCellByIndex: (row: number, col: number) => void; setIsEditing: (isEditing: boolean) => void; @@ -185,11 +185,11 @@ export class CollectionSchemaCell extends React.Component<CellProps> { const onItemDown = (e: React.PointerEvent) => { fieldIsDoc && SetupDrag(this._focusRef, () => this._document[props.fieldKey] instanceof Doc ? this._document[props.fieldKey] : this._document, - this._document[props.fieldKey] instanceof Doc ? (doc: Doc, target: Doc | undefined, addDoc: (newDoc: Doc) => any) => addDoc(doc) : this.props.moveDocument, + this._document[props.fieldKey] instanceof Doc ? (doc: Doc | Doc[], target: Doc | undefined, addDoc: (newDoc: Doc | Doc[]) => any) => addDoc(doc) : this.props.moveDocument, this._document[props.fieldKey] instanceof Doc ? "alias" : this.props.Document.schemaDoc ? "copy" : undefined)(e); }; const onPointerEnter = (e: React.PointerEvent): void => { - if (e.buttons === 1 && DragManager.Vals.Instance.GetIsDragging() && (type === "document" || type === undefined)) { + if (e.buttons === 1 && SnappingManager.GetIsDragging() && (type === "document" || type === undefined)) { dragRef.current!.className = "collectionSchemaView-cellContainer doc-drag-over"; } }; diff --git a/src/client/views/collections/CollectionSchemaHeaders.tsx b/src/client/views/collections/CollectionSchemaHeaders.tsx index 507ee89e4..dae0600b1 100644 --- a/src/client/views/collections/CollectionSchemaHeaders.tsx +++ b/src/client/views/collections/CollectionSchemaHeaders.tsx @@ -7,7 +7,7 @@ import { library, IconProp } from "@fortawesome/fontawesome-svg-core"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { ColumnType } from "./CollectionSchemaView"; import { faFile } from "@fortawesome/free-regular-svg-icons"; -import { SchemaHeaderField, PastelSchemaPalette } from "../../../new_fields/SchemaHeaderField"; +import { SchemaHeaderField, PastelSchemaPalette } from "../../../fields/SchemaHeaderField"; import { undoBatch } from "../../util/UndoManager"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; diff --git a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx index f9cd9a628..6f1e8ac1f 100644 --- a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx +++ b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx @@ -2,18 +2,18 @@ import React = require("react"); import { ReactTableDefaults, TableCellRenderer, RowInfo } from "react-table"; import "./CollectionSchemaView.scss"; import { Transform } from "../../util/Transform"; -import { Doc } from "../../../new_fields/Doc"; +import { Doc } from "../../../fields/Doc"; import { DragManager, SetupDrag, dropActionType } from "../../util/DragManager"; -import { SelectionManager } from "../../util/SelectionManager"; -import { Cast, FieldValue, StrCast } from "../../../new_fields/Types"; +import { Cast, FieldValue, StrCast } from "../../../fields/Types"; import { ContextMenu } from "../ContextMenu"; import { action } from "mobx"; import { library } from '@fortawesome/fontawesome-svg-core'; import { faGripVertical, faTrash } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { DocumentManager } from "../../util/DocumentManager"; -import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; +import { SchemaHeaderField } from "../../../fields/SchemaHeaderField"; import { undoBatch } from "../../util/UndoManager"; +import { SnappingManager } from "../../util/SnappingManager"; library.add(faGripVertical, faTrash); @@ -32,7 +32,7 @@ export class MovableColumn extends React.Component<MovableColumnProps> { private _dragRef: React.RefObject<HTMLDivElement> = React.createRef(); onPointerEnter = (e: React.PointerEvent): void => { - if (e.buttons === 1 && DragManager.Vals.Instance.GetIsDragging()) { + if (e.buttons === 1 && SnappingManager.GetIsDragging()) { this._header!.current!.className = "collectionSchema-col-wrapper"; document.addEventListener("pointermove", this.onDragMove, true); } @@ -130,8 +130,8 @@ export class MovableColumn extends React.Component<MovableColumnProps> { export interface MovableRowProps { rowInfo: RowInfo; ScreenToLocalTransform: () => Transform; - addDoc: (doc: Doc, relativeTo?: Doc, before?: boolean) => boolean; - removeDoc: (doc: Doc) => boolean; + addDoc: (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => boolean; + removeDoc: (doc: Doc | Doc[]) => boolean; rowFocused: boolean; textWrapRow: (doc: Doc) => void; rowWrapped: boolean; @@ -143,7 +143,7 @@ export class MovableRow extends React.Component<MovableRowProps> { private _rowDropDisposer?: DragManager.DragDropDisposer; onPointerEnter = (e: React.PointerEvent): void => { - if (e.buttons === 1 && DragManager.Vals.Instance.GetIsDragging()) { + if (e.buttons === 1 && SnappingManager.GetIsDragging()) { this._header!.current!.className = "collectionSchema-row-wrapper"; document.addEventListener("pointermove", this.onDragMove, true); } @@ -183,7 +183,7 @@ export class MovableRow extends React.Component<MovableRowProps> { if (docDragData) { e.stopPropagation(); if (docDragData.draggedDocuments[0] === rowDoc) return true; - const addDocument = (doc: Doc) => this.props.addDoc(doc, rowDoc, before); + const addDocument = (doc: Doc | Doc[]) => this.props.addDoc(doc, rowDoc, before); const movedDocs = docDragData.draggedDocuments; return (docDragData.dropAction || docDragData.userDropAction) ? docDragData.droppedDocuments.reduce((added: boolean, d) => this.props.addDoc(d, rowDoc, before) || added, false) @@ -201,7 +201,7 @@ export class MovableRow extends React.Component<MovableRowProps> { @undoBatch @action - move: DragManager.MoveFunction = (doc: Doc, targetCollection: Doc | undefined, addDoc) => { + move: DragManager.MoveFunction = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDoc) => { const targetView = targetCollection && DocumentManager.Instance.getDocumentView(targetCollection); if (targetView && targetView.props.ContainingCollectionDoc) { return doc !== targetCollection && doc !== targetView.props.ContainingCollectionDoc && this.props.removeDoc(doc) && addDoc(doc); diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index c0024293f..35f892d65 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -6,13 +6,13 @@ import { action, computed, observable, untracked } from "mobx"; import { observer } from "mobx-react"; import ReactTable, { CellInfo, Column, ComponentPropsGetterR, Resize, SortingRule } from "react-table"; import "react-table/react-table.css"; -import { Doc, DocListCast, Field, Opt } from "../../../new_fields/Doc"; -import { Id } from "../../../new_fields/FieldSymbols"; -import { List } from "../../../new_fields/List"; -import { listSpec } from "../../../new_fields/Schema"; -import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; -import { ComputedField } from "../../../new_fields/ScriptField"; -import { Cast, FieldValue, NumCast, StrCast, BoolCast } from "../../../new_fields/Types"; +import { Doc, DocListCast, Field, Opt } from "../../../fields/Doc"; +import { Id } from "../../../fields/FieldSymbols"; +import { List } from "../../../fields/List"; +import { listSpec } from "../../../fields/Schema"; +import { SchemaHeaderField } from "../../../fields/SchemaHeaderField"; +import { ComputedField } from "../../../fields/ScriptField"; +import { Cast, FieldValue, NumCast, StrCast, BoolCast } from "../../../fields/Types"; import { Docs, DocumentOptions } from "../../documents/Documents"; import { CompileScript, Transformer, ts } from "../../util/Scripting"; import { Transform } from "../../util/Transform"; @@ -207,9 +207,9 @@ export interface SchemaTableProps { ContainingCollectionDoc: Opt<Doc>; fieldKey: string; renderDepth: number; - deleteDocument: (document: Doc) => boolean; - addDocument: (document: Doc) => boolean; - moveDocument: (document: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean) => boolean; + deleteDocument: (document: Doc | Doc[]) => boolean; + addDocument: (document: Doc | Doc[]) => boolean; + moveDocument: (document: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean; ScreenToLocalTransform: () => Transform; active: (outsideReaction: boolean) => boolean; onDrop: (e: React.DragEvent<Element>, options: DocumentOptions, completed?: (() => void) | undefined) => void; diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index f6cdebc9b..cc6077d98 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -4,17 +4,16 @@ import { CursorProperty } from "csstype"; import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import Switch from 'rc-switch'; -import { DataSym, Doc, HeightSym, WidthSym } from "../../../new_fields/Doc"; -import { collectionSchema, documentSchema } from "../../../new_fields/documentSchemas"; -import { Id } from "../../../new_fields/FieldSymbols"; -import { List } from "../../../new_fields/List"; -import { listSpec, makeInterface } from "../../../new_fields/Schema"; -import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; -import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from "../../../new_fields/Types"; -import { TraceMobx } from "../../../new_fields/util"; -import { emptyFunction, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from "../../../Utils"; +import { DataSym, Doc, HeightSym, WidthSym } from "../../../fields/Doc"; +import { collectionSchema, documentSchema } from "../../../fields/documentSchemas"; +import { Id } from "../../../fields/FieldSymbols"; +import { List } from "../../../fields/List"; +import { listSpec, makeInterface } from "../../../fields/Schema"; +import { SchemaHeaderField } from "../../../fields/SchemaHeaderField"; +import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from "../../../fields/Types"; +import { TraceMobx } from "../../../fields/util"; +import { emptyFunction, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils, smoothScroll } from "../../../Utils"; import { DragManager, dropActionType } from "../../util/DragManager"; -import { SelectionManager } from "../../util/SelectionManager"; import { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; import { ContextMenu } from "../ContextMenu"; @@ -26,7 +25,7 @@ import "./CollectionStackingView.scss"; import { CollectionStackingViewFieldColumn } from "./CollectionStackingViewFieldColumn"; import { CollectionSubView } from "./CollectionSubView"; import { CollectionViewType } from "./CollectionView"; -import { ScriptField } from "../../../new_fields/ScriptField"; +import { SnappingManager } from "../../util/SnappingManager"; const _global = (window /* browser */ || global /* node */) as any; type StackingDocument = makeInterface<[typeof collectionSchema, typeof documentSchema]>; @@ -58,6 +57,14 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) } @computed get NodeWidth() { return this.props.PanelWidth() - this.gridGap; } + constructor(props: any) { + super(props); + + if (this.sectionHeaders === undefined) { + this.props.Document.sectionHeaders = new List<SchemaHeaderField>(); + } + } + children(docs: Doc[], columns?: number) { TraceMobx(); this._docXfs.length = 0; @@ -66,7 +73,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) const width = () => this.getDocWidth(d); const dref = React.createRef<HTMLDivElement>(); const dxf = () => this.getDocTransform(d, dref.current!); - this._docXfs.push({ dxf: dxf, width: width, height: height }); + this._docXfs.push({ dxf, width, height }); const rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap); const style = this.isStackingView ? { width: width(), marginTop: i ? this.gridGap : 0, height: height() } : { gridRowEnd: `span ${rowSpan}` }; return <div className={`collectionStackingView-${this.isStackingView ? "columnDoc" : "masonryDoc"}`} key={d[Id]} ref={dref} style={style} > @@ -86,7 +93,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) setTimeout(() => this.props.Document.sectionHeaders = new List<SchemaHeaderField>(), 0); return new Map<SchemaHeaderField, Doc[]>(); } - const sectionHeaders: SchemaHeaderField[] = Array.from(this.sectionHeaders); + const sectionHeaders = Array.from(this.sectionHeaders); const fields = new Map<SchemaHeaderField, Doc[]>(sectionHeaders.map(sh => [sh, []] as [SchemaHeaderField, []])); let changed = false; this.filteredChildren.map(d => { @@ -166,6 +173,23 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) return this.props.addDocTab(doc, where); } + + focusDocument = (doc: Doc, willZoom: boolean, scale?: number, afterFocus?: () => boolean) => { + Doc.BrushDoc(doc); + this.props.focus(doc); + Doc.linkFollowHighlight(doc); + + const found = this._mainCont && Array.from(this._mainCont.getElementsByClassName("documentView-node")).find((node: any) => node.id === doc[Id]); + if (found) { + const top = found.getBoundingClientRect().top; + const localTop = this.props.ScreenToLocalTransform().transformPoint(0, top); + smoothScroll(500, this._mainCont!, localTop[1] + this._mainCont!.scrollTop); + } + afterFocus && setTimeout(() => { + if (afterFocus?.()) { } + }, 500); + } + getDisplayDoc(doc: Doc, dataDoc: Doc | undefined, dxf: () => Transform, width: () => number) { const layoutDoc = Doc.Layout(doc, this.props.ChildLayoutTemplate?.()); const height = () => this.getDocHeight(doc); @@ -183,12 +207,13 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) NativeHeight={returnZero} NativeWidth={returnZero} fitToBox={false} + dontRegisterView={this.props.dontRegisterView} rootSelected={this.rootSelected} dropAction={StrCast(this.props.Document.childDropAction) as dropActionType} onClick={this.onChildClickHandler} onDoubleClick={this.onChildDoubleClickHandler} ScreenToLocalTransform={dxf} - focus={this.props.focus} + focus={this.focusDocument} ContainingCollectionDoc={this.props.CollectionView?.props.Document} ContainingCollectionView={this.props.CollectionView} addDocument={this.props.addDocument} @@ -312,7 +337,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) this.refList.push(ref); const doc = this.props.DataDoc && this.props.DataDoc.layout === this.layoutDoc ? this.props.DataDoc : this.layoutDoc; this.observer = new _global.ResizeObserver(action((entries: any) => { - if (this.props.Document._autoHeight && ref && this.refList.length && !DragManager.Vals.Instance.GetIsDragging()) { + if (this.props.Document._autoHeight && ref && this.refList.length && !SnappingManager.GetIsDragging()) { Doc.Layout(doc)._height = Math.min(1200, Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace("px", ""))))); } })); @@ -359,7 +384,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) this.refList.push(ref); const doc = this.props.DataDoc && this.props.DataDoc.layout === this.layoutDoc ? this.props.DataDoc : this.layoutDoc; this.observer = new _global.ResizeObserver(action((entries: any) => { - if (this.props.Document._autoHeight && ref && this.refList.length && !DragManager.Vals.Instance.GetIsDragging()) { + if (this.props.Document._autoHeight && ref && this.refList.length && !SnappingManager.GetIsDragging()) { Doc.Layout(doc)._height = this.refList.reduce((p, r) => p + Number(getComputedStyle(r).height.replace("px", "")), 0); } })); @@ -407,6 +432,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) if (!e.isPropagationStopped()) { const subItems: ContextMenuProps[] = []; subItems.push({ description: `${this.props.Document.fillColumn ? "Variable Size" : "Autosize"} Column`, event: () => this.props.Document.fillColumn = !this.props.Document.fillColumn, icon: "plus" }); + subItems.push({ description: `${this.Document._autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight, icon: "plus" }); ContextMenu.Instance.addItem({ description: "Options...", subitems: subItems, icon: "eye" }); } } diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index 1d16a5478..53435ccc9 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -4,16 +4,15 @@ import { faPalette } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCast } from "../../../new_fields/Doc"; -import { RichTextField } from "../../../new_fields/RichTextField"; -import { PastelSchemaPalette, SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; -import { ScriptField } from "../../../new_fields/ScriptField"; -import { NumCast, StrCast, Cast } from "../../../new_fields/Types"; -import { ImageField } from "../../../new_fields/URLField"; -import { TraceMobx } from "../../../new_fields/util"; +import { Doc, DocListCast } from "../../../fields/Doc"; +import { RichTextField } from "../../../fields/RichTextField"; +import { PastelSchemaPalette, SchemaHeaderField } from "../../../fields/SchemaHeaderField"; +import { ScriptField } from "../../../fields/ScriptField"; +import { NumCast, StrCast, Cast } from "../../../fields/Types"; +import { ImageField } from "../../../fields/URLField"; +import { TraceMobx } from "../../../fields/util"; import { Docs, DocUtils } from "../../documents/Documents"; import { DragManager } from "../../util/DragManager"; -import { SelectionManager } from "../../util/SelectionManager"; import { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; import { ContextMenu } from "../ContextMenu"; @@ -22,7 +21,8 @@ import { EditableView } from "../EditableView"; import { CollectionStackingView } from "./CollectionStackingView"; import { setupMoveUpEvents, emptyFunction } from "../../../Utils"; import "./CollectionStackingView.scss"; -import { listSpec } from "../../../new_fields/Schema"; +import { listSpec } from "../../../fields/Schema"; +import { SnappingManager } from "../../util/SnappingManager"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -120,7 +120,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC @action pointerEntered = () => { - if (DragManager.Vals.Instance.GetIsDragging()) { + if (SnappingManager.GetIsDragging()) { this._background = "#b4b4b4"; } } @@ -355,7 +355,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC <div className="collectionStackingViewFieldColumn" key={heading} style={{ width: `${100 / ((uniqueHeadings.length + ((chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ? 1 : 0)) || 1)}%`, - height: undefined, // DragManager.Vals.Instance.GetIsDragging() ? "100%" : undefined, + height: undefined, // DraggingManager.GetIsDragging() ? "100%" : undefined, background: this._background }} ref={this.createColumnDropRef} onPointerEnter={this.pointerEntered} onPointerLeave={this.pointerLeave}> diff --git a/src/client/views/collections/CollectionStaffView.tsx b/src/client/views/collections/CollectionStaffView.tsx index 5b9a69bf7..c5c3f96e8 100644 --- a/src/client/views/collections/CollectionStaffView.tsx +++ b/src/client/views/collections/CollectionStaffView.tsx @@ -1,7 +1,7 @@ import { CollectionSubView } from "./CollectionSubView"; import React = require("react"); import { computed, action, IReactionDisposer, reaction, runInAction, observable } from "mobx"; -import { NumCast } from "../../../new_fields/Types"; +import { NumCast } from "../../../fields/Types"; import "./CollectionStaffView.scss"; import { observer } from "mobx-react"; diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index b4ca29b19..c9eb08b45 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -1,14 +1,14 @@ import { action, computed, IReactionDisposer, reaction } from "mobx"; import { basename } from 'path'; -import CursorField from "../../../new_fields/CursorField"; -import { Doc, Opt } from "../../../new_fields/Doc"; -import { Id } from "../../../new_fields/FieldSymbols"; -import { List } from "../../../new_fields/List"; -import { listSpec } from "../../../new_fields/Schema"; -import { ScriptField } from "../../../new_fields/ScriptField"; -import { Cast } from "../../../new_fields/Types"; +import CursorField from "../../../fields/CursorField"; +import { Doc, Opt } from "../../../fields/Doc"; +import { Id } from "../../../fields/FieldSymbols"; +import { List } from "../../../fields/List"; +import { listSpec } from "../../../fields/Schema"; +import { ScriptField } from "../../../fields/ScriptField"; +import { Cast, ScriptCast } from "../../../fields/Types"; import { GestureUtils } from "../../../pen-gestures/GestureUtils"; -import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; +import { CurrentUserUtils } from "../../util/CurrentUserUtils"; import { Upload } from "../../../server/SharedMediaTypes"; import { Utils } from "../../../Utils"; import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils"; @@ -16,7 +16,7 @@ import { DocServer } from "../../DocServer"; import { Docs, DocumentOptions } from "../../documents/Documents"; import { DocumentType } from "../../documents/DocumentTypes"; import { Networking } from "../../Network"; -import { DragManager } from "../../util/DragManager"; +import { DragManager, dropActionType } from "../../util/DragManager"; import { ImageUtils } from "../../util/Import & Export/ImageUtils"; import { InteractionUtils } from "../../util/InteractionUtils"; import { undoBatch, UndoManager } from "../../util/UndoManager"; @@ -27,9 +27,9 @@ import { CollectionView } from "./CollectionView"; import React = require("react"); export interface CollectionViewProps extends FieldViewProps { - addDocument: (document: Doc) => boolean; - removeDocument: (document: Doc) => boolean; - moveDocument: (document: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean) => boolean; + addDocument: (document: Doc | Doc[]) => boolean; + removeDocument: (document: Doc | Doc[]) => boolean; + moveDocument: (document: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean; PanelWidth: () => number; PanelHeight: () => number; VisibleHeight?: () => number; @@ -95,7 +95,7 @@ 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() { - return this.dataDoc[this.props.fieldKey + (this.props.annotationsKey ? "-" + this.props.annotationsKey : "")]; + return this.dataDoc[this.props.annotationsKey || this.props.fieldKey]; } get childLayoutPairs(): { layout: Doc; data: Doc; }[] { @@ -195,43 +195,60 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: protected onGesture(e: Event, ge: GestureUtils.GestureEvent) { } - protected onInternalPreDrop(e: Event, de: DragManager.DropEvent) { + protected onInternalPreDrop(e: Event, de: DragManager.DropEvent, targetAction: dropActionType) { if (de.complete.docDragData) { - if (de.complete.docDragData.draggedDocuments.some(d => this.childDocs.includes(d))) { - de.complete.docDragData.dropAction = "move"; + // if targetDropAction is, say 'alias', but we're just dragging within a collection, we want to ignore the targetAction. + // otherwise, the targetAction should become the actual action (which can still be overridden by the userDropAction -eg, shift/ctrl keys) + if (targetAction && !de.complete.docDragData.draggedDocuments.some(d => d.context === this.props.Document && this.childDocs.includes(d))) { + de.complete.docDragData.dropAction = targetAction; } e.stopPropagation(); } } + addDocument = (doc: Doc | Doc[]) => this.props.addDocument(doc); + @undoBatch @action protected onInternalDrop(e: Event, de: DragManager.DropEvent): boolean { const docDragData = de.complete.docDragData; - (this.props.Document.dropConverter instanceof ScriptField) && - this.props.Document.dropConverter.script.run({ dragData: docDragData }); /// bcz: check this + ScriptCast(this.props.Document.dropConverter)?.script.run({ dragData: docDragData }); if (docDragData) { let added = false; if (docDragData.dropAction || docDragData.userDropAction) { - added = docDragData.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d) || added, false); + added = this.addDocument(docDragData.droppedDocuments); } else if (docDragData.moveDocument) { - const movedDocs = docDragData.draggedDocuments; - added = movedDocs.reduce((added: boolean, d, i) => - docDragData.droppedDocuments[i] !== d ? this.props.addDocument(docDragData.droppedDocuments[i]) : - docDragData.moveDocument?.(d, this.props.Document, this.props.addDocument) || added, false); + const movedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] === d); + const addedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] !== d); + const res = addedDocs.length ? this.addDocument(addedDocs) : true; + added = movedDocs.length ? docDragData.moveDocument(movedDocs, this.props.Document, this.addDocument) : res; } else { - added = docDragData.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d) || added, false); + added = this.addDocument(docDragData.droppedDocuments); } e.stopPropagation(); return added; } else if (de.complete.annoDragData) { e.stopPropagation(); - return this.props.addDocument(de.complete.annoDragData.dropDocument); + return this.addDocument(de.complete.annoDragData.dropDocument); } return false; } + readUploadedFileAsText = (inputFile: File) => { + const temporaryFileReader = new FileReader(); + + return new Promise((resolve, reject) => { + temporaryFileReader.onerror = () => { + temporaryFileReader.abort(); + reject(new DOMException("Problem parsing input file.")); + }; + temporaryFileReader.onload = () => { + resolve(temporaryFileReader.result); + }; + temporaryFileReader.readAsText(inputFile); + }); + } @undoBatch @action protected async onExternalDrop(e: React.DragEvent, options: DocumentOptions, completed?: () => void) { @@ -250,7 +267,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: e.stopPropagation(); e.preventDefault(); - const { addDocument } = this.props; + const { addDocument } = this; if (!addDocument) { alert("this.props.addDocument does not exist. Aborting drop operation."); return; @@ -369,7 +386,21 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: } if (item.kind === "file") { const file = item.getAsFile(); - file && file.type && files.push(file); + file?.type && files.push(file); + + file?.type === "application/json" && this.readUploadedFileAsText(file).then(result => { + console.log(result); + const json = JSON.parse(result as string); + addDocument(Docs.Create.TreeDocument( + json["rectangular-puzzle"].crossword.clues[0].clue.map((c: any) => { + const label = Docs.Create.LabelDocument({ title: c["#text"], _width: 120, _height: 20 }); + const proto = Doc.GetProto(label); + proto._width = 120; + proto._height = 20; + return proto; + } + ), { _width: 150, _height: 600, title: "across", backgroundColor: "white", _singleLine: true })); + }); } } for (const { source: { name, type }, result } of await Networking.UploadFilesToServer(files)) { diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx index a2d4774c8..15bc0bfd5 100644 --- a/src/client/views/collections/CollectionTimeView.tsx +++ b/src/client/views/collections/CollectionTimeView.tsx @@ -1,11 +1,11 @@ import { action, computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { Doc, Opt, DocCastAsync } from "../../../new_fields/Doc"; -import { List } from "../../../new_fields/List"; -import { ObjectField } from "../../../new_fields/ObjectField"; -import { RichTextField } from "../../../new_fields/RichTextField"; -import { ComputedField, ScriptField } from "../../../new_fields/ScriptField"; -import { NumCast, StrCast, BoolCast, Cast } from "../../../new_fields/Types"; +import { Doc, Opt, DocCastAsync } from "../../../fields/Doc"; +import { List } from "../../../fields/List"; +import { ObjectField } from "../../../fields/ObjectField"; +import { RichTextField } from "../../../fields/RichTextField"; +import { ComputedField, ScriptField } from "../../../fields/ScriptField"; +import { NumCast, StrCast, BoolCast, Cast } from "../../../fields/Types"; import { emptyFunction, returnFalse, setupMoveUpEvents } from "../../../Utils"; import { Scripting } from "../../util/Scripting"; import { ContextMenu } from "../ContextMenu"; diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index a00bb6bfb..2aac81146 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -81,7 +81,6 @@ position: relative; text-overflow: ellipsis; white-space: pre-wrap; - overflow: hidden; min-width: 10px; // width:100%;//width: max-content; diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 288fa8794..3e99af724 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -1,18 +1,18 @@ -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faAngleRight, faArrowsAltH, faBell, faCamera, faCaretDown, faCaretRight, faCaretSquareDown, faCaretSquareRight, faExpand, faMinus, faPlus, faTrash, faTrashAlt } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable, runInAction, untracked } from "mobx"; +import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; -import { DataSym, Doc, DocListCast, Field, HeightSym, Opt, WidthSym } from '../../../new_fields/Doc'; -import { Id } from '../../../new_fields/FieldSymbols'; -import { List } from '../../../new_fields/List'; -import { Document, listSpec } from '../../../new_fields/Schema'; -import { ComputedField, ScriptField } from '../../../new_fields/ScriptField'; -import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../new_fields/Types'; +import { DataSym, Doc, DocListCast, Field, HeightSym, Opt, WidthSym } from '../../../fields/Doc'; +import { Id } from '../../../fields/FieldSymbols'; +import { List } from '../../../fields/List'; +import { PrefetchProxy } from '../../../fields/Proxy'; +import { Document, listSpec } from '../../../fields/Schema'; +import { ComputedField, ScriptField } from '../../../fields/ScriptField'; +import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { emptyFunction, emptyPath, returnFalse, returnOne, returnTrue, returnZero, simulateMouseClick, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from "../../documents/DocumentTypes"; import { DocumentManager } from '../../util/DocumentManager'; +import { SnappingManager } from '../../util/SnappingManager'; import { DragManager, dropActionType, SetupDrag } from "../../util/DragManager"; import { Scripting } from '../../util/Scripting'; import { SelectionManager } from '../../util/SelectionManager'; @@ -31,8 +31,8 @@ import { CollectionSubView } from "./CollectionSubView"; import "./CollectionTreeView.scss"; import { CollectionViewType } from './CollectionView'; import React = require("react"); -import { PrefetchProxy } from '../../../new_fields/Proxy'; - +import { makeTemplate } from '../../util/DropConverter'; +import { TraceMobx } from '../../../fields/util'; export interface TreeViewProps { document: Doc; @@ -41,7 +41,7 @@ export interface TreeViewProps { containingCollection: Doc; prevSibling?: Doc; renderDepth: number; - deleteDoc: (doc: Doc) => boolean; + deleteDoc: (doc: Doc | Doc[]) => boolean; moveDocument: DragManager.MoveFunction; dropAction: dropActionType; addDocTab: (doc: Doc, where: string, libraryPath?: Doc[]) => boolean; @@ -49,7 +49,7 @@ export interface TreeViewProps { panelWidth: () => number; panelHeight: () => number; ChromeHeight: undefined | (() => number); - addDocument: (doc: Doc, relativeTo?: Doc, before?: boolean) => boolean; + addDocument: (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => boolean; indentDocument?: () => void; outdentDocument?: () => void; ScreenToLocalTransform: () => Transform; @@ -60,24 +60,12 @@ export interface TreeViewProps { active: (outsideReaction?: boolean) => boolean; treeViewHideHeaderFields: () => boolean; treeViewPreventOpen: boolean; - renderedIds: string[]; + renderedIds: string[]; // list of document ids rendered used to avoid unending expansion of items in a cycle onCheckedClick?: ScriptField; onChildClick?: ScriptField; ignoreFields?: string[]; } -library.add(faTrashAlt); -library.add(faAngleRight); -library.add(faBell); -library.add(faTrash); -library.add(faCamera); -library.add(faExpand); -library.add(faCaretDown); -library.add(faCaretRight); -library.add(faCaretSquareDown); -library.add(faCaretSquareRight); -library.add(faArrowsAltH); -library.add(faPlus, faMinus); @observer /** * Renders a treeView of a collection of documents @@ -88,66 +76,59 @@ library.add(faPlus, faMinus); * treeViewExpandedView : name of field whose contents are being displayed as the document's subtree */ class TreeView extends React.Component<TreeViewProps> { + static _editTitleScript: ScriptField | undefined; private _header?: React.RefObject<HTMLDivElement> = React.createRef(); private _treedropDisposer?: DragManager.DragDropDisposer; private _dref = React.createRef<HTMLDivElement>(); private _tref = React.createRef<HTMLDivElement>(); + private _docRef = React.createRef<DocumentView>(); get displayName() { return "TreeView(" + this.props.document.title + ")"; } // this makes mobx trace() statements more descriptive - get defaultExpandedView() { return this.childDocs ? this.fieldKey : StrCast(this.props.document.defaultExpandedView, "fields"); } @observable _overrideTreeViewOpen = false; // override of the treeViewOpen field allowing the display state to be independent of the document's state set treeViewOpen(c: boolean) { if (this.props.treeViewPreventOpen) this._overrideTreeViewOpen = c; else this.props.document.treeViewOpen = this._overrideTreeViewOpen = c; } - @computed get treeViewOpen() { return (!this.props.treeViewPreventOpen && BoolCast(this.props.document.treeViewOpen)) || this._overrideTreeViewOpen; } + @computed get treeViewOpen() { return (!this.props.treeViewPreventOpen && !this.props.document.treeViewPreventOpen && BoolCast(this.props.document.treeViewOpen)) || this._overrideTreeViewOpen; } @computed get treeViewExpandedView() { return StrCast(this.props.document.treeViewExpandedView, this.defaultExpandedView); } @computed get MAX_EMBED_HEIGHT() { return NumCast(this.props.containingCollection.maxEmbedHeight, 200); } - @computed get dataDoc() { return this.templateDataDoc ? this.templateDataDoc : this.props.document; } + @computed get dataDoc() { return this.props.document[DataSym]; } @computed get fieldKey() { const splits = StrCast(Doc.LayoutField(this.props.document)).split("fieldKey={\'"); return splits.length > 1 ? splits[1].split("\'")[0] : "data"; } childDocList(field: string) { const layout = Doc.LayoutField(this.props.document) instanceof Doc ? Doc.LayoutField(this.props.document) as Doc : undefined; - return ((this.props.dataDoc ? Cast(this.props.dataDoc[field], listSpec(Doc)) : undefined) || - (layout ? Cast(layout[field], listSpec(Doc)) : undefined) || - Cast(this.props.document[field], listSpec(Doc))) as Doc[]; + return ((this.props.dataDoc ? DocListCast(this.props.dataDoc[field]) : undefined) || // if there's a data doc for an expanded template, use it's data field + (layout ? Cast(layout[field], listSpec(Doc)) : undefined) || // else if there's a layout doc, display it's fields + Cast(this.props.document[field], listSpec(Doc))) as Doc[]; // otherwise use the document's data field } @computed get childDocs() { return this.childDocList(this.fieldKey); } @computed get childLinks() { return this.childDocList("links"); } - @computed get templateDataDoc() { - if (this.props.dataDoc === undefined && Doc.LayoutField(this.props.document) !== "string") { - // if there is no dataDoc (ie, we're not rendering a template layout), but this document has a layout document (not a layout string), - // then we render the layout document as a template and use this document as the data context for the template layout. - return this.props.document; - } - return this.props.dataDoc; - } @computed get boundsOfCollectionDocument() { return StrCast(this.props.document.type).indexOf(DocumentType.COL) === -1 || !DocListCast(this.props.document[this.fieldKey]).length ? undefined : Doc.ComputeContentBounds(DocListCast(this.props.document[this.fieldKey])); } - @undoBatch delete = () => this.props.deleteDoc(this.props.document); - @undoBatch openRight = () => this.props.addDocTab(this.props.dropAction === "alias" ? Doc.MakeAlias(this.props.document) : this.props.document, "onRight", this.props.libraryPath); - @undoBatch indent = () => this.props.addDocument(this.props.document) && this.delete(); - @undoBatch move = (doc: Doc, target: Doc | undefined, addDoc: (doc: Doc) => boolean) => { + @undoBatch openRight = () => this.props.addDocTab(this.props.document, "onRight", this.props.libraryPath); + @undoBatch move = (doc: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => { return this.props.document !== target && this.props.deleteDoc(doc) && addDoc(doc); } - @undoBatch @action remove = (document: Document, key: string) => { - return Doc.RemoveDocFromList(this.dataDoc, key, document); + @undoBatch @action remove = (doc: Doc | Doc[], key: string) => { + return (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => + flg && Doc.RemoveDocFromList(this.dataDoc, key, doc), true); } - @undoBatch @action removeDoc = (document: Document) => { - return Doc.RemoveDocFromList(this.props.containingCollection, Doc.LayoutFieldKey(this.props.containingCollection), document); + @undoBatch @action removeDoc = (doc: Doc | Doc[]) => { + return (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => + flg && Doc.RemoveDocFromList(this.props.containingCollection, Doc.LayoutFieldKey(this.props.containingCollection), doc), true); } protected createTreeDropTarget = (ele: HTMLDivElement) => { - this._treedropDisposer && this._treedropDisposer(); + this._treedropDisposer?.(); ele && (this._treedropDisposer = DragManager.MakeDropTarget(ele, this.treeDrop.bind(this)), this.props.document); } onPointerEnter = (e: React.PointerEvent): void => { this.props.active(true) && Doc.BrushDoc(this.dataDoc); - if (e.buttons === 1 && DragManager.Vals.Instance.GetIsDragging()) { + if (e.buttons === 1 && SnappingManager.GetIsDragging()) { this._header!.current!.className = "treeViewItem-header"; document.addEventListener("pointermove", this.onDragMove, true); } @@ -185,7 +166,7 @@ class TreeView extends React.Component<TreeViewProps> { })} OnFillDown={undoBatch((value: string) => { Doc.SetInPlace(this.props.document, key, value, false); - const doc = Docs.Create.FreeformDocument([], { title: "-", x: 0, y: 0, _width: 100, _height: 25, templates: new List<string>([Templates.Title.Layout]) }); + const doc = Docs.Create.FreeformDocument([], { title: "-", x: 0, y: 0, _width: 100, _height: 25, _LODdisable: true, templates: new List<string>([Templates.Title.Layout]) }); Doc.SetInPlace(this.props.document, "editTitle", undefined, false); Doc.SetInPlace(doc, "editTitle", true, false); return this.props.addDocument(doc); @@ -221,9 +202,10 @@ class TreeView extends React.Component<TreeViewProps> { if (de.complete.docDragData) { e.stopPropagation(); if (de.complete.docDragData.draggedDocuments[0] === this.props.document) return true; - let addDoc = (doc: Doc) => this.props.addDocument(doc, undefined, before); + let addDoc = (doc: Doc | Doc[]) => this.props.addDocument(doc, undefined, before); if (inside) { - addDoc = (doc: Doc) => Doc.AddDocToList(this.dataDoc, this.fieldKey, doc) || addDoc(doc); + addDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce( + ((flg: boolean, doc) => flg && Doc.AddDocToList(this.dataDoc, this.fieldKey, doc)), true) || addDoc(doc); } const movedDocs = (de.complete.docDragData.treeViewId === this.props.treeViewId[Id] ? de.complete.docDragData.draggedDocuments : de.complete.docDragData.droppedDocuments); const move = de.complete.docDragData.dropAction === "move" || de.complete.docDragData.dropAction; @@ -283,8 +265,9 @@ class TreeView extends React.Component<TreeViewProps> { let contentElement: (JSX.Element | null)[] | JSX.Element = []; if (contents instanceof Doc || (Cast(contents, listSpec(Doc)) && (Cast(contents, listSpec(Doc))!.length && Cast(contents, listSpec(Doc))![0] instanceof Doc))) { - const remDoc = (doc: Doc) => this.remove(doc, key); - const addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false, true); + const remDoc = (doc: Doc | Doc[]) => this.remove(doc, key); + const addDoc = (doc: Doc | Doc[], addBefore?: Doc, before?: boolean) => (doc instanceof Doc ? [doc] : doc).reduce( + (flg, doc) => flg && Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false, true), true); contentElement = TreeView.GetChildElements(contents instanceof Doc ? [contents] : DocListCast(contents), this.props.treeViewId, doc, undefined, key, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move, this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, @@ -324,10 +307,12 @@ class TreeView extends React.Component<TreeViewProps> { rtfHeight = () => this.rtfWidth() < Doc.Layout(this.props.document)?.[WidthSym]() ? Math.min(Doc.Layout(this.props.document)?.[HeightSym](), this.MAX_EMBED_HEIGHT) : this.MAX_EMBED_HEIGHT; @computed get renderContent() { + TraceMobx(); const expandKey = this.treeViewExpandedView === this.fieldKey ? this.fieldKey : this.treeViewExpandedView === "links" ? "links" : undefined; if (expandKey !== undefined) { - const remDoc = (doc: Doc) => this.remove(doc, expandKey); - const addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.dataDoc, expandKey, doc, addBefore, before, false, true); + const remDoc = (doc: Doc | Doc[]) => this.remove(doc, expandKey); + const addDoc = (doc: Doc | Doc[], addBefore?: Doc, before?: boolean) => + (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && Doc.AddDocToList(this.dataDoc, expandKey, doc, addBefore, before, false, true), true); const docs = expandKey === "links" ? this.childLinks : this.childDocs; const sortKey = `${this.fieldKey}-sortAscending`; return <ul key={expandKey + "more"} onClick={(e) => { @@ -336,7 +321,7 @@ class TreeView extends React.Component<TreeViewProps> { }}> {!docs ? (null) : TreeView.GetChildElements(docs, this.props.treeViewId, Doc.Layout(this.props.document), - this.templateDataDoc, expandKey, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move, + this.dataDoc, expandKey, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move, StrCast(this.props.document.childDropAction, this.props.dropAction) as dropActionType, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.treeViewHideHeaderFields, this.props.treeViewPreventOpen, [...this.props.renderedIds, this.props.document[Id]], this.props.libraryPath, this.props.onCheckedClick, this.props.onChildClick, this.props.ignoreFields)} @@ -352,7 +337,7 @@ class TreeView extends React.Component<TreeViewProps> { return <div ref={this._dref} style={{ display: "inline-block", height: panelHeight() }} key={this.props.document[Id] + this.props.document.title}> <ContentFittingDocumentView Document={layoutDoc} - DataDoc={this.templateDataDoc} + DataDoc={this.dataDoc} LibraryPath={emptyPath} renderDepth={this.props.renderDepth + 1} rootSelected={returnTrue} @@ -402,13 +387,16 @@ class TreeView extends React.Component<TreeViewProps> { @computed get renderBullet() { const checked = this.props.document.type === DocumentType.COL ? undefined : this.onCheckedClick ? (this.props.document.treeViewChecked ? this.props.document.treeViewChecked : "unchecked") : undefined; - return <div className="bullet" title="view inline" onClick={this.bulletClick} style={{ color: StrCast(this.props.document.color, checked === "unchecked" ? "white" : "inherit"), opacity: checked === "unchecked" ? undefined : 0.4 }}> + return <div className="bullet" + title={this.childDocs?.length ? `click to see ${this.childDocs?.length} items` : "view fields"} + onClick={this.bulletClick} + style={{ color: StrCast(this.props.document.color, checked === "unchecked" ? "white" : "inherit"), opacity: checked === "unchecked" ? undefined : 0.4 }}> {<FontAwesomeIcon icon={checked === "check" ? "check" : (checked === "x" ? "times" : checked === "unchecked" ? "square" : !this.treeViewOpen ? (this.childDocs ? "caret-square-right" : "caret-right") : (this.childDocs ? "caret-square-down" : "caret-down"))} />} </div>; } showContextMenu = (e: React.MouseEvent) => { - simulateMouseClick(this._docRef.current!.ContentDiv!, e.clientX, e.clientY + 30, e.screenX, e.screenY + 30); + this._docRef.current?.ContentDiv && simulateMouseClick(this._docRef.current.ContentDiv, e.clientX, e.clientY + 30, e.screenX, e.screenY + 30); e.stopPropagation(); } focusOnDoc = (doc: Doc) => DocumentManager.Instance.getFirstDocumentView(doc)?.props.focus(doc, true); @@ -416,15 +404,13 @@ class TreeView extends React.Component<TreeViewProps> { const focusScript = ScriptField.MakeFunction(`DocFocus(self)`); return [{ script: focusScript!, label: "Focus" }]; } - _docRef = React.createRef<DocumentView>(); /** * Renders the EditableView title element for placement into the tree. */ @computed get renderTitle() { - const onItemDown = SetupDrag(this._tref, () => this.dataDoc, this.move, this.props.dropAction, this.props.treeViewId[Id], true); - const editTitle = ScriptField.MakeFunction("setInPlace(this, 'editTitle', true)"); - + TraceMobx(); + (!TreeView._editTitleScript) && (TreeView._editTitleScript = ScriptField.MakeFunction("setInPlace(self, 'editTitle', true)")); const headerElements = ( <> <FontAwesomeIcon icon="cog" size="sm" onClick={e => this.showContextMenu(e)}></FontAwesomeIcon> @@ -445,13 +431,12 @@ class TreeView extends React.Component<TreeViewProps> { <FontAwesomeIcon title="open in pane on right" icon="angle-right" size="lg" /> </div>); return <> - <div className="docContainer" ref={this._tref} title="click to edit title" id={`docContainer-${this.props.parentKey}`} onPointerDown={onItemDown} + <div className="docContainer" ref={this._tref} title="click to edit title" id={`docContainer-${this.props.parentKey}`} style={{ - background: Doc.IsHighlighted(this.props.document) ? "orange" : Doc.IsBrushed(this.props.document) ? "#06121212" : "0", fontWeight: this.props.document.searchMatch ? "bold" : undefined, textDecoration: Doc.GetT(this.props.document, "title", "string", true) ? "underline" : undefined, outline: BoolCast(this.props.document.workspaceBrush) ? "dashed 1px #06123232" : undefined, - pointerEvents: this.props.active() || DragManager.Vals.Instance.GetIsDragging() ? undefined : "none" + pointerEvents: this.props.active() || SnappingManager.GetIsDragging() ? undefined : "none" }} > {Doc.GetT(this.props.document, "editTitle", "boolean", true) ? this.editableView("title") : @@ -459,12 +444,13 @@ class TreeView extends React.Component<TreeViewProps> { ref={this._docRef} Document={this.props.document} DataDoc={undefined} - LibraryPath={this.props.libraryPath || []} + treeViewId={this.props.treeViewId[Id]} + LibraryPath={this.props.libraryPath || emptyPath} addDocument={undefined} addDocTab={this.props.addDocTab} rootSelected={returnTrue} pinToPres={emptyFunction} - onClick={this.props.onChildClick || editTitle} + onClick={this.props.onChildClick || TreeView._editTitleScript} dropAction={this.props.dropAction} moveDocument={this.move} removeDocument={this.removeDoc} @@ -476,7 +462,7 @@ class TreeView extends React.Component<TreeViewProps> { NativeWidth={returnZero} contextMenuItems={this.contextMenuItems} renderDepth={1} - focus={emptyFunction} + focus={returnTrue} parentActive={returnTrue} whenActiveChanged={emptyFunction} bringToFront={emptyFunction} @@ -491,8 +477,9 @@ class TreeView extends React.Component<TreeViewProps> { } render() { + TraceMobx(); const sorting = this.props.document[`${this.fieldKey}-sortAscending`]; - setTimeout(() => runInAction(() => untracked(() => this._overrideTreeViewOpen = this.treeViewOpen)), 0); + //setTimeout(() => runInAction(() => untracked(() => this._overrideTreeViewOpen = this.treeViewOpen)), 0); return <div className="treeViewItem-container" ref={this.createTreeDropTarget}> <li className="collection-child"> <div className="treeViewItem-header" ref={this._header} onClick={e => { @@ -500,12 +487,14 @@ class TreeView extends React.Component<TreeViewProps> { e.stopPropagation(); e.preventDefault(); } - }} onPointerDown={e => { - if (this.props.active(true)) { - e.stopPropagation(); - e.preventDefault(); - } - }} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}> + }} + onPointerDown={e => { + if (this.props.active(true)) { + e.stopPropagation(); + e.preventDefault(); + } + }} + onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}> {this.renderBullet} {this.renderTitle} </div> @@ -523,8 +512,8 @@ class TreeView extends React.Component<TreeViewProps> { key: string, parentCollectionDoc: Doc | undefined, parentPrevSibling: Doc | undefined, - add: (doc: Doc, relativeTo?: Doc, before?: boolean) => boolean, - remove: ((doc: Doc) => boolean), + add: (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => boolean, + remove: ((doc: Doc | Doc[]) => boolean), move: DragManager.MoveFunction, dropAction: dropActionType, addDocTab: (doc: Doc, where: string) => boolean, @@ -613,7 +602,7 @@ class TreeView extends React.Component<TreeViewProps> { remove(child); } }; - const addDocument = (doc: Doc, relativeTo?: Doc, before?: boolean) => { + const addDocument = (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => { return add(doc, relativeTo ? relativeTo : docs[i], before !== undefined ? before : false); }; const childLayout = Doc.Layout(pair.layout); @@ -683,22 +672,26 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll } @action - remove = (document: Document): boolean => { - const children = Cast(this.props.Document[DataSym][this.props.fieldKey], listSpec(Doc), []); - if (children.indexOf(document) !== -1) { - children.splice(children.indexOf(document), 1); + remove = (doc: Doc | Doc[]): boolean => { + const docs = doc instanceof Doc ? [doc] : 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; } @action - addDoc = (doc: Document, relativeTo: Opt<Doc>, before?: boolean): boolean => { - const doAddDoc = () => - Doc.AddDocToList(this.props.Document[DataSym], this.props.fieldKey, doc, relativeTo, before, false, false, false); + addDoc = (doc: Doc | Doc[], relativeTo: Opt<Doc>, before?: boolean): boolean => { + const doAddDoc = (doc: Doc | Doc[]) => + (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => + flg && Doc.AddDocToList(this.props.Document[DataSym], this.props.fieldKey, doc, relativeTo, before, false, false, false), true); if (this.props.Document.resolvedDataDoc instanceof Promise) { - this.props.Document.resolvedDataDoc.then((resolved: any) => doAddDoc()); + this.props.Document.resolvedDataDoc.then((resolved: any) => doAddDoc(doc)); } else { - doAddDoc(); + doAddDoc(doc); } return true; } @@ -742,9 +735,16 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll Doc.GetProto(img).doubleClickView = doubleClickView; } }); + Doc.GetProto(d).type = "buxton"; Doc.GetProto(d).proto = heroView; // all devices "are" heroViews that share the same layout & defaults. Seems better than making them all be independent and copy a layout string // .layout = ImageBox.LayoutString("hero"); }); + const iconBuxtonView = ImageDocument(fallbackImg, { title: "hero", _width: 60, onDoubleClick: ScriptField.MakeScript("deiconifyView(self)") }); + iconBuxtonView.isTemplateDoc = makeTemplate(iconBuxtonView, true, "icon_buxton"); + Doc.UserDoc()["template-icon-view-buxton"] = new PrefetchProxy(iconBuxtonView); + const tempIcons = Doc.GetProto(Cast(Doc.UserDoc()["template-icons"], Doc, null)); + Doc.AddDocToList(tempIcons, "data", iconBuxtonView); + Document.childLayoutTemplate = heroView; Document.childClickedOpenTemplateView = new PrefetchProxy(detailView); Document._viewType = CollectionViewType.Time; @@ -767,16 +767,19 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll return <div id="toolbar" key="toolbar"> <button className="toolbar-button round-button" title="Empty" onClick={undoBatch(action(() => Doc.GetProto(this.props.Document)[this.props.fieldKey] = undefined))}> - <FontAwesomeIcon icon={faTrash} size="sm" /> + <FontAwesomeIcon icon={"trash"} size="sm" /> </button> </div >; } + onKeyPress = (e: React.KeyboardEvent) => { + console.log(e); + } render() { if (!(this.props.Document instanceof Doc)) return (null); const dropAction = StrCast(this.props.Document.childDropAction) as dropActionType; - const addDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => this.addDoc(doc, relativeTo, before); - const moveDoc = (d: Doc, target: Doc | undefined, addDoc: (doc: Doc) => boolean) => this.props.moveDocument(d, target, addDoc); + const addDoc = (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => this.addDoc(doc, relativeTo, before); + const moveDoc = (d: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => this.props.moveDocument(d, target, addDoc); const childDocs = this.props.overrideDocuments ? this.props.overrideDocuments : this.childDocs; return !childDocs ? (null) : ( <div className="collectionTreeView-dropTarget" id="body" @@ -786,6 +789,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll paddingRight: `${NumCast(this.props.Document._xPadding, 10)}px`, paddingTop: `${NumCast(this.props.Document._yPadding, 20)}px` }} + onKeyPress={this.onKeyPress} onContextMenu={this.onContextMenu} onWheel={(e: React.WheelEvent) => this._mainEle && this._mainEle.scrollHeight > this._mainEle.clientHeight && e.stopPropagation()} onDrop={this.onTreeDrop} @@ -800,7 +804,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll SetValue={undoBatch((value: string) => Doc.SetInPlace(this.dataDoc, "title", value, false) || true)} OnFillDown={undoBatch((value: string) => { Doc.SetInPlace(this.dataDoc, "title", value, false); - const doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, _width: 100, _height: 25, templates: new List<string>([Templates.Title.Layout]) }); + const doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, _width: 100, _height: 25, _LODdisable: true, templates: new List<string>([Templates.Title.Layout]) }); EditableView.loadId = doc[Id]; Doc.SetInPlace(doc, "editTitle", true, false); this.addDoc(doc, childDocs.length ? childDocs[0] : undefined, true); diff --git a/src/client/views/collections/CollectionView.scss b/src/client/views/collections/CollectionView.scss index d43dd387a..7877fe155 100644 --- a/src/client/views/collections/CollectionView.scss +++ b/src/client/views/collections/CollectionView.scss @@ -70,7 +70,7 @@ height: 30px; position: absolute; bottom: 15px; - left: 15px; + right: 15px; border: 2px solid black; border-radius: 50%; padding: 3px; diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index c22ebbcbd..5b344813d 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -7,17 +7,15 @@ import { observer } from "mobx-react"; 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 '../../../new_fields/DateField'; -import { DataSym, Doc, DocListCast, Field, Opt } from '../../../new_fields/Doc'; -import { List } from '../../../new_fields/List'; -import { BoolCast, Cast, NumCast, StrCast, ScriptCast } from '../../../new_fields/Types'; -import { ImageField } from '../../../new_fields/URLField'; -import { TraceMobx } from '../../../new_fields/util'; +import { DateField } from '../../../fields/DateField'; +import { DataSym, Doc, DocListCast, Field, Opt } from '../../../fields/Doc'; +import { List } from '../../../fields/List'; +import { BoolCast, Cast, NumCast, StrCast, ScriptCast } from '../../../fields/Types'; +import { ImageField } from '../../../fields/URLField'; +import { TraceMobx } from '../../../fields/util'; import { Utils, setupMoveUpEvents, returnFalse, returnZero, emptyPath, emptyFunction, returnOne } from '../../../Utils'; import { DocumentType } from '../../documents/DocumentTypes'; -import { DocumentManager } from '../../util/DocumentManager'; import { ImageUtils } from '../../util/Import & Export/ImageUtils'; -import { SelectionManager } from '../../util/SelectionManager'; import { ContextMenu } from "../ContextMenu"; import { FieldView, FieldViewProps } from '../nodes/FieldView'; import { ScriptBox } from '../ScriptBox'; @@ -37,15 +35,14 @@ import { CollectionTimeView } from './CollectionTimeView'; import { CollectionTreeView } from "./CollectionTreeView"; import './CollectionView.scss'; import { CollectionViewBaseChrome } from './CollectionViewChromes'; -import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; -import { Id } from '../../../new_fields/FieldSymbols'; -import { listSpec } from '../../../new_fields/Schema'; +import { CurrentUserUtils } from '../../util/CurrentUserUtils'; +import { Id } from '../../../fields/FieldSymbols'; +import { listSpec } from '../../../fields/Schema'; import { Docs } from '../../documents/Documents'; -import { ScriptField, ComputedField } from '../../../new_fields/ScriptField'; +import { ScriptField, ComputedField } from '../../../fields/ScriptField'; import { InteractionUtils } from '../../util/InteractionUtils'; -import { ObjectField } from '../../../new_fields/ObjectField'; +import { ObjectField } from '../../../fields/ObjectField'; import CollectionMapView from './CollectionMapView'; -import { Transform } from 'prosemirror-transform'; import { CollectionGridView } from './collectionGrid/CollectionGridView'; import { CollectionPileView } from './CollectionPileView'; const higflyout = require("@hig/flyout"); @@ -74,15 +71,15 @@ export enum CollectionViewType { Pile = "pileup" } export interface CollectionViewCustomProps { - filterAddDocument: (doc: Doc) => boolean; // allows a document that renders a Collection view to filter or modify any documents added to the collection (see PresBox for an example) + filterAddDocument: (doc: Doc | Doc[]) => boolean; // allows a document that renders a Collection view to filter or modify any documents added to the collection (see PresBox for an example) childLayoutTemplate?: () => Opt<Doc>; // specify a layout Doc template to use for children of the collection childLayoutString?: string; // specify a layout string to use for children of the collection } export interface CollectionRenderProps { - addDocument: (document: Doc) => boolean; - removeDocument: (document: Doc) => boolean; - moveDocument: (document: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean) => boolean; + addDocument: (document: Doc | Doc[]) => boolean; + removeDocument: (document: Doc | Doc[]) => boolean; + moveDocument: (document: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean; active: () => boolean; whenActiveChanged: (isActive: boolean) => void; PanelWidth: () => number; @@ -121,35 +118,33 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus whenActiveChanged = (isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive); @action.bound - addDocument(doc: Doc): boolean { - if (this.props.filterAddDocument?.(doc) === false) { - return false; + addDocument = (doc: Doc | Doc[]): boolean => { + if (doc instanceof Doc) { + 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]); - !docList.includes(doc) && (targetDataDoc[this.props.fieldKey] = new List<Doc>([...docList, doc])); // DocAddToList may write to targetdataDoc's parent ... we don't want this. should really change GetProto to GetDataDoc and test for resolvedDataDoc there - // Doc.AddDocToList(targetDataDoc, this.props.fieldKey, doc); - targetDataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now())); - doc.context = this.props.Document; - Doc.GetProto(doc).lastOpened = new DateField; + const added = docs.filter(d => !docList.includes(d)); + if (added.length) { + added.map(doc => 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; } @action.bound - removeDocument(doc: Doc): boolean { + removeDocument = (doc: any): boolean => { + const docs = doc instanceof Doc ? [doc] : doc as Doc[]; const targetDataDoc = this.props.Document[DataSym]; - const docView = DocumentManager.Instance.getDocumentView(doc, this.props.ContainingCollectionView); - docView && SelectionManager.DeselectDoc(docView); const value = DocListCast(targetDataDoc[this.props.fieldKey]); - let index = value.reduce((p, v, i) => (v instanceof Doc && v === doc) ? i : p, -1); - index = index !== -1 ? index : value.reduce((p, v, i) => (v instanceof Doc && Doc.AreProtosEqual(v, doc)) ? i : p, -1); - - doc.context = undefined; - ContextMenu.Instance?.clearItems(); - if (index !== -1) { - value.splice(index, 1); - targetDataDoc[this.props.fieldKey] = new List<Doc>(value); + 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; @@ -160,7 +155,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus // otherwise, the document being moved must be able to be removed from its container before // moving it into the target. @action.bound - moveDocument(doc: Doc, targetCollection: Doc | undefined, addDocument: (doc: Doc) => boolean): boolean { + moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => { if (Doc.AreProtosEqual(this.props.Document, targetCollection)) { return true; } @@ -168,10 +163,14 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus } showIsTagged = () => { - const children = DocListCast(this.props.Document[this.props.fieldKey]); - const imageProtos = children.filter(doc => Cast(doc.data, ImageField)).map(Doc.GetProto); - const allTagged = imageProtos.length > 0 && imageProtos.every(image => image.googlePhotosTags); - return !allTagged ? (null) : <img id={"google-tags"} src={"/assets/google_tags.png"} />; + return (null); + // this section would display an icon in the bototm right of a collection to indicate that all + // photos had been processed through Google's content analysis API and Google's tags had been + // assigned to the documents googlePhotosTags field. + // const children = DocListCast(this.props.Document[this.props.fieldKey]); + // const imageProtos = children.filter(doc => Cast(doc.data, ImageField)).map(Doc.GetProto); + // const allTagged = imageProtos.length > 0 && imageProtos.every(image => image.googlePhotosTags); + // return !allTagged ? (null) : <img id={"google-tags"} src={"/assets/google_tags.png"} />; } private SubViewHelper = (type: CollectionViewType, renderProps: CollectionRenderProps) => { @@ -269,7 +268,6 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus subItems.push({ description: "lightbox", event: action(() => this._isLightboxOpen = true), icon: "eye" }); !existingVm && ContextMenu.Instance.addItem({ description: "View Modes...", subitems: subItems, icon: "eye" }); - this.setupViewTypes("Change Perspective...", (vtype => { this.props.Document._viewType = vtype; return this.props.Document; }), true); this.setupViewTypes("Add a Perspective...", vtype => { const newRendition = Doc.MakeAlias(this.props.Document); newRendition._viewType = vtype; diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx index 62b03bbdc..5dc0b09ac 100644 --- a/src/client/views/collections/CollectionViewChromes.tsx +++ b/src/client/views/collections/CollectionViewChromes.tsx @@ -2,11 +2,11 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; import * as React from "react"; -import { Doc, DocListCast } from "../../../new_fields/Doc"; -import { Id } from "../../../new_fields/FieldSymbols"; -import { List } from "../../../new_fields/List"; -import { listSpec } from "../../../new_fields/Schema"; -import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types"; +import { Doc, DocListCast } 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 { undoBatch } from "../../util/UndoManager"; diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx index 10c6ead1a..649406e6c 100644 --- a/src/client/views/collections/ParentDocumentSelector.tsx +++ b/src/client/views/collections/ParentDocumentSelector.tsx @@ -1,12 +1,12 @@ import * as React from "react"; import './ParentDocumentSelector.scss'; -import { Doc } from "../../../new_fields/Doc"; +import { Doc } from "../../../fields/Doc"; import { observer } from "mobx-react"; import { observable, action, runInAction, trace, computed, reaction, IReactionDisposer } from "mobx"; -import { Id } from "../../../new_fields/FieldSymbols"; +import { Id } from "../../../fields/FieldSymbols"; import { SearchUtil } from "../../util/SearchUtil"; import { CollectionDockingView } from "./CollectionDockingView"; -import { NumCast, StrCast } from "../../../new_fields/Types"; +import { NumCast, StrCast } from "../../../fields/Types"; import { CollectionViewType } from "./CollectionView"; import { DocumentButtonBar } from "../DocumentButtonBar"; import { DocumentManager } from "../../util/DocumentManager"; @@ -94,7 +94,7 @@ export class ParentDocSelector extends React.Component<SelectorProps> { } @observer -export class DockingViewButtonSelector extends React.Component<{ views: DocumentView[], Stack: any }> { +export class DockingViewButtonSelector extends React.Component<{ views: () => DocumentView[], Stack: any }> { customStylesheet(styles: any) { return { ...styles, @@ -120,7 +120,7 @@ export class DockingViewButtonSelector extends React.Component<{ views: Document if (getComputedStyle(this._ref.current!).width !== "100%") { e.stopPropagation(); e.preventDefault(); } - this.props.views[0]?.select(false); + this.props.views()[0]?.select(false); }} className="buttonSelector"> <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={this.flyout} stylesheet={this.customStylesheet}> <FontAwesomeIcon icon={"cog"} size={"sm"} /> diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx index 9a864078a..3860ce2d7 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx @@ -1,15 +1,15 @@ -import { Doc, Field, FieldResult, WidthSym, HeightSym } from "../../../../new_fields/Doc"; -import { NumCast, StrCast, Cast } from "../../../../new_fields/Types"; +import { Doc, Field, FieldResult, WidthSym, HeightSym } from "../../../../fields/Doc"; +import { NumCast, StrCast, Cast } from "../../../../fields/Types"; import { ScriptBox } from "../../ScriptBox"; import { CompileScript } from "../../../util/Scripting"; -import { ScriptField } from "../../../../new_fields/ScriptField"; +import { ScriptField } from "../../../../fields/ScriptField"; import { OverlayView, OverlayElementOptions } from "../../OverlayView"; import { emptyFunction, aggregateBounds } from "../../../../Utils"; import React = require("react"); -import { Id, ToString } from "../../../../new_fields/FieldSymbols"; -import { ObjectField } from "../../../../new_fields/ObjectField"; -import { RefField } from "../../../../new_fields/RefField"; -import { listSpec } from "../../../../new_fields/Schema"; +import { Id, ToString } from "../../../../fields/FieldSymbols"; +import { ObjectField } from "../../../../fields/ObjectField"; +import { RefField } from "../../../../fields/RefField"; +import { listSpec } from "../../../../fields/Schema"; export interface ViewDefBounds { type: string; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index d67d1993e..f3fc04752 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -1,5 +1,5 @@ import { observer } from "mobx-react"; -import { Doc } from "../../../../new_fields/Doc"; +import { Doc } from "../../../../fields/Doc"; import { Utils } from '../../../../Utils'; import { DocumentView } from "../../nodes/DocumentView"; import "./CollectionFreeFormLinkView.scss"; @@ -7,8 +7,9 @@ import React = require("react"); import v5 = require("uuid/v5"); import { DocumentType } from "../../../documents/DocumentTypes"; import { observable, action, reaction, IReactionDisposer } from "mobx"; -import { StrCast } from "../../../../new_fields/Types"; -import { Id } from "../../../../new_fields/FieldSymbols"; +import { StrCast, Cast } from "../../../../fields/Types"; +import { Id } from "../../../../fields/FieldSymbols"; +import { SnappingManager } from "../../../util/SnappingManager"; export interface CollectionFreeFormLinkViewProps { A: DocumentView; @@ -24,6 +25,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo 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; 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") : []; @@ -79,11 +81,11 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo componentWillUnmount() { this._anchorDisposer?.(); } - render() { + if (SnappingManager.GetIsDragging()) return null; this.props.A.props.ScreenToLocalTransform().transform(this.props.B.props.ScreenToLocalTransform()); - const acont = this.props.A.props.Document.type === DocumentType.LINK ? this.props.A.ContentDiv!.getElementsByClassName("linkAnchorBox-cont") : []; - const bcont = this.props.B.props.Document.type === DocumentType.LINK ? this.props.B.ContentDiv!.getElementsByClassName("linkAnchorBox-cont") : []; + const 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, @@ -105,15 +107,11 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo 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={(pt1[0] + pt2[0]) / 2} y={(pt1[1] + pt2[1]) / 2}> + <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> <path className="collectionfreeformlinkview-linkLine" style={{ opacity: this._opacity, strokeDasharray: "2 2" }} d={`M ${pt1[0]} ${pt1[1]} C ${pt1[0] + pt1norm[0]} ${pt1[1] + pt1norm[1]}, ${pt2[0] + pt2norm[0]} ${pt2[1] + pt2norm[1]}, ${pt2[0]} ${pt2[1]}`} /> - {/* <line key="linkLine" className="collectionfreeformlinkview-linkLine" - style={{ opacity: this._opacity, strokeDasharray: "2 2" }} - x1={`${pt1[0]}`} y1={`${pt1[1]}`} - x2={`${pt2[0]}`} y2={`${pt2[1]}`} /> */} </>); } }
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx index 0873fd1bb..ae81b4b36 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx @@ -1,16 +1,15 @@ import { computed } from "mobx"; import { observer } from "mobx-react"; -import { Doc } from "../../../../new_fields/Doc"; -import { Id } from "../../../../new_fields/FieldSymbols"; +import { Doc } from "../../../../fields/Doc"; +import { Id } from "../../../../fields/FieldSymbols"; import { DocumentManager } from "../../../util/DocumentManager"; import { DocumentView } from "../../nodes/DocumentView"; import "./CollectionFreeFormLinksView.scss"; import { CollectionFreeFormLinkView } from "./CollectionFreeFormLinkView"; import React = require("react"); import { Utils, emptyFunction } from "../../../../Utils"; -import { SelectionManager } from "../../../util/SelectionManager"; import { DocumentType } from "../../../documents/DocumentTypes"; -import { DragManager } from "../../../util/DragManager"; +import { SnappingManager } from "../../../util/SnappingManager"; @observer export class CollectionFreeFormLinksView extends React.Component { @@ -32,12 +31,12 @@ export class CollectionFreeFormLinksView extends React.Component { }, [] as { a: DocumentView, b: DocumentView, l: Doc[] }[]); return connections.filter(c => c.a.props.Document.type === DocumentType.LINK && - c.a.props.bringToFront !== emptyFunction && c.b.props.bringToFront !== emptyFunction // bcz: this prevents links to be drawn to anchors in CollectionTree views -- this is a hack that should be fixed + c.a.props.pinToPres !== emptyFunction && c.b.props.pinToPres !== emptyFunction // bcz: this prevents links to be drawn to anchors in CollectionTree views -- this is a hack that should be fixed ).map(c => <CollectionFreeFormLinkView key={Utils.GenerateGuid()} A={c.a} B={c.b} LinkDocs={c.l} />); } render() { - return DragManager.Vals.Instance.GetIsDragging() ? (null) : <div className="collectionfreeformlinksview-container"> + return SnappingManager.GetIsDragging() ? (null) : <div className="collectionfreeformlinksview-container"> <svg className="collectionfreeformlinksview-svgCanvas"> {this.uniqueConnections} </svg> diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx index 92fa2781c..548ad78a5 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx @@ -1,16 +1,16 @@ import { observer } from "mobx-react"; import * as mobxUtils from 'mobx-utils'; -import CursorField from "../../../../new_fields/CursorField"; -import { listSpec } from "../../../../new_fields/Schema"; -import { Cast } from "../../../../new_fields/Types"; -import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils"; +import CursorField from "../../../../fields/CursorField"; +import { listSpec } from "../../../../fields/Schema"; +import { Cast } from "../../../../fields/Types"; +import { CurrentUserUtils } from "../../../util/CurrentUserUtils"; import { CollectionViewProps } from "../CollectionSubView"; import "./CollectionFreeFormView.scss"; import React = require("react"); import v5 = require("uuid/v5"); import { computed } from "mobx"; -import { FieldResult } from "../../../../new_fields/Doc"; -import { List } from "../../../../new_fields/List"; +import { FieldResult } from "../../../../fields/Doc"; +import { List } from "../../../../fields/List"; @observer export class CollectionFreeFormRemoteCursors extends React.Component<CollectionViewProps> { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 2aa9b1f5f..bf679309c 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,21 +1,21 @@ import { library } from "@fortawesome/fontawesome-svg-core"; import { faEye, faEyeSlash } from "@fortawesome/free-regular-svg-icons"; import { faBraille, faChalkboard, faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faFileUpload, faPaintBrush, faTable, faUpload } from "@fortawesome/free-solid-svg-icons"; -import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from "mobx"; +import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, _allowStateChangesInsideComputed } from "mobx"; import { observer } from "mobx-react"; import { computedFn } from "mobx-utils"; -import { Doc, HeightSym, Opt, WidthSym, DocListCast } from "../../../../new_fields/Doc"; -import { documentSchema, collectionSchema } from "../../../../new_fields/documentSchemas"; -import { Id } from "../../../../new_fields/FieldSymbols"; -import { InkData, InkField, InkTool, PointData } from "../../../../new_fields/InkField"; -import { List } from "../../../../new_fields/List"; -import { RichTextField } from "../../../../new_fields/RichTextField"; -import { createSchema, listSpec, makeInterface } from "../../../../new_fields/Schema"; -import { ScriptField } from "../../../../new_fields/ScriptField"; -import { BoolCast, Cast, FieldValue, NumCast, ScriptCast, StrCast } from "../../../../new_fields/Types"; -import { TraceMobx } from "../../../../new_fields/util"; +import { Doc, HeightSym, Opt, WidthSym, DocListCast } from "../../../../fields/Doc"; +import { documentSchema, collectionSchema } from "../../../../fields/documentSchemas"; +import { Id } from "../../../../fields/FieldSymbols"; +import { InkData, InkField, InkTool, PointData } from "../../../../fields/InkField"; +import { List } from "../../../../fields/List"; +import { RichTextField } from "../../../../fields/RichTextField"; +import { createSchema, listSpec, makeInterface } from "../../../../fields/Schema"; +import { ScriptField, ComputedField } from "../../../../fields/ScriptField"; +import { BoolCast, Cast, FieldValue, NumCast, ScriptCast, StrCast } from "../../../../fields/Types"; +import { TraceMobx } from "../../../../fields/util"; import { GestureUtils } from "../../../../pen-gestures/GestureUtils"; -import { aggregateBounds, intersectRect, returnOne, Utils, returnZero, returnFalse } from "../../../../Utils"; +import { aggregateBounds, intersectRect, returnOne, Utils, returnZero, returnFalse, numberRange } from "../../../../Utils"; import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; import { DocServer } from "../../../DocServer"; import { Docs } from "../../../documents/Documents"; @@ -45,6 +45,7 @@ import { MarqueeView } from "./MarqueeView"; import React = require("react"); import { CollectionViewType } from "../CollectionView"; import { Timeline } from "../../animationtimeline/Timeline"; +import { SnappingManager } from "../../../util/SnappingManager"; library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload); @@ -112,8 +113,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P this.props.PanelWidth() / (this.contentBounds.r - this.contentBounds.x)) : this.Document.scale || 1) - private centeringShiftX = () => !this.nativeWidth && !this.isAnnotationOverlay ? this.props.PanelWidth() / 2 / this.parentScaling : 0; // shift so pan position is at center of window for non-overlay collections - private centeringShiftY = () => !this.nativeHeight && !this.isAnnotationOverlay ? this.props.PanelHeight() / 2 / this.parentScaling : 0;// shift so pan position is at center of window for non-overlay collections + private centeringShiftX = () => !this.isAnnotationOverlay ? this.props.PanelWidth() / 2 / this.parentScaling / this.contentScaling : 0; // shift so pan position is at center of window for non-overlay collections + private centeringShiftY = () => !this.isAnnotationOverlay ? this.props.PanelHeight() / 2 / this.parentScaling / this.contentScaling : 0;// shift so pan position is at center of window for non-overlay collections private getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth + 1, -this.borderWidth + 1).translate(-this.centeringShiftX(), -this.centeringShiftY()).transform(this.getLocalTransform()); private getTransformOverlay = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth + 1, -this.borderWidth + 1); private getContainerTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth, -this.borderWidth); @@ -122,11 +123,27 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P FormattedTextBox.SelectOnLoad = newBox[Id];// track the new text box so we can give it a prop that tells it to focus itself when it's displayed this.addDocument(newBox); } - private addDocument = (newBox: Doc) => { - const added = this.props.addDocument(newBox); - added && this.bringToFront(newBox); - added && this.updateCluster(newBox); - return added; + addDocument = (newBox: Doc | Doc[]) => { + const timecode = Cast(this.props.Document.timecode, "number", null); + if (timecode !== undefined) { + ((newBox instanceof Doc) ? [newBox] : newBox).map(doc => { + doc["x-indexed"] = new List<number>(numberRange(timecode + 1).map(i => NumCast(doc.x))); + doc["y-indexed"] = new List<number>(numberRange(timecode + 1).map(i => NumCast(doc.y))); + doc.timecode = ComputedField.MakeFunction("collection.timecode", {}, { collection: this.props.Document }); + doc.x = ComputedField.MakeInterpolated("x", "timecode"); + doc.y = ComputedField.MakeInterpolated("y", "timecode"); + }); + } + + if (newBox instanceof Doc) { + const added = this.props.addDocument(newBox); + added && this.bringToFront(newBox); + added && this.updateCluster(newBox); + return added; + } else { + return this.props.addDocument(newBox); + // bcz: deal with clusters + } } private selectDocuments = (docs: Doc[]) => { SelectionManager.DeselectAll(); @@ -152,6 +169,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P const xfo = this.getTransformOverlay(); const [xp, yp] = xf.transformPoint(de.x, de.y); const [xpo, ypo] = xfo.transformPoint(de.x, de.y); + const zsorted = this.childLayoutPairs.map(pair => pair.layout).slice().sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex)); if (super.onInternalDrop(e, de)) { if (de.complete.docDragData) { if (de.complete.docDragData.droppedDocuments.length) { @@ -161,20 +179,25 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P const y = (z ? ypo : yp) - de.complete.docDragData.offset[1]; const dropX = NumCast(firstDoc.x); const dropY = NumCast(firstDoc.y); - de.complete.docDragData.droppedDocuments.forEach(action((d: Doc) => { - const layoutDoc = Doc.Layout(d); - d.x = x + NumCast(d.x) - dropX; - d.y = y + NumCast(d.y) - dropY; - if (!NumCast(layoutDoc._width)) { - layoutDoc._width = 300; - } - if (!NumCast(layoutDoc._height)) { - const nw = NumCast(layoutDoc._nativeWidth); - const nh = NumCast(layoutDoc._nativeHeight); - layoutDoc._height = nw && nh ? nh / nw * NumCast(layoutDoc._width) : 300; + const droppedDocs = de.complete.docDragData.droppedDocuments; + runInAction(() => { + zsorted.forEach((doc, index) => doc.zIndex = index + 1); + for (let i = 0; i < droppedDocs.length; i++) { + const d = droppedDocs[i]; + const layoutDoc = Doc.Layout(d); + d.x = x + NumCast(d.x) - dropX; + d.y = y + NumCast(d.y) - dropY; + if (!NumCast(layoutDoc._width)) { + layoutDoc._width = 300; + } + if (!NumCast(layoutDoc._height)) { + const nw = NumCast(layoutDoc._nativeWidth); + const nh = NumCast(layoutDoc._nativeHeight); + layoutDoc._height = nw && nh ? nh / nw * NumCast(layoutDoc._width) : 300; + } + d.isBackground === undefined && (d.zIndex = zsorted.length + 1 + i); // bringToFront } - d.isBackground === undefined && this.bringToFront(d); - })); + }); (de.complete.docDragData.droppedDocuments.length === 1 || de.shiftKey) && this.updateClusterDocs(de.complete.docDragData.droppedDocuments); } @@ -327,6 +350,13 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } this._hitCluster = this.props.Document.useClusters ? this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)) !== -1 : false; if (e.button === 0 && (!e.shiftKey || this._hitCluster) && !e.altKey && !e.ctrlKey && this.props.active(true)) { + + // if (!this.props.Document.aliasOf && !this.props.ContainingCollectionView) { + // this.props.addDocTab(this.props.Document, "replace"); + // e.stopPropagation(); + // e.preventDefault(); + // return; + // } document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); document.addEventListener("pointermove", this.onPointerMove); @@ -382,7 +412,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P case GestureUtils.Gestures.Stroke: const points = ge.points; const B = this.getTransform().transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height); - const inkDoc = Docs.Create.InkDocument(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, parseInt(InkingControl.Instance.selectedWidth), points, { title: "ink stroke", x: B.x, y: B.y, _width: B.width, _height: B.height }); + const inkDoc = Docs.Create.InkDocument(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, InkingControl.Instance.selectedWidth, points, { title: "ink stroke", x: B.x, y: B.y, _width: B.width, _height: B.height }); this.addDocument(inkDoc); e.stopPropagation(); break; @@ -551,7 +581,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P document.removeEventListener("pointerup", this.onPointerUp); return; } - this.pan(e); + (!MarqueeView.DragMarquee || e.altKey) && this.pan(e); } e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers e.preventDefault(); @@ -697,7 +727,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @action zoom = (pointX: number, pointY: number, deltaY: number): void => { - let deltaScale = deltaY > 0 ? (1 / 1.1) : 1.1; + let deltaScale = deltaY > 0 ? (1 / 1.05) : 1.05; if (deltaScale * this.zoomScaling() < 1 && this.isAnnotationOverlay) { deltaScale = 1 / this.zoomScaling(); } @@ -705,7 +735,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P const [x, y] = this.getTransform().transformPoint(pointX, pointY); const localTransform = this.getLocalTransform().inverse().scaleAbout(deltaScale, x, y); - if (localTransform.Scale >= 0.15) { + if (localTransform.Scale >= 0.15 || localTransform.Scale > this.zoomScaling()) { const safeScale = Math.min(Math.max(0.15, localTransform.Scale), 40); this.props.Document.scale = Math.abs(safeScale); this.setPan(-localTransform.TranslateX / safeScale, -localTransform.TranslateY / safeScale); @@ -720,7 +750,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } else if (this.props.active(true)) { e.stopPropagation(); - this.zoom(e.clientX, e.clientY, e.deltaY); + if (!e.ctrlKey && MarqueeView.DragMarquee) this.setPan(this.panX() + e.deltaX, this.panY() + e.deltaY, "None", true); + else this.zoom(e.clientX, e.clientY, e.deltaY); } this.props.Document.targetScale = NumCast(this.props.Document.scale); } @@ -759,20 +790,21 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } } - bringToFront = (doc: Doc, sendToBack?: boolean) => { + bringToFront = action((doc: Doc, sendToBack?: boolean) => { if (sendToBack || doc.isBackground) { doc.zIndex = 0; } else { const docs = this.childLayoutPairs.map(pair => pair.layout); - docs.slice().sort((doc1, doc2) => { - if (doc1 === doc) return 1; - if (doc2 === doc) return -1; - return NumCast(doc1.zIndex) - NumCast(doc2.zIndex); - }).forEach((doc, index) => doc.zIndex = index + 1); - doc.zIndex = docs.length + 1; + docs.slice().sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex)); + let zlast = docs.length ? NumCast(docs[docs.length - 1].zIndex) : 1; + if (zlast - docs.length > 100) { + for (let i = 0; i < docs.length; i++) doc.zIndex = i + 1; + zlast = docs.length + 1; + } + doc.zIndex = zlast + 1; } - } + }); scaleAtPt(docpt: number[], scale: number) { const screenXY = this.getTransform().inverse().transformPoint(docpt[0], docpt[1]); @@ -872,6 +904,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P FreezeDimensions: this.props.freezeChildDimensions, layoutKey: undefined, setupDragLines: this.setupDragLines, + dontRegisterView: this.props.dontRegisterView, rootSelected: childData ? this.rootSelected : returnFalse, dropAction: StrCast(this.props.Document.childDropAction) as dropActionType, onClick: this.onChildClickHandler, @@ -894,14 +927,22 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P addDocTab = action((doc: Doc, where: string) => { if (where === "inParent") { - const pt = this.getTransform().transformPoint(NumCast(doc.x), NumCast(doc.y)); - doc.x = pt[0]; - doc.y = pt[1]; - this.props.addDocument(doc); - return true; + if (doc instanceof Doc) { + const pt = this.getTransform().transformPoint(NumCast(doc.x), NumCast(doc.y)); + doc.x = pt[0]; + doc.y = pt[1]; + return this.props.addDocument(doc); + } else { + (doc as any as Doc[]).forEach(doc => { + const pt = this.getTransform().transformPoint(NumCast(doc.x), NumCast(doc.y)); + doc.x = pt[0]; + doc.y = pt[1]; + }); + return this.props.addDocument(doc); + } } if (where === "inPlace" && this.layoutDoc.isInPlaceContainer) { - this.dataDoc[this.props.fieldKey] = new List<Doc>([doc]); + this.dataDoc[this.props.fieldKey] = doc instanceof Doc ? doc : new List<Doc>(doc as any as Doc[]); return true; } return this.props.addDocTab(doc, where); @@ -915,6 +956,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P const { x, y, z, color, zIndex } = params.pair.layout; return { x: NumCast(x), y: NumCast(y), z: Cast(z, "number"), color: StrCast(color), zIndex: Cast(zIndex, "number"), + transition: StrCast(layoutDoc.transition), width: Cast(layoutDoc._width, "number"), height: Cast(layoutDoc._height, "number"), pair: params.pair, replica: "" }; } @@ -1001,8 +1043,9 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P get doLayoutComputation() { const { newPool, computedElementData } = this.doInternalLayoutComputation; - runInAction(() => - Array.from(newPool.entries()).map(entry => { + const array = Array.from(newPool.entries()); + runInAction(() => { + for (const entry of array) { const lastPos = this._cachedPool.get(entry[0]); // last computed pos const newPos = entry[1]; if (!lastPos || newPos.x !== lastPos.x || newPos.y !== lastPos.y || newPos.z !== lastPos.z || newPos.zIndex !== lastPos.zIndex) { @@ -1011,7 +1054,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P if (!lastPos || newPos.height !== lastPos.height || newPos.width !== lastPos.width) { this._layoutSizeData.set(entry[0], { width: newPos.width, height: newPos.height }); } - })); + } + }); this._cachedPool.clear(); Array.from(newPool.entries()).forEach(k => this._cachedPool.set(k[0], k[1])); const elements: ViewDefResult[] = computedElementData.slice(); @@ -1036,9 +1080,14 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P bounds: this.childDataProvider(entry[1].pair.layout, entry[1].replica) })); + if (this.props.isAnnotationOverlay) { + this.props.Document.scale = Math.max(1, NumCast(this.props.Document.scale)); + } + return elements; } + @action componentDidMount() { super.componentDidMount?.(); this._layoutComputeReaction = reaction(() => this.doLayoutComputation, @@ -1057,12 +1106,13 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } promoteCollection = undoBatch(action(() => { - this.childDocs.forEach(doc => { + const childDocs = this.childDocs.slice(); + childDocs.forEach(doc => { const scr = this.getTransform().inverse().transformPoint(NumCast(doc.x), NumCast(doc.y)); doc.x = scr?.[0]; doc.y = scr?.[1]; - this.props.addDocTab(doc, "inParent") && this.props.removeDocument(doc); }); + this.props.addDocTab(childDocs as any as Doc, "inParent"); this.props.ContainingCollectionView?.removeDocument(this.props.Document); })); layoutDocsInGrid = () => { @@ -1086,22 +1136,69 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P }); }, "arrange contents"); } + @undoBatch + @action + toggleNativeDimensions = () => { + Doc.toggleNativeDimensions(this.layoutDoc, this.props.ContentScaling(), this.props.NativeWidth(), this.props.NativeHeight()); + } + + @undoBatch + @action + snaphsotInterpolated = (): void => { + if (this.props.Document.timecode === undefined) { + this.childDocs.map(doc => { + this.props.Document.timecode = 0; + doc["x-indexed"] = new List<number>([NumCast(doc.x)]); + doc["y-indexed"] = new List<number>([NumCast(doc.y)]); + doc.timecode = ComputedField.MakeFunction("collection.timecode", {}, { collection: this.props.Document }); + doc.x = ComputedField.MakeInterpolated("x", "timecode"); + doc.y = ComputedField.MakeInterpolated("y", "timecode"); + }); + } + const timecode = NumCast(this.props.Document.timecode); + this.childDocs.map(doc => { + const xindexed = Cast(doc['x-indexed'], listSpec("number"), null); + const yindexed = Cast(doc['y-indexed'], listSpec("number"), null); + xindexed.length <= timecode + 1 && xindexed.push(NumCast(doc.x)); + yindexed.length <= timecode + 1 && yindexed.push(NumCast(doc.y)); + }); + this.childDocs.map(doc => doc.transition = "transform 1s"); + this.props.Document.timecode = Math.max(0, timecode + 1); + setTimeout(() => this.childDocs.map(doc => doc.transition = undefined), 1010); + } + @undoBatch + @action + backupInterpolated = (): void => { + this.childDocs.map(doc => doc.transition = "transform 1s"); + this.props.Document.timecode = Math.max(0, NumCast(this.props.Document.timecode) - 1); + setTimeout(() => this.childDocs.map(doc => doc.transition = undefined), 1010); + } + private thumbIdentifier?: number; onContextMenu = (e: React.MouseEvent) => { - if (this.props.children && this.props.annotationsKey) return; + if (this.props.annotationsKey) return; + + ContextMenu.Instance.addItem({ + description: (this._timelineVisible ? "Close" : "Open") + " Animation Timeline", event: action(() => { + this._timelineVisible = !this._timelineVisible; + }), icon: this._timelineVisible ? faEyeSlash : faEye + }); + ContextMenu.Instance.addItem({ description: "Advance", event: this.snaphsotInterpolated, icon: BoolCast(this.Document.lockedTransform) ? "unlock" : "lock" }); + ContextMenu.Instance.addItem({ description: "Backup ", event: this.backupInterpolated, icon: BoolCast(this.Document.lockedTransform) ? "unlock" : "lock" }); + const options = ContextMenu.Instance.findByDescription("Options..."); const optionItems: ContextMenuProps[] = options && "subitems" in options ? options.subitems : []; optionItems.push({ description: "reset view", event: () => { this.props.Document._panX = this.props.Document._panY = 0; this.props.Document.scale = 1; }, icon: "compress-arrows-alt" }); - optionItems.push({ description: `${this.Document._LODdisable ? "Enable LOD" : "Disable LOD"}`, event: () => this.Document._LODdisable = !this.Document._LODdisable, icon: "table" }); + optionItems.push({ description: "Reset default note style", event: () => Doc.UserDoc().defaultTextLayout = undefined, icon: "eye" }); + optionItems.push({ description: (!this.layoutDoc._nativeWidth || !this.layoutDoc._nativeHeight ? "Freeze" : "Unfreeze") + " Aspect", event: this.toggleNativeDimensions, icon: "snowflake" }); optionItems.push({ description: `${this.fitToContent ? "Unset" : "Set"} Fit To Container`, event: () => this.Document._fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" }); optionItems.push({ description: `${this.Document.useClusters ? "Uncluster" : "Use Clusters"}`, event: () => this.updateClusters(!this.Document.useClusters), icon: "braille" }); this.props.ContainingCollectionView && optionItems.push({ description: "Promote Collection", event: this.promoteCollection, icon: "table" }); optionItems.push({ description: "Arrange contents in grid", event: this.layoutDocsInGrid, icon: "table" }); // layoutItems.push({ description: "Analyze Strokes", event: this.analyzeStrokes, icon: "paint-brush" }); - optionItems.push({ description: "Jitter Rotation", event: action(() => this.props.Document._jitterRotation = (this.props.Document._jitterRotation ? 0 : 10)), icon: "paint-brush" }); optionItems.push({ description: "Import document", icon: "upload", event: ({ x, y }) => { const input = document.createElement("input"); @@ -1129,14 +1226,9 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P input.click(); } }); + optionItems.push({ description: `${this.Document._LODdisable ? "Enable LOD" : "Disable LOD"}`, event: () => this.Document._LODdisable = !this.Document._LODdisable, icon: "table" }); ContextMenu.Instance.addItem({ description: "Options...", subitems: optionItems, icon: "eye" }); - - ContextMenu.Instance.addItem({ - description: (this._timelineVisible ? "Close" : "Open") + " Animation Timeline", event: action(() => { - this._timelineVisible = !this._timelineVisible; - }), icon: this._timelineVisible ? faEyeSlash : faEye - }); } @observable _timelineVisible = false; @@ -1147,7 +1239,12 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @action setupDragLines = () => { - const size = this.props.ScreenToLocalTransform().transformDirection(this.props.PanelWidth(), this.props.PanelHeight()); + const activeDocs = this.getActiveDocuments(); + if (activeDocs.length > 50) { + DragManager.SetSnapLines([], []); + return; + } + const size = this.getTransform().transformDirection(this.props.PanelWidth(), this.props.PanelHeight()); const selRect = { left: this.panX() - size[0] / 2, top: this.panY() - size[1] / 2, width: size[0], height: size[1] }; const docDims = (doc: Doc) => ({ left: NumCast(doc.x), top: NumCast(doc.y), width: NumCast(doc._width), height: NumCast(doc._height) }); const isDocInView = (doc: Doc, rect: { left: number, top: number, width: number, height: number }) => { @@ -1174,7 +1271,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P DragManager.SetSnapLines(horizLines, vertLines); } onPointerOver = (e: React.PointerEvent) => { - if (DragManager.Vals.Instance.GetIsDragging()) { + if (SnappingManager.GetIsDragging()) { this.setupDragLines(); } e.stopPropagation(); @@ -1215,8 +1312,16 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P return false; }); @computed get marqueeView() { - return <MarqueeView {...this.props} nudge={this.nudge} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} addDocument={this.addDocument} - addLiveTextDocument={this.addLiveTextBox} getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}> + return <MarqueeView {...this.props} + nudge={this.nudge} + addDocTab={this.addDocTab} + activeDocuments={this.getActiveDocuments} + selectDocuments={this.selectDocuments} + addDocument={this.addDocument} + addLiveTextDocument={this.addLiveTextBox} + getContainerTransform={this.getContainerTransform} + getTransform={this.getTransform} + isAnnotationOverlay={this.isAnnotationOverlay}> <CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY} shifted={!this.nativeHeight && !this.isAnnotationOverlay} easing={this.easing} viewDefDivClick={this.props.viewDefDivClick} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}> {this.children} @@ -1233,7 +1338,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P const wscale = nw ? this.props.PanelWidth() / nw : 1; return wscale < hscale ? wscale : hscale; } - @computed get backgroundEvents() { return this.layoutDoc.isBackground && DragManager.Vals.Instance.GetIsDragging(); } + @computed get backgroundEvents() { return this.layoutDoc.isBackground && SnappingManager.GetIsDragging(); } render() { TraceMobx(); const clientRect = this._mainCont?.getBoundingClientRect(); @@ -1247,7 +1352,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P return <div className={"collectionfreeformview-container"} ref={this.createDashEventsTarget} onPointerOver={this.onPointerOver} - onWheel={this.onPointerWheel} onClick={this.onClick} //pointerEvents: DragManager.Vals.Instance.GetIsDragging() ? "all" : undefined, + onWheel={this.onPointerWheel} onClick={this.onClick} //pointerEvents: DraggingManager.GetIsDragging() ? "all" : undefined, onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onExternalDrop.bind(this)} onContextMenu={this.onContextMenu} style={{ pointerEvents: this.backgroundEvents ? "all" : undefined, diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss index 1291e7dc1..a811dd15a 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss @@ -7,6 +7,7 @@ height:100%; overflow: hidden; border-radius: inherit; + user-select: none; } .marqueeView:focus-within { diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 5bac075b4..492ba6ed6 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -1,37 +1,34 @@ import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCast, DataSym, WidthSym, HeightSym, Opt } from "../../../../new_fields/Doc"; -import { InkField, InkData } from "../../../../new_fields/InkField"; -import { List } from "../../../../new_fields/List"; -import { SchemaHeaderField } from "../../../../new_fields/SchemaHeaderField"; -import { Cast, NumCast, FieldValue, StrCast } from "../../../../new_fields/Types"; +import { Doc, Opt } from "../../../../fields/Doc"; +import { InkData, InkField } from "../../../../fields/InkField"; +import { List } from "../../../../fields/List"; +import { RichTextField } from "../../../../fields/RichTextField"; +import { SchemaHeaderField } from "../../../../fields/SchemaHeaderField"; +import { Cast, FieldValue, NumCast, StrCast } from "../../../../fields/Types"; import { Utils } from "../../../../Utils"; -import { Docs, DocUtils, DocumentOptions } from "../../../documents/Documents"; +import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; +import { Docs, DocumentOptions, DocUtils } from "../../../documents/Documents"; import { SelectionManager } from "../../../util/SelectionManager"; import { Transform } from "../../../util/Transform"; import { undoBatch } from "../../../util/UndoManager"; import { ContextMenu } from "../../ContextMenu"; +import { FormattedTextBox } from "../../nodes/formattedText/FormattedTextBox"; import { PreviewCursor } from "../../PreviewCursor"; import { SubCollectionViewProps } from "../CollectionSubView"; +import { CollectionView } from "../CollectionView"; import MarqueeOptionsMenu from "./MarqueeOptionsMenu"; import "./MarqueeView.scss"; import React = require("react"); -import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; -import { RichTextField } from "../../../../new_fields/RichTextField"; -import { CollectionView } from "../CollectionView"; -import { FormattedTextBox } from "../../nodes/formattedText/FormattedTextBox"; -import { ScriptField } from "../../../../new_fields/ScriptField"; +import { InteractionUtils } from "../../../util/InteractionUtils"; interface MarqueeViewProps { getContainerTransform: () => Transform; getTransform: () => Transform; - addDocument: (doc: Doc) => boolean; activeDocuments: () => Doc[]; selectDocuments: (docs: Doc[], ink: { Document: Doc, Ink: Map<any, any> }[]) => void; - removeDocument: (doc: Doc) => boolean; addLiveTextDocument: (doc: Doc) => void; isSelected: () => boolean; - isAnnotationOverlay?: boolean; nudge: (x: number, y: number) => boolean; setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void; } @@ -39,7 +36,7 @@ interface MarqueeViewProps { @observer export class MarqueeView extends React.Component<SubCollectionViewProps & MarqueeViewProps> { - private _mainCont = React.createRef<HTMLDivElement>(); + @observable public static DragMarquee = false; @observable _lastX: number = 0; @observable _lastY: number = 0; @observable _downX: number = 0; @@ -108,7 +105,11 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque }); } else if (!e.ctrlKey) { FormattedTextBox.SelectOnLoadChar = FormattedTextBox.DefaultLayout ? e.key : ""; - const tbox = Docs.Create.TextDocument("", { _width: 200, _height: 100, x: x, y: y, _autoHeight: true, title: "-typed text-" }); + const tbox = Docs.Create.TextDocument("", { + _width: 200, _height: 100, x: x, y: y, _autoHeight: true, _fontSize: NumCast(Doc.UserDoc().fontSize), + _backgroundColor: StrCast(Doc.UserDoc().backgroundColor), + title: "-typed text-" + }); const template = FormattedTextBox.DefaultLayout; if (template instanceof Doc) { tbox._width = NumCast(template._width); @@ -164,12 +165,13 @@ 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; - if (e.button === 2 || (e.button === 0 && e.altKey)) { + // 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); - if (e.altKey) { - //e.stopPropagation(); // bcz: removed so that you can alt-click on button in a collection to switch link following behaviors. - e.preventDefault(); - } + // (!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(); } @@ -191,14 +193,13 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque } else { this.cleanupInteractions(true); // stop listening for events if another lower-level handle (e.g. another Marquee) has stopPropagated this } - if (e.altKey) { + if (e.altKey || MarqueeView.DragMarquee) { e.preventDefault(); } } @action onPointerUp = (e: PointerEvent): void => { - if (!this.props.active(true)) this.props.selectDocuments([this.props.Document], []); if (this._visible) { const mselect = this.marqueeSelect(); if (!e.shiftKey) { @@ -209,6 +210,11 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque const docs = mselect.length ? mselect : [this.props.Document]; this.props.selectDocuments(docs, []); } + const hideMarquee = () => { + this.hideMarquee(); + MarqueeOptionsMenu.Instance.fadeOut(true); + document.removeEventListener("pointerdown", hideMarquee); + }; if (!this._commandExecuted && (Math.abs(this.Bounds.height * this.Bounds.width) > 100)) { MarqueeOptionsMenu.Instance.createCollection = this.collection; MarqueeOptionsMenu.Instance.delete = this.delete; @@ -217,17 +223,13 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque MarqueeOptionsMenu.Instance.showMarquee = this.showMarquee; MarqueeOptionsMenu.Instance.hideMarquee = this.hideMarquee; MarqueeOptionsMenu.Instance.jumpTo(e.clientX, e.clientY); + document.addEventListener("pointerdown", hideMarquee); + } else { + this.hideMarquee(); } this.cleanupInteractions(true, this._commandExecuted); - const hideMarquee = () => { - this.hideMarquee(); - MarqueeOptionsMenu.Instance.fadeOut(true); - document.removeEventListener("pointerdown", hideMarquee); - }; - document.addEventListener("pointerdown", hideMarquee); - - if (e.altKey) { + if (e.altKey || MarqueeView.DragMarquee) { e.preventDefault(); } } @@ -300,10 +302,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque @action delete = () => { - this.marqueeSelect(false).map(d => this.props.removeDocument(d)); - if (this.ink) { - // this.marqueeInkDelete(this.ink.inkData); - } + this.props.removeDocument(this.marqueeSelect(false)); SelectionManager.DeselectAll(); this.cleanupInteractions(false); MarqueeOptionsMenu.Instance.fadeOut(true); @@ -349,13 +348,14 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque const bounds = this.Bounds; const selected = this.marqueeSelect(false); if (e instanceof KeyboardEvent ? e.key === "c" : true) { - selected.map(d => { - this.props.removeDocument(d); + selected.map(action(d => { + //this.props.removeDocument(d); d.x = NumCast(d.x) - bounds.left - bounds.width / 2; d.y = NumCast(d.y) - bounds.top - bounds.height / 2; d.displayTimecode = undefined; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection return d; - }); + })); + this.props.removeDocument(selected); } const newCollection = this.getCollection(selected, (e as KeyboardEvent)?.key === "t" ? Docs.Create.StackingDocument : undefined); this.props.addDocument(newCollection); @@ -551,7 +551,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque marqueeSelect(selectBackgrounds: boolean = true) { const selRect = this.Bounds; const selection: Doc[] = []; - this.props.activeDocuments().filter(doc => !doc.isBackground && doc.z === undefined).map(doc => { + this.props.activeDocuments().filter(doc => !doc.isBackground && !doc.z).map(doc => { const layoutDoc = Doc.Layout(doc); const x = NumCast(doc.x); const y = NumCast(doc.y); @@ -613,7 +613,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque render() { return <div className="marqueeView" - style={{ overflow: StrCast(this.props.Document._overflow), }} + style={{ overflow: StrCast(this.props.Document._overflow), cursor: MarqueeView.DragMarquee && this ? "crosshair" : "hand" }} onScroll={(e) => e.currentTarget.scrollTop = e.currentTarget.scrollLeft = 0} onClick={this.onClick} onPointerDown={this.onPointerDown}> {this._visible ? this.marqueeDiv : null} {this.props.children} diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx index 38b16e184..c0e1a0232 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx @@ -1,10 +1,10 @@ import { action, computed } from 'mobx'; import { observer } from 'mobx-react'; import * as React from "react"; -import { Doc } from '../../../../new_fields/Doc'; -import { documentSchema } from '../../../../new_fields/documentSchemas'; -import { makeInterface } from '../../../../new_fields/Schema'; -import { BoolCast, NumCast, ScriptCast, StrCast, Cast } from '../../../../new_fields/Types'; +import { Doc } from '../../../../fields/Doc'; +import { documentSchema } from '../../../../fields/documentSchemas'; +import { makeInterface } from '../../../../fields/Schema'; +import { BoolCast, NumCast, ScriptCast, StrCast, Cast } from '../../../../fields/Types'; import { DragManager, dropActionType } from '../../../util/DragManager'; import { Transform } from '../../../util/Transform'; import { undoBatch } from '../../../util/UndoManager'; @@ -13,7 +13,7 @@ import { CollectionSubView } from '../CollectionSubView'; import "./collectionMulticolumnView.scss"; import ResizeBar from './MulticolumnResizer'; import WidthLabel from './MulticolumnWidthLabel'; -import { List } from '../../../../new_fields/List'; +import { List } from '../../../../fields/List'; import { returnZero, returnFalse, returnOne } from '../../../../Utils'; type MulticolumnDocument = makeInterface<[typeof documentSchema]>; @@ -46,12 +46,12 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu */ @computed private get ratioDefinedDocs() { - return this.childLayoutPairs.map(pair => pair.layout).filter(layout => StrCast(layout.dimUnit, "*") === DimUnit.Ratio); + return this.childLayoutPairs.map(pair => pair.layout).filter(layout => StrCast(layout._dimUnit, "*") === DimUnit.Ratio); } /** - * This loops through all childLayoutPairs and extracts the values for dimUnit - * and dimMagnitude, ignoring any that are malformed. Additionally, it then + * This loops through all childLayoutPairs and extracts the values for _dimUnit + * and _dimMagnitude, ignoring any that are malformed. Additionally, it then * normalizes the ratio values so that one * value is always 1, with the remaining * values proportionate to that easily readable metric. * @returns the list of the resolved width specifiers (unit and magnitude pairs) @@ -62,8 +62,8 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu let starSum = 0; const widthSpecifiers: WidthSpecifier[] = []; this.childLayoutPairs.map(pair => { - const unit = StrCast(pair.layout.dimUnit, "*"); - const magnitude = NumCast(pair.layout.dimMagnitude, 1); + const unit = StrCast(pair.layout._dimUnit, "*"); + const magnitude = NumCast(pair.layout._dimMagnitude, 1); if (unit && magnitude && magnitude > 0 && resolvedUnits.includes(unit)) { (unit === DimUnit.Ratio) && (starSum += magnitude); widthSpecifiers.push({ magnitude, unit }); @@ -83,9 +83,9 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu setTimeout(() => { const { ratioDefinedDocs } = this; if (this.childLayoutPairs.length) { - const minimum = Math.min(...ratioDefinedDocs.map(doc => NumCast(doc.dimMagnitude, 1))); + const minimum = Math.min(...ratioDefinedDocs.map(doc => NumCast(doc._dimMagnitude, 1))); if (minimum !== 0) { - ratioDefinedDocs.forEach(layout => layout.dimMagnitude = NumCast(layout.dimMagnitude, 1) / minimum, 1); + ratioDefinedDocs.forEach(layout => layout._dimMagnitude = NumCast(layout._dimMagnitude, 1) / minimum, 1); } } }); @@ -161,8 +161,8 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu if (columnUnitLength === undefined) { return 0; // we're still waiting on promises to resolve } - let width = NumCast(layout.dimMagnitude, 1); - if (StrCast(layout.dimUnit, "*") === DimUnit.Ratio) { + let width = NumCast(layout._dimMagnitude, 1); + if (StrCast(layout._dimUnit, "*") === DimUnit.Ratio) { width *= columnUnitLength; } return width; @@ -194,8 +194,8 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu onInternalDrop = (e: Event, de: DragManager.DropEvent) => { if (super.onInternalDrop(e, de)) { de.complete.docDragData?.droppedDocuments.forEach(action((d: Doc) => { - d.dimUnit = "*"; - d.dimMagnitude = 1; + d._dimUnit = "*"; + d._dimMagnitude = 1; })); } return false; diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx index b55b67a9e..602246d07 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx @@ -1,10 +1,10 @@ import { observer } from 'mobx-react'; -import { makeInterface } from '../../../../new_fields/Schema'; -import { documentSchema } from '../../../../new_fields/documentSchemas'; +import { makeInterface } from '../../../../fields/Schema'; +import { documentSchema } from '../../../../fields/documentSchemas'; import { CollectionSubView, SubCollectionViewProps } from '../CollectionSubView'; import * as React from "react"; -import { Doc } from '../../../../new_fields/Doc'; -import { NumCast, StrCast, BoolCast, ScriptCast } from '../../../../new_fields/Types'; +import { Doc } from '../../../../fields/Doc'; +import { NumCast, StrCast, BoolCast, ScriptCast } from '../../../../fields/Types'; import { ContentFittingDocumentView } from '../../nodes/ContentFittingDocumentView'; import { Utils, returnZero, returnFalse, returnOne } from '../../../../Utils'; import "./collectionMultirowView.scss"; @@ -14,7 +14,7 @@ import HeightLabel from './MultirowHeightLabel'; import ResizeBar from './MultirowResizer'; import { undoBatch } from '../../../util/UndoManager'; import { DragManager, dropActionType } from '../../../util/DragManager'; -import { List } from '../../../../new_fields/List'; +import { List } from '../../../../fields/List'; type MultirowDocument = makeInterface<[typeof documentSchema]>; const MultirowDocument = makeInterface(documentSchema); @@ -46,12 +46,12 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument) */ @computed private get ratioDefinedDocs() { - return this.childLayoutPairs.map(pair => pair.layout).filter(layout => StrCast(layout.dimUnit, "*") === DimUnit.Ratio); + return this.childLayoutPairs.map(pair => pair.layout).filter(layout => StrCast(layout._dimUnit, "*") === DimUnit.Ratio); } /** - * This loops through all childLayoutPairs and extracts the values for dimUnit - * and dimUnit, ignoring any that are malformed. Additionally, it then + * This loops through all childLayoutPairs and extracts the values for _dimUnit + * and _dimUnit, ignoring any that are malformed. Additionally, it then * normalizes the ratio values so that one * value is always 1, with the remaining * values proportionate to that easily readable metric. * @returns the list of the resolved width specifiers (unit and magnitude pairs) @@ -62,8 +62,8 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument) let starSum = 0; const heightSpecifiers: HeightSpecifier[] = []; this.childLayoutPairs.map(pair => { - const unit = StrCast(pair.layout.dimUnit, "*"); - const magnitude = NumCast(pair.layout.dimMagnitude, 1); + const unit = StrCast(pair.layout._dimUnit, "*"); + const magnitude = NumCast(pair.layout._dimMagnitude, 1); if (unit && magnitude && magnitude > 0 && resolvedUnits.includes(unit)) { (unit === DimUnit.Ratio) && (starSum += magnitude); heightSpecifiers.push({ magnitude, unit }); @@ -83,9 +83,9 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument) setTimeout(() => { const { ratioDefinedDocs } = this; if (this.childLayoutPairs.length) { - const minimum = Math.min(...ratioDefinedDocs.map(layout => NumCast(layout.dimMagnitude, 1))); + const minimum = Math.min(...ratioDefinedDocs.map(layout => NumCast(layout._dimMagnitude, 1))); if (minimum !== 0) { - ratioDefinedDocs.forEach(layout => layout.dimMagnitude = NumCast(layout.dimMagnitude, 1) / minimum); + ratioDefinedDocs.forEach(layout => layout._dimMagnitude = NumCast(layout._dimMagnitude, 1) / minimum); } } }); @@ -161,8 +161,8 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument) if (rowUnitLength === undefined) { return 0; // we're still waiting on promises to resolve } - let height = NumCast(layout.dimMagnitude, 1); - if (StrCast(layout.dimUnit, "*") === DimUnit.Ratio) { + let height = NumCast(layout._dimMagnitude, 1); + if (StrCast(layout._dimUnit, "*") === DimUnit.Ratio) { height *= rowUnitLength; } return height; @@ -194,8 +194,8 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument) onInternalDrop = (e: Event, de: DragManager.DropEvent) => { if (super.onInternalDrop(e, de)) { de.complete.docDragData?.droppedDocuments.forEach(action((d: Doc) => { - d.dimUnit = "*"; - d.dimMagnitude = 1; + d._dimUnit = "*"; + d._dimMagnitude = 1; })); } return false; diff --git a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx index e1e604686..734915a93 100644 --- a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx +++ b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx @@ -1,8 +1,8 @@ import * as React from "react"; import { observer } from "mobx-react"; import { observable, action } from "mobx"; -import { Doc } from "../../../../new_fields/Doc"; -import { NumCast, StrCast } from "../../../../new_fields/Types"; +import { Doc } from "../../../../fields/Doc"; +import { NumCast, StrCast } from "../../../../fields/Types"; import { DimUnit } from "./CollectionMulticolumnView"; import { UndoManager } from "../../../util/UndoManager"; @@ -43,12 +43,12 @@ export default class ResizeBar extends React.Component<ResizerProps> { const unitLength = columnUnitLength(); if (unitLength) { if (toNarrow) { - const scale = StrCast(toNarrow.dimUnit, "*") === DimUnit.Ratio ? unitLength : 1; - toNarrow.dimMagnitude = Math.max(0.05, NumCast(toNarrow.dimMagnitude, 1) - Math.abs(movementX) / scale); + const scale = StrCast(toNarrow._dimUnit, "*") === DimUnit.Ratio ? unitLength : 1; + toNarrow._dimMagnitude = Math.max(0.05, NumCast(toNarrow._dimMagnitude, 1) - Math.abs(movementX) / scale); } if (toWiden) { - const scale = StrCast(toWiden.dimUnit, "*") === DimUnit.Ratio ? unitLength : 1; - toWiden.dimMagnitude = Math.max(0.05, NumCast(toWiden.dimMagnitude, 1) + Math.abs(movementX) / scale); + const scale = StrCast(toWiden._dimUnit, "*") === DimUnit.Ratio ? unitLength : 1; + toWiden._dimMagnitude = Math.max(0.05, NumCast(toWiden._dimMagnitude, 1) + Math.abs(movementX) / scale); } } } @@ -56,17 +56,17 @@ export default class ResizeBar extends React.Component<ResizerProps> { private get isActivated() { const { toLeft, toRight } = this.props; if (toLeft && toRight) { - if (StrCast(toLeft.dimUnit, "*") === DimUnit.Pixel && StrCast(toRight.dimUnit, "*") === DimUnit.Pixel) { + if (StrCast(toLeft._dimUnit, "*") === DimUnit.Pixel && StrCast(toRight._dimUnit, "*") === DimUnit.Pixel) { return false; } return true; } else if (toLeft) { - if (StrCast(toLeft.dimUnit, "*") === DimUnit.Pixel) { + if (StrCast(toLeft._dimUnit, "*") === DimUnit.Pixel) { return false; } return true; } else if (toRight) { - if (StrCast(toRight.dimUnit, "*") === DimUnit.Pixel) { + if (StrCast(toRight._dimUnit, "*") === DimUnit.Pixel) { return false; } return true; diff --git a/src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx b/src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx index 5b2054428..9985a9fba 100644 --- a/src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx +++ b/src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx @@ -1,8 +1,8 @@ import * as React from "react"; import { observer } from "mobx-react"; import { computed } from "mobx"; -import { Doc } from "../../../../new_fields/Doc"; -import { NumCast, StrCast, BoolCast } from "../../../../new_fields/Types"; +import { Doc } from "../../../../fields/Doc"; +import { NumCast, StrCast, BoolCast } from "../../../../fields/Types"; import { EditableView } from "../../EditableView"; import { DimUnit } from "./CollectionMulticolumnView"; diff --git a/src/client/views/collections/collectionMulticolumn/MultirowHeightLabel.tsx b/src/client/views/collections/collectionMulticolumn/MultirowHeightLabel.tsx index 899577fd5..aa5439fa4 100644 --- a/src/client/views/collections/collectionMulticolumn/MultirowHeightLabel.tsx +++ b/src/client/views/collections/collectionMulticolumn/MultirowHeightLabel.tsx @@ -1,8 +1,8 @@ import * as React from "react"; import { observer } from "mobx-react"; import { computed } from "mobx"; -import { Doc } from "../../../../new_fields/Doc"; -import { NumCast, StrCast, BoolCast } from "../../../../new_fields/Types"; +import { Doc } from "../../../../fields/Doc"; +import { NumCast, StrCast, BoolCast } from "../../../../fields/Types"; import { EditableView } from "../../EditableView"; import { DimUnit } from "./CollectionMultirowView"; diff --git a/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx b/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx index 9df8cc3e2..d0bc4d01c 100644 --- a/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx +++ b/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx @@ -1,8 +1,8 @@ import * as React from "react"; import { observer } from "mobx-react"; import { observable, action } from "mobx"; -import { Doc } from "../../../../new_fields/Doc"; -import { NumCast, StrCast } from "../../../../new_fields/Types"; +import { Doc } from "../../../../fields/Doc"; +import { NumCast, StrCast } from "../../../../fields/Types"; import { DimUnit } from "./CollectionMultirowView"; import { UndoManager } from "../../../util/UndoManager"; @@ -41,12 +41,12 @@ export default class ResizeBar extends React.Component<ResizerProps> { const unitLength = columnUnitLength(); if (unitLength) { if (toNarrow) { - const scale = StrCast(toNarrow.dimUnit, "*") === DimUnit.Ratio ? unitLength : 1; - toNarrow.dimMagnitude = Math.max(0.05, NumCast(toNarrow.dimMagnitude, 1) - Math.abs(movementY) / scale); + const scale = StrCast(toNarrow._dimUnit, "*") === DimUnit.Ratio ? unitLength : 1; + toNarrow._dimMagnitude = Math.max(0.05, NumCast(toNarrow._dimMagnitude, 1) - Math.abs(movementY) / scale); } if (toWiden) { - const scale = StrCast(toWiden.dimUnit, "*") === DimUnit.Ratio ? unitLength : 1; - toWiden.dimMagnitude = Math.max(0.05, NumCast(toWiden.dimMagnitude, 1) + Math.abs(movementY) / scale); + const scale = StrCast(toWiden._dimUnit, "*") === DimUnit.Ratio ? unitLength : 1; + toWiden._dimMagnitude = Math.max(0.05, NumCast(toWiden._dimMagnitude, 1) + Math.abs(movementY) / scale); } } } @@ -54,17 +54,17 @@ export default class ResizeBar extends React.Component<ResizerProps> { private get isActivated() { const { toTop, toBottom } = this.props; if (toTop && toBottom) { - if (StrCast(toTop.dimUnit, "*") === DimUnit.Pixel && StrCast(toBottom.dimUnit, "*") === DimUnit.Pixel) { + if (StrCast(toTop._dimUnit, "*") === DimUnit.Pixel && StrCast(toBottom._dimUnit, "*") === DimUnit.Pixel) { return false; } return true; } else if (toTop) { - if (StrCast(toTop.dimUnit, "*") === DimUnit.Pixel) { + if (StrCast(toTop._dimUnit, "*") === DimUnit.Pixel) { return false; } return true; } else if (toBottom) { - if (StrCast(toBottom.dimUnit, "*") === DimUnit.Pixel) { + if (StrCast(toBottom._dimUnit, "*") === DimUnit.Pixel) { return false; } return true; diff --git a/src/client/views/linking/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx index b7f3dd995..5fb4cf3c6 100644 --- a/src/client/views/linking/LinkEditor.tsx +++ b/src/client/views/linking/LinkEditor.tsx @@ -3,8 +3,8 @@ import { faArrowLeft, faCog, faEllipsisV, faExchangeAlt, faPlus, faTable, faTime import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, observable } from "mobx"; import { observer } from "mobx-react"; -import { Doc } from "../../../new_fields/Doc"; -import { StrCast } from "../../../new_fields/Types"; +import { Doc } from "../../../fields/Doc"; +import { StrCast } from "../../../fields/Types"; import { Utils } from "../../../Utils"; import { LinkManager } from "../../util/LinkManager"; import './LinkEditor.scss'; diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx index b768eacc3..786d6be47 100644 --- a/src/client/views/linking/LinkMenu.tsx +++ b/src/client/views/linking/LinkMenu.tsx @@ -4,7 +4,7 @@ import { DocumentView } from "../nodes/DocumentView"; import { LinkEditor } from "./LinkEditor"; import './LinkMenu.scss'; import React = require("react"); -import { Doc } from "../../../new_fields/Doc"; +import { Doc } from "../../../fields/Doc"; import { LinkManager } from "../../util/LinkManager"; import { LinkMenuGroup } from "./LinkMenuGroup"; import { faTrash } from '@fortawesome/free-solid-svg-icons'; diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx index c97e1062d..89deb3a55 100644 --- a/src/client/views/linking/LinkMenuGroup.tsx +++ b/src/client/views/linking/LinkMenuGroup.tsx @@ -1,9 +1,9 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action } from "mobx"; import { observer } from "mobx-react"; -import { Doc } from "../../../new_fields/Doc"; -import { Id } from "../../../new_fields/FieldSymbols"; -import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; +import { Doc } from "../../../fields/Doc"; +import { Id } from "../../../fields/FieldSymbols"; +import { SchemaHeaderField } from "../../../fields/SchemaHeaderField"; import { Docs } from "../../documents/Documents"; import { DragManager, SetupDrag } from "../../util/DragManager"; import { LinkManager } from "../../util/LinkManager"; diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx index d4e58fa8e..17cd33241 100644 --- a/src/client/views/linking/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -3,8 +3,8 @@ import { faArrowRight, faChevronDown, faChevronUp, faEdit, faEye, faTimes } from import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, observable } from 'mobx'; import { observer } from "mobx-react"; -import { Doc, DocListCast } from '../../../new_fields/Doc'; -import { Cast, StrCast } from '../../../new_fields/Types'; +import { Doc, DocListCast } from '../../../fields/Doc'; +import { Cast, StrCast } from '../../../fields/Types'; import { DragManager } from '../../util/DragManager'; import { LinkManager } from '../../util/LinkManager'; import { ContextMenu } from '../ContextMenu'; @@ -38,7 +38,7 @@ export async function StartLinkTargetsDrag(dragEle: HTMLElement, docView: Docume } const dragData = new DragManager.DocumentDragData(moddrag.length ? moddrag : draggedDocs); - dragData.moveDocument = (doc: Doc, targetCollection: Doc | undefined, addDocument: (doc: Doc) => boolean): boolean => { + dragData.moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => { docView.props.removeDocument?.(doc); addDocument(doc); return true; diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index 1c5e13620..1a935d9b0 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -2,23 +2,23 @@ import React = require("react"); import { FieldViewProps, FieldView } from './FieldView'; import { observer } from "mobx-react"; import "./AudioBox.scss"; -import { Cast, DateCast, NumCast } from "../../../new_fields/Types"; -import { AudioField, nullAudio } from "../../../new_fields/URLField"; +import { Cast, DateCast, NumCast } from "../../../fields/Types"; +import { AudioField, nullAudio } from "../../../fields/URLField"; import { ViewBoxBaseComponent } from "../DocComponent"; -import { makeInterface, createSchema } from "../../../new_fields/Schema"; -import { documentSchema } from "../../../new_fields/documentSchemas"; +import { makeInterface, createSchema } from "../../../fields/Schema"; +import { documentSchema } from "../../../fields/documentSchemas"; import { Utils, returnTrue, emptyFunction, returnOne, returnTransparent, returnFalse, returnZero } from "../../../Utils"; import { runInAction, observable, reaction, IReactionDisposer, computed, action } from "mobx"; -import { DateField } from "../../../new_fields/DateField"; +import { DateField } from "../../../fields/DateField"; import { SelectionManager } from "../../util/SelectionManager"; -import { Doc, DocListCast } from "../../../new_fields/Doc"; +import { Doc, DocListCast } from "../../../fields/Doc"; import { ContextMenuProps } from "../ContextMenuItem"; import { ContextMenu } from "../ContextMenu"; -import { Id } from "../../../new_fields/FieldSymbols"; +import { Id } from "../../../fields/FieldSymbols"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { DocumentView } from "./DocumentView"; import { Docs, DocUtils } from "../../documents/Documents"; -import { ComputedField } from "../../../new_fields/ScriptField"; +import { ComputedField } from "../../../fields/ScriptField"; import { Networking } from "../../Network"; import { LinkAnchorBox } from "./LinkAnchorBox"; diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index cdbe506a5..5d6e587d9 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -1,14 +1,14 @@ import { computed, IReactionDisposer, observable, reaction, trace } from "mobx"; import { observer } from "mobx-react"; -import { Doc, HeightSym, WidthSym } from "../../../new_fields/Doc"; -import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; +import { Doc, HeightSym, WidthSym } from "../../../fields/Doc"; +import { Cast, NumCast, StrCast } from "../../../fields/Types"; import { Transform } from "../../util/Transform"; import { DocComponent } from "../DocComponent"; import "./CollectionFreeFormDocumentView.scss"; import { DocumentView, DocumentViewProps } from "./DocumentView"; import React = require("react"); -import { Document } from "../../../new_fields/documentSchemas"; -import { TraceMobx } from "../../../new_fields/util"; +import { Document } from "../../../fields/documentSchemas"; +import { TraceMobx } from "../../../fields/util"; import { ContentFittingDocumentView } from "./ContentFittingDocumentView"; export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx index 6e4341b27..6d53915ea 100644 --- a/src/client/views/nodes/ColorBox.tsx +++ b/src/client/views/nodes/ColorBox.tsx @@ -1,10 +1,10 @@ import React = require("react"); import { observer } from "mobx-react"; import { SketchPicker } from 'react-color'; -import { documentSchema } from "../../../new_fields/documentSchemas"; -import { makeInterface } from "../../../new_fields/Schema"; -import { StrCast } from "../../../new_fields/Types"; -import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; +import { documentSchema } from "../../../fields/documentSchemas"; +import { makeInterface } from "../../../fields/Schema"; +import { StrCast } from "../../../fields/Types"; +import { CurrentUserUtils } from "../../util/CurrentUserUtils"; import { SelectionManager } from "../../util/SelectionManager"; import { ViewBoxBaseComponent } from "../DocComponent"; import { InkingControl } from "../InkingControl"; @@ -24,9 +24,13 @@ export class ColorBox extends ViewBoxBaseComponent<FieldViewProps, ColorDocument onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()} style={{ transform: `scale(${this.props.ContentScaling()})`, width: `${100 / this.props.ContentScaling()}%`, height: `${100 / this.props.ContentScaling()}%` }} > - <SketchPicker onChange={InkingControl.Instance.switchColor} + <SketchPicker onChange={InkingControl.Instance.switchColor} presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']} color={StrCast(CurrentUserUtils.ActivePen ? CurrentUserUtils.ActivePen.backgroundColor : undefined, StrCast(selDoc?._backgroundColor, StrCast(selDoc?.backgroundColor, "black")))} /> + <div style={{ display: "grid", gridTemplateColumns: "20% 80%", paddingTop: "10px" }}> + <div>{InkingControl.Instance.selectedWidth ?? 2}</div> + <input type="range" value={InkingControl.Instance.selectedWidth ?? 2} defaultValue={2} min={1} max={100} onChange={(e: React.ChangeEvent<HTMLInputElement>) => InkingControl.Instance.switchWidth(e.target.value)} /> + </div> </div>; } }
\ No newline at end of file diff --git a/src/client/views/nodes/ContentFittingDocumentView.tsx b/src/client/views/nodes/ContentFittingDocumentView.tsx index 7e45e8fcf..39097865a 100644 --- a/src/client/views/nodes/ContentFittingDocumentView.tsx +++ b/src/client/views/nodes/ContentFittingDocumentView.tsx @@ -2,9 +2,9 @@ import React = require("react"); import { computed } from "mobx"; import { observer } from "mobx-react"; import "react-table/react-table.css"; -import { Doc, Opt, WidthSym, HeightSym } from "../../../new_fields/Doc"; -import { NumCast, StrCast, Cast } from "../../../new_fields/Types"; -import { TraceMobx } from "../../../new_fields/util"; +import { Doc, Opt, WidthSym, HeightSym } from "../../../fields/Doc"; +import { NumCast, StrCast, Cast } from "../../../fields/Types"; +import { TraceMobx } from "../../../fields/util"; import { emptyFunction, returnOne } from "../../../Utils"; import '../DocumentDecorations.scss'; import { DocumentView, DocumentViewProps } from "../nodes/DocumentView"; diff --git a/src/client/views/nodes/DocumentBox.scss b/src/client/views/nodes/DocHolderBox.scss index 3a27c16c1..6a9ef0b6f 100644 --- a/src/client/views/nodes/DocumentBox.scss +++ b/src/client/views/nodes/DocHolderBox.scss @@ -2,12 +2,12 @@ width: 100%; height: 100%; pointer-events: all; - background: rgb(241, 239, 235); position: absolute; .documentBox-lock { margin: auto; color: white; position: absolute; + padding: 3px; } .contentFittingDocumentView { position: absolute; diff --git a/src/client/views/nodes/DocumentBox.tsx b/src/client/views/nodes/DocHolderBox.tsx index b53c7cfe6..0c5239d66 100644 --- a/src/client/views/nodes/DocumentBox.tsx +++ b/src/client/views/nodes/DocHolderBox.tsx @@ -1,12 +1,12 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, IReactionDisposer, reaction } from "mobx"; import { observer } from "mobx-react"; -import { Doc, Field } from "../../../new_fields/Doc"; -import { collectionSchema, documentSchema } from "../../../new_fields/documentSchemas"; -import { makeInterface } from "../../../new_fields/Schema"; -import { ComputedField } from "../../../new_fields/ScriptField"; -import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; -import { TraceMobx } from "../../../new_fields/util"; +import { Doc, Field } from "../../../fields/Doc"; +import { collectionSchema, documentSchema } from "../../../fields/documentSchemas"; +import { makeInterface, listSpec } from "../../../fields/Schema"; +import { ComputedField } from "../../../fields/ScriptField"; +import { Cast, NumCast, StrCast } from "../../../fields/Types"; +import { TraceMobx } from "../../../fields/util"; import { emptyPath, returnFalse, returnOne, returnZero } from "../../../Utils"; import { DocumentType } from "../../documents/DocumentTypes"; import { DragManager } from "../../util/DragManager"; @@ -15,7 +15,7 @@ import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; import { ViewBoxAnnotatableComponent } from "../DocComponent"; import { ContentFittingDocumentView } from "./ContentFittingDocumentView"; -import "./DocumentBox.scss"; +import "./DocHolderBox.scss"; import { DocumentView } from "./DocumentView"; import { FieldView, FieldViewProps } from "./FieldView"; import React = require("react"); @@ -32,7 +32,7 @@ export class DocHolderBox extends ViewBoxAnnotatableComponent<FieldViewProps, Do _contRef = React.createRef<HTMLDivElement>(); _curSelection = -1; componentDidMount() { - this._prevSelectionDisposer = reaction(() => this.layoutDoc[this.props.fieldKey], (data) => { + this._prevSelectionDisposer = reaction(() => this.dataDoc[this.fieldKey], (data) => { if (data instanceof Doc && !this.isSelectionLocked()) { this._selections.indexOf(data) !== -1 && this._selections.splice(this._selections.indexOf(data), 1); this._selections.push(data); @@ -53,13 +53,13 @@ export class DocHolderBox extends ViewBoxAnnotatableComponent<FieldViewProps, Do ContextMenu.Instance.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); } lockSelection = () => { - this.layoutDoc[this.props.fieldKey] = this.layoutDoc[this.props.fieldKey]; + this.dataDoc[this.fieldKey] = this.dataDoc[this.fieldKey]; } showSelection = () => { - this.layoutDoc[this.props.fieldKey] = ComputedField.MakeFunction(`selectedDocs(self,this.excludeCollections,[_last_])?.[0]`); + this.dataDoc[this.fieldKey] = ComputedField.MakeFunction(`selectedDocs(self,this.excludeCollections,[_last_])?.[0]`); } isSelectionLocked = () => { - const kvpstring = Field.toKeyValueString(this.layoutDoc, this.props.fieldKey); + const kvpstring = Field.toKeyValueString(this.dataDoc, this.fieldKey); return !kvpstring || kvpstring.includes("DOC"); } toggleLockSelection = () => { @@ -69,13 +69,13 @@ export class DocHolderBox extends ViewBoxAnnotatableComponent<FieldViewProps, Do prevSelection = () => { this.lockSelection(); if (this._curSelection > 0) { - this.layoutDoc[this.props.fieldKey] = this._selections[--this._curSelection]; + this.dataDoc[this.fieldKey] = this._selections[--this._curSelection]; return true; } } nextSelection = () => { if (this._curSelection < this._selections.length - 1 && this._selections.length) { - this.layoutDoc[this.props.fieldKey] = this._selections[++this._curSelection]; + this.dataDoc[this.fieldKey] = this._selections[++this._curSelection]; return true; } } @@ -89,8 +89,8 @@ export class DocHolderBox extends ViewBoxAnnotatableComponent<FieldViewProps, Do (e.nativeEvent as any).formattedHandled = true; e.stopPropagation(); } - get xPad() { return NumCast(this.props.Document._xPadding); } - get yPad() { return NumCast(this.props.Document._yPadding); } + get xPad() { return NumCast(this.rootDoc._xPadding); } + get yPad() { return NumCast(this.rootDoc._yPadding); } onClick = (e: React.MouseEvent) => { let hitWidget: boolean | undefined = false; if (this._contRef.current!.getBoundingClientRect().top + this.yPad > e.clientY) hitWidget = (() => { this.props.select(false); return true; })(); @@ -107,83 +107,89 @@ export class DocHolderBox extends ViewBoxAnnotatableComponent<FieldViewProps, Do pwidth = () => this.props.PanelWidth() - 2 * this.xPad; pheight = () => this.props.PanelHeight() - 2 * this.yPad; getTransform = () => this.props.ScreenToLocalTransform().translate(-this.xPad, -this.yPad); - isActive = () => this.active() || !this.props.renderDepth; - layoutTemplateDoc = () => Cast(this.props.Document.childLayoutTemplate, Doc, null); + isActive = (outsideReaction: boolean) => this.active(outsideReaction) || this.props.renderDepth <= 1; + layoutTemplateDoc = () => Cast(this.layoutDoc.childLayoutTemplate, Doc, null); get renderContents() { - const containedDoc = Cast(this.dataDoc[this.props.fieldKey], Doc, null); + const containedDoc = Cast(this.dataDoc[this.fieldKey], Doc, null); const layoutTemplate = StrCast(this.layoutDoc.childLayoutString); - const contents = !(containedDoc instanceof Doc) ? (null) : this.layoutDoc.childLayoutString || this.layoutTemplateDoc() ? - <DocumentView - Document={containedDoc} - DataDoc={undefined} - LibraryPath={emptyPath} - ContainingCollectionView={this as any} // bcz: hack! need to pass a prop that can be used to select the container (ie, 'this') when the up selector in document decorations is clicked. currently, the up selector allows only a containing collection to be selected - ContainingCollectionDoc={undefined} - fitToBox={true} - LayoutTemplateString={layoutTemplate} - LayoutTemplate={this.layoutTemplateDoc} - rootSelected={this.props.isSelected} - addDocument={this.props.addDocument} - moveDocument={this.props.moveDocument} - removeDocument={this.props.removeDocument} - addDocTab={this.props.addDocTab} - pinToPres={this.props.pinToPres} - ScreenToLocalTransform={this.getTransform} - renderDepth={this.props.renderDepth + 1} - NativeHeight={returnZero} - NativeWidth={returnZero} - PanelWidth={this.pwidth} - PanelHeight={this.pheight} - focus={this.props.focus} - parentActive={this.isActive} - dontRegisterView={true} - whenActiveChanged={this.props.whenActiveChanged} - bringToFront={returnFalse} - ContentScaling={returnOne} /> : - <ContentFittingDocumentView - Document={containedDoc} - DataDoc={undefined} - LibraryPath={emptyPath} - ContainingCollectionView={this as any} // bcz: hack! need to pass a prop that can be used to select the container (ie, 'this') when the up selector in document decorations is clicked. currently, the up selector allows only a containing collection to be selected - ContainingCollectionDoc={undefined} - fitToBox={true} - LayoutTemplateString={layoutTemplate} - LayoutTemplate={this.layoutTemplateDoc} - rootSelected={this.props.isSelected} - addDocument={this.props.addDocument} - moveDocument={this.props.moveDocument} - removeDocument={this.props.removeDocument} - addDocTab={this.props.addDocTab} - pinToPres={this.props.pinToPres} - ScreenToLocalTransform={this.getTransform} - renderDepth={this.props.renderDepth + 1} - NativeHeight={returnZero} - NativeWidth={returnZero} - PanelWidth={this.pwidth} - PanelHeight={this.pheight} - focus={this.props.focus} - parentActive={this.isActive} - dontRegisterView={true} - whenActiveChanged={this.props.whenActiveChanged} - bringToFront={returnFalse} - ContentScaling={returnOne} - />; + const contents = !(containedDoc instanceof Doc) || + Cast(containedDoc[Doc.LayoutFieldKey(containedDoc)], listSpec(Doc), null)?.includes(this.rootDoc) + ? (null) : this.layoutDoc.childLayoutString || this.layoutTemplateDoc() ? + <DocumentView + Document={containedDoc} + DataDoc={undefined} + LibraryPath={emptyPath} + ContainingCollectionView={this as any} // bcz: hack! need to pass a prop that can be used to select the container (ie, 'this') when the up selector in document decorations is clicked. currently, the up selector allows only a containing collection to be selected + ContainingCollectionDoc={undefined} + fitToBox={true} + backgroundColor={this.props.backgroundColor} + LayoutTemplateString={layoutTemplate} + LayoutTemplate={this.layoutTemplateDoc} + rootSelected={this.props.isSelected} + addDocument={this.props.addDocument} + moveDocument={this.props.moveDocument} + removeDocument={this.props.removeDocument} + addDocTab={this.props.addDocTab} + pinToPres={this.props.pinToPres} + ScreenToLocalTransform={this.getTransform} + renderDepth={containedDoc.type !== DocumentType.DOCHOLDER && !this.props.renderDepth ? 0 : this.props.renderDepth + 1} + NativeHeight={returnZero} + NativeWidth={returnZero} + PanelWidth={this.pwidth} + PanelHeight={this.pheight} + focus={this.props.focus} + parentActive={this.isActive} + dontRegisterView={true} + whenActiveChanged={this.props.whenActiveChanged} + bringToFront={returnFalse} + ContentScaling={returnOne} /> : + <ContentFittingDocumentView + Document={containedDoc} + DataDoc={undefined} + LibraryPath={emptyPath} + ContainingCollectionView={this as any} // bcz: hack! need to pass a prop that can be used to select the container (ie, 'this') when the up selector in document decorations is clicked. currently, the up selector allows only a containing collection to be selected + ContainingCollectionDoc={undefined} + fitToBox={true} + backgroundColor={this.props.backgroundColor} + ignoreAutoHeight={true} + LayoutTemplateString={layoutTemplate} + LayoutTemplate={this.layoutTemplateDoc} + rootSelected={this.props.isSelected} + addDocument={this.props.addDocument} + moveDocument={this.props.moveDocument} + removeDocument={this.props.removeDocument} + addDocTab={this.props.addDocTab} + pinToPres={this.props.pinToPres} + ScreenToLocalTransform={this.getTransform} + renderDepth={containedDoc.type !== DocumentType.DOCHOLDER && !this.props.renderDepth ? 0 : this.props.renderDepth + 1} + NativeHeight={returnZero} + NativeWidth={returnZero} + PanelWidth={this.pwidth} + PanelHeight={this.pheight} + focus={this.props.focus} + parentActive={this.isActive} + dontRegisterView={true} + whenActiveChanged={this.props.whenActiveChanged} + bringToFront={returnFalse} + ContentScaling={returnOne} + />; return contents; } render() { + const containedDoc = Cast(this.dataDoc[this.fieldKey], Doc, null); TraceMobx(); return <div className="documentBox-container" ref={this._contRef} onContextMenu={this.specificContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick} style={{ - background: StrCast(this.layoutDoc.backgroundColor), + background: this.props.backgroundColor?.(containedDoc), border: `#00000021 solid ${this.xPad}px`, borderTop: `#0000005e solid ${this.yPad}px`, borderBottom: `#0000005e solid ${this.yPad}px`, }}> {this.renderContents} <div className="documentBox-lock" onClick={this.onLockClick} ref={this.createDropTarget} - style={{ marginTop: - this.yPad }}> + style={{ marginTop: - this.yPad, background: "black" }}> <FontAwesomeIcon icon={this.isSelectionLocked() ? "lock" : "unlock"} size="sm" /> </div> </div >; @@ -195,13 +201,13 @@ export class DocHolderBox extends ViewBoxAnnotatableComponent<FieldViewProps, Do if (de.complete.docDragData) { if (de.complete.docDragData.draggedDocuments[0].type === DocumentType.FONTICON) { const doc = Cast(de.complete.docDragData.draggedDocuments[0].dragFactory, Doc, null); - this.props.Document.childLayoutTemplate = doc; + this.layoutDoc.childLayoutTemplate = doc; } } } protected createDropTarget = (ele: HTMLDivElement) => { this._dropDisposer?.(); - ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.props.Document)); + ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.rootDoc)); } } diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 81667e549..f4785bb0c 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -1,7 +1,7 @@ import { computed } from "mobx"; import { observer } from "mobx-react"; -import { Doc, Opt, Field } from "../../../new_fields/Doc"; -import { Cast, StrCast, NumCast } from "../../../new_fields/Types"; +import { Doc, Opt, Field } from "../../../fields/Doc"; +import { Cast, StrCast, NumCast } from "../../../fields/Types"; import { OmitKeys, Without, emptyPath } from "../../../Utils"; import DirectoryImportBox from "../../util/Import & Export/DirectoryImportBox"; import { CollectionDockingView } from "../collections/CollectionDockingView"; @@ -14,7 +14,7 @@ import { LabelBox } from "./LabelBox"; import { SliderBox } from "./SliderBox"; import { LinkBox } from "./LinkBox"; import { ScriptingBox } from "./ScriptingBox"; -import { DocHolderBox } from "./DocumentBox"; +import { DocHolderBox } from "./DocHolderBox"; import { DocumentViewProps } from "./DocumentView"; import "./DocumentView.scss"; import { FontIconBox } from "./FontIconBox"; @@ -35,8 +35,8 @@ import { WebBox } from "./WebBox"; import { InkingStroke } from "../InkingStroke"; import React = require("react"); import { RecommendationsBox } from "../RecommendationsBox"; -import { TraceMobx } from "../../../new_fields/util"; -import { ScriptField } from "../../../new_fields/ScriptField"; +import { TraceMobx } from "../../../fields/util"; +import { ScriptField } from "../../../fields/ScriptField"; import XRegExp = require("xregexp"); const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? @@ -135,7 +135,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { CreateBindings(onClick: Opt<ScriptField>, onInput: Opt<ScriptField>): JsxBindings { const list = { - ...OmitKeys(this.props, ['parentActive'], (obj: any) => obj.active = this.props.parentActive).omit, + ...OmitKeys(this.props, ['parentActive'], "", (obj: any) => obj.active = this.props.parentActive).omit, RootDoc: Cast(this.layoutDoc?.rootDocument, Doc, null) || this.layoutDoc, Document: this.layoutDoc, DataDoc: this.dataDoc, diff --git a/src/client/views/nodes/DocumentIcon.tsx b/src/client/views/nodes/DocumentIcon.tsx index f56f5e829..fb54f18e8 100644 --- a/src/client/views/nodes/DocumentIcon.tsx +++ b/src/client/views/nodes/DocumentIcon.tsx @@ -3,7 +3,7 @@ import * as React from "react"; import { DocumentView } from "./DocumentView"; import { DocumentManager } from "../../util/DocumentManager"; import { Transformer, Scripting, ts } from "../../util/Scripting"; -import { Field } from "../../../new_fields/Doc"; +import { Field } from "../../../fields/Doc"; @observer export class DocumentIcon extends React.Component<{ view: DocumentView, index: number }> { diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index c4cd5978a..340fa06a8 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -4,18 +4,18 @@ 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 } from "../../../new_fields/Doc"; -import { Document } from '../../../new_fields/documentSchemas'; -import { Id } from '../../../new_fields/FieldSymbols'; -import { InkTool } from '../../../new_fields/InkField'; -import { listSpec } from "../../../new_fields/Schema"; -import { SchemaHeaderField } from '../../../new_fields/SchemaHeaderField'; -import { ScriptField } from '../../../new_fields/ScriptField'; -import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types"; -import { ImageField } from '../../../new_fields/URLField'; -import { TraceMobx } from '../../../new_fields/util'; +import { Doc, DocListCast, HeightSym, Opt, WidthSym, DataSym } 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 { ImageField } from '../../../fields/URLField'; +import { TraceMobx } from '../../../fields/util'; import { GestureUtils } from '../../../pen-gestures/GestureUtils'; -import { emptyFunction, OmitKeys, returnOne, returnTransparent, Utils } from "../../../Utils"; +import { emptyFunction, OmitKeys, returnOne, returnTransparent, Utils, emptyPath } from "../../../Utils"; import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; import { ClientRecommender } from '../../ClientRecommender'; import { DocServer } from "../../DocServer"; @@ -23,6 +23,7 @@ import { Docs, DocUtils } from "../../documents/Documents"; import { DocumentType } from '../../documents/DocumentTypes'; import { ClientUtils } from '../../util/ClientUtils'; import { DocumentManager } from "../../util/DocumentManager"; +import { SnappingManager } from '../../util/SnappingManager'; import { DragManager, dropActionType } from "../../util/DragManager"; import { InteractionUtils } from '../../util/InteractionUtils'; import { Scripting } from '../../util/Scripting'; @@ -62,18 +63,20 @@ export interface DocumentViewProps { LayoutTemplate?: () => Opt<Doc>; LibraryPath: Doc[]; fitToBox?: boolean; + 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; + treeViewId?: string; dropAction?: dropActionType; dragDivName?: string; nudge?: (x: number, y: number) => void; - addDocument?: (doc: Doc) => boolean; - removeDocument?: (doc: Doc) => boolean; - moveDocument?: (doc: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean) => boolean; + addDocument?: (doc: Doc | Doc[]) => boolean; + removeDocument?: (doc: Doc | Doc[]) => boolean; + moveDocument?: (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean; ScreenToLocalTransform: () => Transform; setupDragLines?: () => void; renderDepth: number; @@ -241,6 +244,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu dragData.removeDocument = this.props.removeDocument; dragData.moveDocument = this.props.moveDocument;// this.Document.onDragStart ? undefined : this.props.moveDocument; dragData.dragDivName = this.props.dragDivName; + dragData.treeViewId = this.props.treeViewId; DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { hideSource: !dropAction && !this.Document.onDragStart }); } } @@ -301,11 +305,15 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu }, console.log); func(); } else { - const fullScreenAlias = Doc.MakeAlias(this.props.Document); - if (StrCast(fullScreenAlias.layoutKey) !== "layout_fullScreen" && fullScreenAlias.layout_fullScreen) { - fullScreenAlias.layoutKey = "layout_fullScreen"; - } - UndoManager.RunInBatch(() => this.props.addDocTab(fullScreenAlias, "inTab"), "double tap"); + UndoManager.RunInBatch(() => { + if (StrCast(this.props.Document.layoutKey) !== "layout_fullScreen" && this.props.Document.layout_fullScreen) { + const fullScreenAlias = Doc.MakeAlias(this.props.Document); + fullScreenAlias.layoutKey = "layout_fullScreen"; + this.props.addDocTab(fullScreenAlias, "inTab"); + } else { + this.props.addDocTab(this.props.Document, "inTab"); + } + }, "double tap"); SelectionManager.DeselectAll(); Doc.UnBrushDoc(this.props.Document); } @@ -321,7 +329,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu UndoManager.RunInBatch(func, "on click"); } else func(); } else if (this.Document["onClick-rawScript"] && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes("ScriptingBox")) {// bcz: hack? don't edit a script if you're clicking on a scripting box itself - UndoManager.RunInBatch(() => Doc.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick"); + const alias = Doc.MakeAlias(this.props.Document); + Doc.makeCustomViewClicked(alias, undefined, "onClick"); + this.props.addDocTab(alias, "onRight"); + // UndoManager.RunInBatch(() => Doc.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick"); //ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", e.clientX, e.clientY), "on button click"); } else if (this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) { DocListCast(this.props.Document.links).length && this.followLinkClick(e.altKey, e.ctrlKey, e.shiftKey); @@ -514,7 +525,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu if ((!e.nativeEvent.cancelBubble || this.onClickHandler || this.Document.onDragStart) && // if this is part of a template, let the event go up to the tempalte root unless right/ctrl clicking !((this.props.Document.rootDocument) && !(e.ctrlKey || e.button > 0))) { - if ((this.active || this.Document.onDragStart || this.onClickHandler) && + if ((this.active || this.Document.onDragStart) && !e.ctrlKey && (e.button === 0 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) && !this.Document.inOverlay) { @@ -537,12 +548,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu if (e.cancelBubble && this.active) { document.removeEventListener("pointermove", this.onPointerMove); // stop listening to pointerMove if something else has stopPropagated it (e.g., the MarqueeView) } - else if (!e.cancelBubble && (SelectionManager.IsSelected(this, true) || this.props.parentActive(true) || this.Document.onDragStart || this.onClickHandler) && !this.Document.lockedPosition && !this.Document.inOverlay) { + else if (!e.cancelBubble && (SelectionManager.IsSelected(this, true) || this.props.parentActive(true) || this.Document.onDragStart) && !this.Document.lockedPosition && !this.Document.inOverlay) { if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) { if (!e.altKey && (!this.topMost || this.Document.onDragStart || this.onClickHandler) && (e.buttons === 1 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE))) { document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); - this.startDragging(this._downX, this._downY, this.props.dropAction ? this.props.dropAction : this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined); + this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && "alias") || (this.props.dropAction || this.Document.dropAction || undefined) as dropActionType); } } e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers @@ -637,18 +648,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @undoBatch @action - public static unfreezeNativeDimensions(layoutDoc: Doc) { - layoutDoc._nativeWidth = undefined; - layoutDoc._nativeHeight = undefined; - } - toggleNativeDimensions = () => { - if (this.Document._nativeWidth || this.Document._nativeHeight) { - DocumentView.unfreezeNativeDimensions(this.layoutDoc); - } - else { - Doc.freezeNativeDimensions(this.layoutDoc, this.props.PanelWidth(), this.props.PanelHeight()); - } + Doc.toggleNativeDimensions(this.layoutDoc, this.props.ContentScaling(), this.props.PanelWidth(), this.props.PanelHeight()); } @undoBatch @@ -668,7 +669,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu toggleBackground = (temporary: boolean): void => { this.Document._overflow = temporary ? "visible" : "hidden"; this.Document.isBackground = !temporary ? !this.Document.isBackground : (this.Document.isBackground ? undefined : true); - this.Document.isBackground && this.props.bringToFront(this.Document, true); + if (this.Document.isBackground) { + this.props.bringToFront(this.props.Document, true); + this.props.Document[DataSym][Doc.LayoutFieldKey(this.Document) + "-nativeWidth"] = this.Document[WidthSym](); + this.props.Document[DataSym][Doc.LayoutFieldKey(this.Document) + "-nativeHeight"] = this.Document[HeightSym](); + } } @undoBatch @@ -703,7 +708,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } const cm = ContextMenu.Instance; - const templateDoc = Cast(this.props.Document[StrCast(this.props.Document.layoutKey)], Doc, null); const customScripts = Cast(this.props.Document.contextMenuScripts, listSpec(ScriptField), []); Cast(this.props.Document.contextMenuLabels, listSpec("string"), []).forEach((label, i) => @@ -712,28 +716,16 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu cm.addItem({ description: item.label, event: () => item.script.script.run({ this: this.layoutDoc, self: this.rootDoc }), icon: "sticky-note" })); - let open = cm.findByDescription("Add a Perspective..."); - const openItems: ContextMenuProps[] = open && "subitems" in open ? open.subitems : []; - openItems.push({ description: "Open Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "onRight"), icon: "layer-group" }); - templateDoc && openItems.push({ description: "Open Template ", event: () => this.props.addDocTab(templateDoc, "onRight"), icon: "eye" }); - if (!open) { - open = { description: "Add a Perspective....", subitems: openItems, icon: "external-link-alt" }; - cm.addItem(open); - } - let options = cm.findByDescription("Options..."); const optionItems: ContextMenuProps[] = options && "subitems" in options ? options.subitems : []; - optionItems.push({ description: `${this.Document._chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.Document._chromeStatus = (this.Document._chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" }); - optionItems.push({ description: `${this.Document._autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight, icon: "plus" }); - optionItems.push({ description: this.Document.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.Document.lockedPosition) ? "unlock" : "lock" }); - optionItems.push({ description: this.Document.lockedTransform ? "Unlock Transform" : "Lock Transform", event: this.toggleLockTransform, icon: BoolCast(this.Document.lockedTransform) ? "unlock" : "lock" }); + const templateDoc = Cast(this.props.Document[StrCast(this.props.Document.layoutKey)], Doc, null); + optionItems.push({ description: "Open Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "onRight"), icon: "layer-group" }); + templateDoc && optionItems.push({ description: "Open Template ", event: () => this.props.addDocTab(templateDoc, "onRight"), icon: "eye" }); if (!options) { options = { description: "Options...", subitems: optionItems, icon: "compass" }; cm.addItem(options); } - cm.moveAfter(options, open); - 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" }); @@ -745,6 +737,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu onClicks.push({ description: "Edit onClick Script", event: () => UndoManager.RunInBatch(() => Doc.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick"), icon: "edit" }); !existingOnClick && cm.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" }); + const funcs: ContextMenuProps[] = []; if (this.Document.onDragStart) { funcs.push({ description: "Drag an Alias", icon: "edit", event: () => this.Document.dragFactory && (this.Document.onDragStart = ScriptField.MakeFunction('getAlias(this.dragFactory)')) }); @@ -756,16 +749,15 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const more = cm.findByDescription("More..."); const moreItems: ContextMenuProps[] = more && "subitems" in more ? more.subitems : []; moreItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc), icon: "concierge-bell" }); - moreItems.push({ description: !this.Document._nativeWidth || !this.Document._nativeHeight ? "Freeze" : "Unfreeze", event: this.toggleNativeDimensions, icon: "snowflake" }); + moreItems.push({ description: `${this.Document._chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.Document._chromeStatus = (this.Document._chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" }); + moreItems.push({ description: this.Document.lockedTransform ? "Unlock Transform" : "Lock Transform", event: this.toggleLockTransform, icon: BoolCast(this.Document.lockedTransform) ? "unlock" : "lock" }); + moreItems.push({ description: this.Document.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.Document.lockedPosition) ? "unlock" : "lock" }); if (!ClientUtils.RELEASE) { // let copies: ContextMenuProps[] = []; moreItems.push({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]), icon: "fingerprint" }); // cm.addItem({ description: "Copy...", subitems: copies, icon: "copy" }); } - if (Cast(this.props.Document.data, ImageField)) { - moreItems.push({ description: "Export to Google Photos", event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: "caret-square-right" }); - } if (Cast(Doc.GetProto(this.props.Document).data, listSpec(Doc))) { moreItems.push({ description: "Export to Google Photos Album", event: () => GooglePhotos.Export.CollectionToAlbum({ collection: this.props.Document }).then(console.log), icon: "caret-square-right" }); moreItems.push({ description: "Tag Child Images via Google Photos", event: () => GooglePhotos.Query.TagChildImages(this.props.Document), icon: "caret-square-right" }); @@ -782,6 +774,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu // a.download = `DocExport-${this.props.Document[Id]}.zip`; // a.click(); }); + const recommender_subitems: ContextMenuProps[] = []; recommender_subitems.push({ @@ -814,53 +807,52 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu moreItems.push({ description: "Publish", event: () => DocUtils.Publish(this.props.Document, this.Document.title || "", this.props.addDocument, this.props.removeDocument), icon: "file" }); moreItems.push({ description: "Undo Debug Test", event: () => UndoManager.TraceOpenBatches(), icon: "exclamation" }); !more && cm.addItem({ description: "More...", subitems: moreItems, icon: "hand-point-right" }); + + cm.moveAfter(cm.findByDescription("More...")!, cm.findByDescription("OnClick...")!); + runInAction(() => { - if (!ClientUtils.RELEASE) { - const setWriteMode = (mode: DocServer.WriteMode) => { - DocServer.AclsMode = mode; - const mode1 = mode; - const mode2 = mode === DocServer.WriteMode.Default ? mode : DocServer.WriteMode.Playground; - DocServer.setFieldWriteMode("x", mode1); - DocServer.setFieldWriteMode("y", mode1); - DocServer.setFieldWriteMode("_width", mode1); - DocServer.setFieldWriteMode("_height", mode1); - - DocServer.setFieldWriteMode("_panX", mode2); - DocServer.setFieldWriteMode("_panY", mode2); - DocServer.setFieldWriteMode("scale", mode2); - DocServer.setFieldWriteMode("_viewType", mode2); - }; - const aclsMenu: ContextMenuProps[] = []; - aclsMenu.push({ description: "Default (write/read all)", event: () => setWriteMode(DocServer.WriteMode.Default), icon: DocServer.AclsMode === DocServer.WriteMode.Default ? "check" : "exclamation" }); - aclsMenu.push({ description: "Playground (write own/no read)", event: () => setWriteMode(DocServer.WriteMode.Playground), icon: DocServer.AclsMode === DocServer.WriteMode.Playground ? "check" : "exclamation" }); - aclsMenu.push({ description: "Live Playground (write own/read others)", event: () => setWriteMode(DocServer.WriteMode.LivePlayground), icon: DocServer.AclsMode === DocServer.WriteMode.LivePlayground ? "check" : "exclamation" }); - aclsMenu.push({ description: "Live Readonly (no write/read others)", event: () => setWriteMode(DocServer.WriteMode.LiveReadonly), icon: DocServer.AclsMode === DocServer.WriteMode.LiveReadonly ? "check" : "exclamation" }); - cm.addItem({ description: "Collaboration ACLs...", subitems: aclsMenu, icon: "share" }); - } + const setWriteMode = (mode: DocServer.WriteMode) => { + DocServer.AclsMode = mode; + const mode1 = mode; + const mode2 = mode === DocServer.WriteMode.Default ? mode : DocServer.WriteMode.Playground; + DocServer.setFieldWriteMode("x", mode1); + DocServer.setFieldWriteMode("y", mode1); + DocServer.setFieldWriteMode("_width", mode1); + DocServer.setFieldWriteMode("_height", mode1); + + DocServer.setFieldWriteMode("_panX", mode2); + DocServer.setFieldWriteMode("_panY", mode2); + DocServer.setFieldWriteMode("scale", mode2); + DocServer.setFieldWriteMode("_viewType", mode2); + }; + const aclsMenu: ContextMenuProps[] = []; + aclsMenu.push({ description: "Share", event: () => SharingManager.Instance.open(this), icon: "external-link-alt" }); + aclsMenu.push({ description: "Default (write/read all)", event: () => setWriteMode(DocServer.WriteMode.Default), icon: DocServer.AclsMode === DocServer.WriteMode.Default ? "check" : "exclamation" }); + aclsMenu.push({ description: "Playground (write own/no read)", event: () => setWriteMode(DocServer.WriteMode.Playground), icon: DocServer.AclsMode === DocServer.WriteMode.Playground ? "check" : "exclamation" }); + aclsMenu.push({ description: "Live Playground (write own/read others)", event: () => setWriteMode(DocServer.WriteMode.LivePlayground), icon: DocServer.AclsMode === DocServer.WriteMode.LivePlayground ? "check" : "exclamation" }); + aclsMenu.push({ description: "Live Readonly (no write/read others)", event: () => setWriteMode(DocServer.WriteMode.LiveReadonly), icon: DocServer.AclsMode === DocServer.WriteMode.LiveReadonly ? "check" : "exclamation" }); + cm.addItem({ description: "Collaboration ...", subitems: aclsMenu, icon: "share" }); }); runInAction(() => { - cm.addItem({ - description: "Share", - event: () => SharingManager.Instance.open(this), - icon: "external-link-alt" - }); - if (!this.topMost && !(e instanceof Touch)) { // DocumentViews should stop propagation of this event e.stopPropagation(); } ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); - if (!SelectionManager.IsSelected(this, true)) { - SelectionManager.SelectDoc(this, false); - } + !SelectionManager.IsSelected(this, true) && SelectionManager.SelectDoc(this, false); }); const path = this.props.LibraryPath.reduce((p: string, d: Doc) => p + "/" + (Doc.AreProtosEqual(d, (Doc.UserDoc()["tabs-button-library"] as Doc).sourcePanel as Doc) ? "" : d.title), ""); - cm.addItem({ + const item = ({ description: `path: ${path}`, event: () => { - this.props.LibraryPath.map(lp => Doc.GetProto(lp).treeViewOpen = lp.treeViewOpen = true); - Doc.linkFollowHighlight(this.props.Document); + if (this.props.LibraryPath !== emptyPath) { + this.props.LibraryPath.map(lp => Doc.GetProto(lp).treeViewOpen = lp.treeViewOpen = true); + Doc.linkFollowHighlight(this.props.Document); + } else { + Doc.AddDocToList(Doc.GetProto(Doc.UserDoc().myCatalog as Doc), "data", this.props.Document[DataSym]); + } }, icon: "check" }); + //cm.addItem(item); } recommender = async () => { @@ -1005,6 +997,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu renderDepth={this.props.renderDepth} PanelWidth={this.panelWidth} PanelHeight={this.panelHeight} + ignoreAutoHeight={this.props.ignoreAutoHeight} focus={this.props.focus} parentActive={this.props.parentActive} whenActiveChanged={this.props.whenActiveChanged} @@ -1037,7 +1030,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu makeLink = () => this._link; // pass the link placeholde to child views so they can react to make a specialized anchor. This is essentially a function call to the descendants since the value of the _link variable will immediately get set back to undefined. @undoBatch - hideLinkAnchor = (doc: Doc) => doc.hidden = true + 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() { @@ -1073,8 +1066,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu <div className="documentView-captionWrapper" style={{ backgroundColor: StrCast(this.layoutDoc["caption-backgroundColor"]), color: StrCast(this.layoutDoc["caption-color"]) }}> <DocumentContentsView {...OmitKeys(this.props, ['children']).omit} hideOnLeave={true} - layoutTemplateString={`<FormattedTextBox {...props} fieldKey={'${showCaption}'}/>`} - ContentScaling={this.childScaling} + LayoutTemplateString={`<FormattedTextBox {...props} fieldKey={'${showCaption}'}/>`} + ContentScaling={returnOne} ChromeHeight={this.chromeHeight} isSelected={this.isSelected} select={this.select} @@ -1102,7 +1095,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } @computed get ignorePointerEvents() { return this.props.pointerEvents === false || - (this.Document.isBackground && !this.isSelected() && !DragManager.Vals.Instance.GetIsDragging()) || + (this.Document.isBackground && !this.isSelected() && !SnappingManager.GetIsDragging()) || (this.Document.type === DocumentType.INK && InkingControl.Instance.selectedTool !== InkTool.None); } @undoBatch @@ -1137,10 +1130,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const highlightStyles = ["solid", "dashed", "solid", "solid", "solid", "solid", "solid"]; let highlighting = fullDegree && this.layoutDoc.type !== DocumentType.FONTICON && this.layoutDoc._viewType !== CollectionViewType.Linear; highlighting = highlighting && this.props.focus !== emptyFunction; // bcz: hack to turn off highlighting onsidebar panel documents. need to flag a document as not highlightable in a more direct way - return <div className={`documentView-node${this.topMost ? "-topmost" : ""}`} ref={this._mainCont} onKeyDown={this.onKeyDown} + return <div className={`documentView-node${this.topMost ? "-topmost" : ""}`} + id={this.props.Document[Id]} + ref={this._mainCont} onKeyDown={this.onKeyDown} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick} - // onPointerEnter={e => Doc.BrushDoc(this.props.Document)} - // onPointerLeave={e => Doc.BrushDoc(this.props.Document)} onPointerEnter={action(() => Doc.BrushDoc(this.props.Document))} onPointerLeave={action((e: React.PointerEvent<HTMLDivElement>) => { let entered = false; @@ -1172,7 +1165,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu </> : this.innards} {(this.Document.isBackground !== undefined || this.isSelected(false)) && this.props.renderDepth > 0 && this.props.PanelWidth() > 0 ? - <div className="documentView-lock" onClick={() => this.toggleBackground(true)}> <FontAwesomeIcon icon={this.Document.isBackground ? "unlock" : "lock"} size="lg" /> </div> + <div className="documentView-lock" onClick={() => this.toggleBackground(true)}> + <FontAwesomeIcon icon={this.Document.isBackground ? "unlock" : "lock"} style={{ color: this.Document.isBackground ? "red" : undefined }} size="lg" /> + </div> : (null)} </div>; { this._showKPQuery ? <KeyphraseQueryView keyphrases={this._queries}></KeyphraseQueryView> : undefined; } diff --git a/src/client/views/nodes/FaceRectangles.tsx b/src/client/views/nodes/FaceRectangles.tsx index 3c7f1f206..92ca276cb 100644 --- a/src/client/views/nodes/FaceRectangles.tsx +++ b/src/client/views/nodes/FaceRectangles.tsx @@ -1,8 +1,8 @@ import React = require("react"); -import { Doc, DocListCast } from "../../../new_fields/Doc"; -import { Cast, NumCast } from "../../../new_fields/Types"; +import { Doc, DocListCast } from "../../../fields/Doc"; +import { Cast, NumCast } from "../../../fields/Types"; import { observer } from "mobx-react"; -import { Id } from "../../../new_fields/FieldSymbols"; +import { Id } from "../../../fields/FieldSymbols"; import FaceRectangle from "./FaceRectangle"; interface FaceRectanglesProps { diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 016d2a1ae..e9dc43bd8 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -1,11 +1,11 @@ import React = require("react"); import { computed } from "mobx"; import { observer } from "mobx-react"; -import { DateField } from "../../../new_fields/DateField"; -import { Doc, FieldResult, Opt, Field } from "../../../new_fields/Doc"; -import { List } from "../../../new_fields/List"; -import { ScriptField } from "../../../new_fields/ScriptField"; -import { AudioField, VideoField } from "../../../new_fields/URLField"; +import { DateField } from "../../../fields/DateField"; +import { Doc, FieldResult, Opt, Field } from "../../../fields/Doc"; +import { List } from "../../../fields/List"; +import { ScriptField } from "../../../fields/ScriptField"; +import { AudioField, VideoField } from "../../../fields/URLField"; import { Transform } from "../../util/Transform"; import { CollectionView } from "../collections/CollectionView"; import { AudioBox } from "./AudioBox"; @@ -31,11 +31,11 @@ export interface FieldViewProps { select: (isCtrlPressed: boolean) => void; rootSelected: (outsideReaction?: boolean) => boolean; renderDepth: number; - addDocument?: (document: Doc) => boolean; + addDocument?: (document: Doc | Doc[]) => boolean; addDocTab: (document: Doc, where: string) => boolean; pinToPres: (document: Doc) => void; - removeDocument?: (document: Doc) => boolean; - moveDocument?: (document: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean) => boolean; + removeDocument?: (document: Doc | Doc[]) => boolean; + moveDocument?: (document: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean; backgroundColor?: (document: Doc) => string | undefined; ScreenToLocalTransform: () => Transform; bringToFront: (doc: Doc, sendToBack?: boolean) => void; @@ -43,6 +43,7 @@ export interface FieldViewProps { whenActiveChanged: (isActive: boolean) => void; dontRegisterView?: boolean; focus: (doc: Doc) => void; + ignoreAutoHeight?: boolean; PanelWidth: () => number; PanelHeight: () => number; NativeHeight: () => number; diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx index c6ea6a882..cf0b16c7c 100644 --- a/src/client/views/nodes/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox.tsx @@ -1,15 +1,16 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { createSchema, makeInterface } from '../../../new_fields/Schema'; +import { createSchema, makeInterface } from '../../../fields/Schema'; import { DocComponent } from '../DocComponent'; import './FontIconBox.scss'; import { FieldView, FieldViewProps } from './FieldView'; -import { StrCast, Cast } from '../../../new_fields/Types'; +import { StrCast, Cast } from '../../../fields/Types'; import { Utils } from "../../../Utils"; import { runInAction, observable, reaction, IReactionDisposer } from 'mobx'; -import { Doc } from '../../../new_fields/Doc'; +import { Doc } from '../../../fields/Doc'; import { ContextMenu } from '../ContextMenu'; +import { ScriptField } from '../../../fields/ScriptField'; const FontIconSchema = createSchema({ icon: "string" }); @@ -23,7 +24,7 @@ export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>( _ref: React.RefObject<HTMLButtonElement> = React.createRef(); _backgroundReaction: IReactionDisposer | undefined; componentDidMount() { - this._backgroundReaction = reaction(() => this.props.Document.backgroundColor, + this._backgroundReaction = reaction(() => this.layoutDoc.backgroundColor, () => { if (this._ref && this._ref.current) { const col = Utils.fromRGBAstr(getComputedStyle(this._ref.current).backgroundColor); @@ -35,13 +36,21 @@ export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>( } showTemplate = (): void => { - const dragFactory = Cast(this.props.Document.dragFactory, Doc, null); + const dragFactory = Cast(this.layoutDoc.dragFactory, Doc, null); dragFactory && this.props.addDocTab(dragFactory, "onRight"); } + dragAsTemplate = (): void => { + this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)'); + } + useAsPrototype = (): void => { + this.layoutDoc.onDragStart = ScriptField.MakeFunction('makeDelegate(this.dragFactory, true)'); + } specificContextMenu = (): void => { const cm = ContextMenu.Instance; cm.addItem({ description: "Show Template", event: this.showTemplate, icon: "tag" }); + cm.addItem({ description: "Use as Render Template", event: this.dragAsTemplate, icon: "tag" }); + cm.addItem({ description: "Use as Prototype", event: this.useAsPrototype, icon: "tag" }); } componentWillUnmount() { @@ -49,12 +58,12 @@ export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>( } render() { - const referenceDoc = (this.props.Document.dragFactory instanceof Doc ? this.props.Document.dragFactory : this.props.Document); + const referenceDoc = (this.layoutDoc.dragFactory instanceof Doc ? this.layoutDoc.dragFactory : this.layoutDoc); const referenceLayout = Doc.Layout(referenceDoc); - return <button className="fontIconBox-outerDiv" title={StrCast(this.props.Document.title)} ref={this._ref} onContextMenu={this.specificContextMenu} + return <button className="fontIconBox-outerDiv" title={StrCast(this.layoutDoc.title)} ref={this._ref} onContextMenu={this.specificContextMenu} style={{ background: StrCast(referenceLayout.backgroundColor), - boxShadow: this.props.Document.ischecked ? `4px 4px 12px black` : undefined + boxShadow: this.layoutDoc.ischecked ? `4px 4px 12px black` : undefined }}> <FontAwesomeIcon className="fontIconBox-icon" icon={this.dataDoc.icon as any} color={this._foregroundColor} size="sm" /> {!this.rootDoc.label ? (null) : <div className="fontIconBox-label"> {StrCast(this.rootDoc.label).substring(0, 5)} </div>} diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 08917d281..47e7607d6 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -4,16 +4,16 @@ import { faAsterisk, faBrain, faFileAudio, faImage, faPaintBrush } from '@fortaw import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, observable, runInAction } from 'mobx'; import { observer } from "mobx-react"; -import { DataSym, Doc, DocListCast, HeightSym, WidthSym } from '../../../new_fields/Doc'; -import { documentSchema } from '../../../new_fields/documentSchemas'; -import { Id } from '../../../new_fields/FieldSymbols'; -import { List } from '../../../new_fields/List'; -import { ObjectField } from '../../../new_fields/ObjectField'; -import { createSchema, listSpec, makeInterface } from '../../../new_fields/Schema'; -import { ComputedField } from '../../../new_fields/ScriptField'; -import { Cast, NumCast, StrCast } from '../../../new_fields/Types'; -import { AudioField, ImageField } from '../../../new_fields/URLField'; -import { TraceMobx } from '../../../new_fields/util'; +import { DataSym, Doc, DocListCast, HeightSym, WidthSym } from '../../../fields/Doc'; +import { documentSchema } from '../../../fields/documentSchemas'; +import { Id } from '../../../fields/FieldSymbols'; +import { List } from '../../../fields/List'; +import { ObjectField } from '../../../fields/ObjectField'; +import { createSchema, listSpec, makeInterface } from '../../../fields/Schema'; +import { ComputedField } from '../../../fields/ScriptField'; +import { Cast, NumCast, StrCast } from '../../../fields/Types'; +import { AudioField, ImageField } from '../../../fields/URLField'; +import { TraceMobx } from '../../../fields/util'; import { emptyFunction, returnOne, Utils, returnZero } from '../../../Utils'; import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices'; import { Docs } from '../../documents/Documents'; @@ -29,6 +29,7 @@ import FaceRectangles from './FaceRectangles'; import { FieldView, FieldViewProps } from './FieldView'; import "./ImageBox.scss"; import React = require("react"); +import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; const requestImageSize = require('../../util/request-image-size'); const path = require('path'); const { Howl } = require('howler'); @@ -157,21 +158,22 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD const field = Cast(this.dataDoc[this.fieldKey], ImageField); if (field) { const funcs: ContextMenuProps[] = []; - funcs.push({ description: "Copy path", event: () => Utils.CopyText(field.url.href), icon: "expand-arrows-alt" }); funcs.push({ description: "Rotate", event: this.rotate, 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" - }); + 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 : []; @@ -181,6 +183,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD !existingAnalyze && ContextMenu.Instance.addItem({ description: "Analyzers...", subitems: modes, icon: "hand-point-right" }); ContextMenu.Instance.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); + + + const existingMore = ContextMenu.Instance.findByDescription("More..."); + const mores: ContextMenuProps[] = existingMore && "subitems" in existingMore ? existingMore.subitems : []; + !existingMore && ContextMenu.Instance.addItem({ description: "More...", subitems: mores, icon: "hand-point-right" }); } } @@ -249,9 +256,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD _curSuffix = "_m"; resize = (imgPath: string) => { + const basePath = imgPath.replace(/_[oms]./, ""); const cachedNativeSize = { - width: imgPath === this.dataDoc[this.fieldKey + "-path"] ? NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"]) : 0, - height: imgPath === this.dataDoc[this.fieldKey + "-path"] ? NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"]) : 0, + width: basePath === this.dataDoc[this.fieldKey + "-path"] ? NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"]) : 0, + height: basePath === this.dataDoc[this.fieldKey + "-path"] ? NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"]) : 0, }; const docAspect = this.layoutDoc[HeightSym]() / this.layoutDoc[WidthSym](); const cachedAspect = cachedNativeSize.height / cachedNativeSize.width; @@ -265,7 +273,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD this.layoutDoc._height = this.layoutDoc[WidthSym]() * rotatedAspect; this.dataDoc[this.fieldKey + "-nativeWidth"] = this.layoutDoc._nativeWidth = this.layoutDoc._width; this.dataDoc[this.fieldKey + "-nativeHeight"] = this.layoutDoc._nativeHeight = this.layoutDoc._height; - this.dataDoc[this.fieldKey + "-path"] = imgPath; + this.dataDoc[this.fieldKey + "-path"] = basePath; } })).catch(console.log); } else if (Math.abs(1 - docAspect / cachedAspect) > 0.1) { diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index 39d7109b1..e983852ea 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -1,13 +1,13 @@ import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; -import { Doc, Field, FieldResult } from "../../../new_fields/Doc"; -import { List } from "../../../new_fields/List"; -import { RichTextField } from "../../../new_fields/RichTextField"; -import { listSpec } from "../../../new_fields/Schema"; -import { ComputedField, ScriptField } from "../../../new_fields/ScriptField"; -import { Cast, FieldValue, NumCast } from "../../../new_fields/Types"; -import { ImageField } from "../../../new_fields/URLField"; +import { Doc, Field, FieldResult } from "../../../fields/Doc"; +import { List } from "../../../fields/List"; +import { RichTextField } from "../../../fields/RichTextField"; +import { listSpec } from "../../../fields/Schema"; +import { ComputedField, ScriptField } from "../../../fields/ScriptField"; +import { Cast, FieldValue, NumCast } from "../../../fields/Types"; +import { ImageField } from "../../../fields/URLField"; import { Docs } from "../../documents/Documents"; import { SetupDrag } from "../../util/DragManager"; import { CompiledScript, CompileScript, ScriptOptions } from "../../util/Scripting"; diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index 6dc4ae578..956d6556b 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -1,6 +1,6 @@ import { action, observable } from 'mobx'; import { observer } from "mobx-react"; -import { Doc, Field, Opt } from '../../../new_fields/Doc'; +import { Doc, Field, Opt } from '../../../fields/Doc'; import { emptyFunction, returnFalse, returnOne, returnZero } from '../../../Utils'; import { Docs } from '../../documents/Documents'; import { Transform } from '../../util/Transform'; diff --git a/src/client/views/nodes/LabelBox.scss b/src/client/views/nodes/LabelBox.scss index 7c7e92379..b605df262 100644 --- a/src/client/views/nodes/LabelBox.scss +++ b/src/client/views/nodes/LabelBox.scss @@ -4,23 +4,19 @@ border-radius: inherit; display: flex; flex-direction: column; + position: absolute; } .labelBox-mainButton { - width: 100%; - height: 100%; + width: fit-content; + height: max-content; border-radius: inherit; letter-spacing: 2px; text-transform: uppercase; overflow: hidden; - display:flex; -} - -.labelBox-mainButtonCenter { - overflow: hidden; - display: inline; - align-items: center; + display: inline-block; margin: auto; + text-overflow: ellipsis; } .labelBox-params { diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx index 3cdec8acb..2d27ec441 100644 --- a/src/client/views/nodes/LabelBox.tsx +++ b/src/client/views/nodes/LabelBox.tsx @@ -3,11 +3,11 @@ import { faEdit } from '@fortawesome/free-regular-svg-icons'; import { action } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, DocListCast } from '../../../new_fields/Doc'; -import { documentSchema } from '../../../new_fields/documentSchemas'; -import { List } from '../../../new_fields/List'; -import { createSchema, listSpec, makeInterface } from '../../../new_fields/Schema'; -import { Cast, NumCast, StrCast } from '../../../new_fields/Types'; +import { Doc, DocListCast } from '../../../fields/Doc'; +import { documentSchema } from '../../../fields/documentSchemas'; +import { List } from '../../../fields/List'; +import { createSchema, listSpec, makeInterface } from '../../../fields/Schema'; +import { Cast, NumCast, StrCast } from '../../../fields/Types'; import { DragManager } from '../../util/DragManager'; import { undoBatch } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; @@ -80,12 +80,9 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps, LabelDocument paddingRight: NumCast(this.layoutDoc._xPadding), paddingTop: NumCast(this.layoutDoc._yPadding), paddingBottom: NumCast(this.layoutDoc._yPadding), - textOverflow: this.layoutDoc._singleLine ? "ellipsis" : undefined, - whiteSpace: this.layoutDoc._singleLine ? "nowrap" : "pre-wrap" + whiteSpace: this.layoutDoc._singleLine ? "pre" : "pre-wrap" }} > - <div className="labelBox-mainButtonCenter"> - {StrCast(this.rootDoc.text, StrCast(this.rootDoc.title))} - </div> + {StrCast(this.rootDoc.text, StrCast(this.rootDoc.title))} </div> <div className="labelBox-fieldKeyParams" > {!missingParams?.length ? (null) : missingParams.map(m => <div key={m} className="labelBox-missingParam">{m}</div>)} diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx index bc36e056e..098aa58e9 100644 --- a/src/client/views/nodes/LinkAnchorBox.tsx +++ b/src/client/views/nodes/LinkAnchorBox.tsx @@ -1,9 +1,9 @@ import { action, observable } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCast } from "../../../new_fields/Doc"; -import { documentSchema } from "../../../new_fields/documentSchemas"; -import { makeInterface } from "../../../new_fields/Schema"; -import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; +import { Doc, DocListCast } from "../../../fields/Doc"; +import { documentSchema } from "../../../fields/documentSchemas"; +import { makeInterface } from "../../../fields/Schema"; +import { Cast, NumCast, StrCast } from "../../../fields/Types"; import { Utils, setupMoveUpEvents, emptyFunction } from '../../../Utils'; import { DocumentManager } from "../../util/DocumentManager"; import { DragManager } from "../../util/DragManager"; @@ -16,7 +16,7 @@ import { ContextMenu } from "../ContextMenu"; import { LinkEditor } from "../linking/LinkEditor"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { SelectionManager } from "../../util/SelectionManager"; -import { TraceMobx } from "../../../new_fields/util"; +import { TraceMobx } from "../../../fields/util"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx index 740f2ef04..3f942e87b 100644 --- a/src/client/views/nodes/LinkBox.tsx +++ b/src/client/views/nodes/LinkBox.tsx @@ -1,13 +1,13 @@ import React = require("react"); import { observer } from "mobx-react"; -import { documentSchema } from "../../../new_fields/documentSchemas"; -import { makeInterface, listSpec } from "../../../new_fields/Schema"; +import { documentSchema } from "../../../fields/documentSchemas"; +import { makeInterface, listSpec } from "../../../fields/Schema"; import { returnFalse, returnZero } from "../../../Utils"; import { CollectionTreeView } from "../collections/CollectionTreeView"; import { ViewBoxBaseComponent } from "../DocComponent"; import { FieldView, FieldViewProps } from './FieldView'; import "./LinkBox.scss"; -import { Cast } from "../../../new_fields/Types"; +import { Cast } from "../../../fields/Types"; type LinkDocument = makeInterface<[typeof documentSchema]>; const LinkDocument = makeInterface(documentSchema); diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 3712c648e..493f23dc4 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -3,11 +3,11 @@ import { action, observable, runInAction, reaction, IReactionDisposer, trace, un import { observer } from "mobx-react"; import * as Pdfjs from "pdfjs-dist"; import "pdfjs-dist/web/pdf_viewer.css"; -import { Opt, WidthSym, Doc, HeightSym } from "../../../new_fields/Doc"; -import { makeInterface } from "../../../new_fields/Schema"; -import { ScriptField } from '../../../new_fields/ScriptField'; -import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; -import { PdfField, URLField } from "../../../new_fields/URLField"; +import { Opt, WidthSym, Doc, HeightSym } from "../../../fields/Doc"; +import { makeInterface } from "../../../fields/Schema"; +import { ScriptField } from '../../../fields/ScriptField'; +import { Cast, NumCast, StrCast } from "../../../fields/Types"; +import { PdfField, URLField } from "../../../fields/URLField"; import { Utils } from '../../../Utils'; import { undoBatch } from '../../util/UndoManager'; import { panZoomSchema } from '../collections/collectionFreeForm/CollectionFreeFormView'; @@ -20,7 +20,7 @@ import { pageSchema } from "./ImageBox"; import { KeyCodes } from '../../util/KeyCodes'; import "./PDFBox.scss"; import React = require("react"); -import { documentSchema } from '../../../new_fields/documentSchemas'; +import { documentSchema } from '../../../fields/documentSchemas'; type PdfDocument = makeInterface<[typeof documentSchema, typeof panZoomSchema, typeof pageSchema]>; const PdfDocument = makeInterface(documentSchema, panZoomSchema, pageSchema); diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx index 343e74c87..342a8a215 100644 --- a/src/client/views/nodes/PresBox.tsx +++ b/src/client/views/nodes/PresBox.tsx @@ -2,11 +2,11 @@ import React = require("react"); import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCast, DocCastAsync } from "../../../new_fields/Doc"; -import { InkTool } from "../../../new_fields/InkField"; -import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types"; +import { Doc, DocListCast, DocCastAsync } from "../../../fields/Doc"; +import { InkTool } from "../../../fields/InkField"; +import { BoolCast, Cast, NumCast, StrCast } from "../../../fields/Types"; import { returnFalse } from "../../../Utils"; -import { documentSchema } from "../../../new_fields/documentSchemas"; +import { documentSchema } from "../../../fields/documentSchemas"; import { DocumentManager } from "../../util/DocumentManager"; import { undoBatch } from "../../util/UndoManager"; import { CollectionDockingView } from "../collections/CollectionDockingView"; @@ -15,11 +15,11 @@ import { InkingControl } from "../InkingControl"; import { FieldView, FieldViewProps } from './FieldView'; import "./PresBox.scss"; import { ViewBoxBaseComponent } from "../DocComponent"; -import { makeInterface } from "../../../new_fields/Schema"; -import { List } from "../../../new_fields/List"; +import { makeInterface } from "../../../fields/Schema"; +import { List } from "../../../fields/List"; import { Docs } from "../../documents/Documents"; -import { PrefetchProxy } from "../../../new_fields/Proxy"; -import { ScriptField } from "../../../new_fields/ScriptField"; +import { PrefetchProxy } from "../../../fields/Proxy"; +import { ScriptField } from "../../../fields/ScriptField"; import { Scripting } from "../../util/Scripting"; type PresBoxSchema = makeInterface<[typeof documentSchema]>; @@ -30,7 +30,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PresBox, fieldKey); } @observable _isChildActive = false; @computed get childDocs() { return DocListCast(this.dataDoc[this.fieldKey]); } - @computed get currentIndex() { return NumCast(this.presElement?.currentIndex); } + @computed get itemIndex() { return NumCast(this.rootDoc._itemIndex); } @computed get presElement() { return Cast(Doc.UserDoc().presElement, Doc, null); } constructor(props: any) { super(props); @@ -42,10 +42,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> // this is a design choice -- we could write this data to the presElements which would require a reaction to keep it up to date, and it would prevent // the preselement docs from being part of multiple presentations since they would all have the same field, or we'd have to keep per-presentation data // stored on each pres element. - (this.presElement as Doc).lookupField = ScriptField.MakeScript( - `if (field === 'indexInPres') return docList(container[container.presentationFieldKey]).indexOf(data);` + - "if (field === 'presCollapsedHeight') return container._viewType === CollectionViewType.Stacking ? 50 : 46;" + - "return undefined;", { field: "string", data: Doc.name, container: Doc.name }); + (this.presElement as Doc).lookupField = ScriptField.MakeFunction("lookupPresBoxField(container, field, data)", + { field: "string", data: Doc.name, container: Doc.name }); } this.props.Document.presentationFieldKey = this.fieldKey; // provide info to the presElement script so that it can look up rendering information about the presBox } @@ -61,15 +59,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @action next = () => { this.updateCurrentPresentation(); - if (this.childDocs[this.currentIndex + 1] !== undefined) { - let nextSelected = this.currentIndex + 1; - this.gotoDocument(nextSelected, this.currentIndex); + if (this.childDocs[this.itemIndex + 1] !== undefined) { + let nextSelected = this.itemIndex + 1; + this.gotoDocument(nextSelected, this.itemIndex); for (nextSelected = nextSelected + 1; nextSelected < this.childDocs.length; nextSelected++) { if (!this.childDocs[nextSelected].groupButton) { break; } else { - this.gotoDocument(nextSelected, this.currentIndex); + this.gotoDocument(nextSelected, this.itemIndex); } } } @@ -79,18 +77,18 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @action back = () => { this.updateCurrentPresentation(); - const docAtCurrent = this.childDocs[this.currentIndex]; + const docAtCurrent = this.childDocs[this.itemIndex]; if (docAtCurrent) { //check if any of the group members had used zooming in including the current document //If so making sure to zoom out, which goes back to state before zooming action - let prevSelected = this.currentIndex; + let prevSelected = this.itemIndex; let didZoom = docAtCurrent.zoomButton; for (; !didZoom && prevSelected > 0 && this.childDocs[prevSelected].groupButton; prevSelected--) { didZoom = this.childDocs[prevSelected].zoomButton; } prevSelected = Math.max(0, prevSelected - 1); - this.gotoDocument(prevSelected, this.currentIndex); + this.gotoDocument(prevSelected, this.itemIndex); } } @@ -194,10 +192,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> this.updateCurrentPresentation(); Doc.UnBrushAllDocs(); if (index >= 0 && index < this.childDocs.length) { - this.presElement.currentIndex = index; + this.rootDoc._itemIndex = index; - if (!this.presElement.presStatus) { - this.presElement.presStatus = true; + if (!this.layoutDoc.presStatus) { + this.layoutDoc.presStatus = true; this.startPresentation(index); } @@ -210,12 +208,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> //The function that starts or resets presentaton functionally, depending on status flag. startOrResetPres = () => { this.updateCurrentPresentation(); - if (this.presElement.presStatus) { + if (this.layoutDoc.presStatus) { this.resetPresentation(); } else { - this.presElement.presStatus = true; + this.layoutDoc.presStatus = true; this.startPresentation(0); - this.gotoDocument(0, this.currentIndex); + this.gotoDocument(0, this.itemIndex); } } @@ -225,7 +223,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> this.updateCurrentPresentation(); this.childDocs.forEach(doc => (doc.presentationTargetDoc as Doc).opacity = 1); this.rootDoc._itemIndex = 0; - this.presElement.presStatus = false; + this.layoutDoc.presStatus = false; } //The function that starts the presentation, also checking if actions should be applied @@ -271,14 +269,17 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> }); whenActiveChanged = action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive)); - addDocumentFilter = (doc: Doc) => { - doc.aliasOf instanceof Doc && (doc.presentationTargetDoc = doc.aliasOf); - !this.childDocs.includes(doc) && (doc.presZoomButton = true); + addDocumentFilter = (doc: Doc | Doc[]) => { + const docs = doc instanceof Doc ? [doc] : doc; + docs.forEach(doc => { + doc.aliasOf instanceof Doc && (doc.presentationTargetDoc = doc.aliasOf); + !this.childDocs.includes(doc) && (doc.presZoomButton = true); + }); return true; } childLayoutTemplate = () => this.rootDoc._viewType !== CollectionViewType.Stacking ? undefined : this.presElement; removeDocument = (doc: Doc) => Doc.RemoveDocFromList(this.dataDoc, this.fieldKey, doc); - selectElement = (doc: Doc) => this.gotoDocument(this.childDocs.indexOf(doc), NumCast(this.rootDoc._itemIndex)); + selectElement = (doc: Doc) => this.gotoDocument(this.childDocs.indexOf(doc), NumCast(this.itemIndex)); getTransform = () => this.props.ScreenToLocalTransform().translate(-5, -65);// listBox padding-left and pres-box-cont minHeight panelHeight = () => this.props.PanelHeight() - 20; active = (outsideReaction?: boolean) => ((InkingControl.Instance.selectedTool === InkTool.None && !this.layoutDoc.isBackground) && @@ -318,6 +319,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> childLayoutTemplate={this.childLayoutTemplate} filterAddDocument={this.addDocumentFilter} removeDocument={returnFalse} + dontRegisterView={true} focus={this.selectElement} ScreenToLocalTransform={this.getTransform} /> : (null) @@ -326,5 +328,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> </div>; } } -Scripting.addGlobal(function lookupPresBoxField(presLayout: Doc, data: Doc, fieldKey: string) { +Scripting.addGlobal(function lookupPresBoxField(container: Doc, field: string, data: Doc) { + if (field === 'indexInPres') return DocListCast(container[StrCast(container.presentationFieldKey)]).indexOf(data); + if (field === 'presCollapsedHeight') return container._viewType === CollectionViewType.Stacking ? 50 : 46; + if (field === 'presStatus') return container.presStatus; + if (field === '_itemIndex') return container._itemIndex; + return undefined; }); diff --git a/src/client/views/nodes/QueryBox.tsx b/src/client/views/nodes/QueryBox.tsx index d248b098c..0fff0b57f 100644 --- a/src/client/views/nodes/QueryBox.tsx +++ b/src/client/views/nodes/QueryBox.tsx @@ -1,17 +1,16 @@ import React = require("react"); import { IReactionDisposer } from "mobx"; import { observer } from "mobx-react"; -import { documentSchema } from "../../../new_fields/documentSchemas"; -import { Id } from '../../../new_fields/FieldSymbols'; -import { makeInterface, listSpec } from "../../../new_fields/Schema"; -import { StrCast, Cast } from "../../../new_fields/Types"; -import { SelectionManager } from "../../util/SelectionManager"; +import { documentSchema } from "../../../fields/documentSchemas"; +import { Id } from '../../../fields/FieldSymbols'; +import { makeInterface, listSpec } from "../../../fields/Schema"; +import { StrCast, Cast } from "../../../fields/Types"; import { ViewBoxAnnotatableComponent } from '../DocComponent'; import { SearchBox } from "../search/SearchBox"; import { FieldView, FieldViewProps } from './FieldView'; import "./QueryBox.scss"; -import { List } from "../../../new_fields/List"; -import { DragManager } from "../../util/DragManager"; +import { List } from "../../../fields/List"; +import { SnappingManager } from "../../util/SnappingManager"; type QueryDocument = makeInterface<[typeof documentSchema]>; const QueryDocument = makeInterface(documentSchema); @@ -28,7 +27,7 @@ export class QueryBox extends ViewBoxAnnotatableComponent<FieldViewProps, QueryD } render() { - const dragging = !DragManager.Vals.Instance.GetIsDragging() ? "" : "-dragging"; + const dragging = !SnappingManager.GetIsDragging() ? "" : "-dragging"; return <div className={`queryBox${dragging}`} onWheel={(e) => e.stopPropagation()} > <SearchBox id={this.props.Document[Id]} diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx index a0ecc9ff5..5d4af2d77 100644 --- a/src/client/views/nodes/ScreenshotBox.tsx +++ b/src/client/views/nodes/ScreenshotBox.tsx @@ -5,10 +5,10 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, computed, IReactionDisposer, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; import * as rp from 'request-promise'; -import { documentSchema } from "../../../new_fields/documentSchemas"; -import { makeInterface } from "../../../new_fields/Schema"; -import { Cast, NumCast } from "../../../new_fields/Types"; -import { VideoField } from "../../../new_fields/URLField"; +import { documentSchema } from "../../../fields/documentSchemas"; +import { makeInterface } from "../../../fields/Schema"; +import { Cast, NumCast } from "../../../fields/Types"; +import { VideoField } from "../../../fields/URLField"; import { emptyFunction, returnFalse, returnOne, Utils, returnZero } from "../../../Utils"; import { Docs, DocUtils } from "../../documents/Documents"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx index c607d6614..0944edf60 100644 --- a/src/client/views/nodes/ScriptingBox.tsx +++ b/src/client/views/nodes/ScriptingBox.tsx @@ -1,10 +1,10 @@ import { action, observable, computed } from "mobx"; import { observer } from "mobx-react"; import * as React from "react"; -import { documentSchema } from "../../../new_fields/documentSchemas"; -import { createSchema, makeInterface, listSpec } from "../../../new_fields/Schema"; -import { ScriptField } from "../../../new_fields/ScriptField"; -import { StrCast, ScriptCast, Cast } from "../../../new_fields/Types"; +import { documentSchema } from "../../../fields/documentSchemas"; +import { createSchema, makeInterface, listSpec } from "../../../fields/Schema"; +import { ScriptField } from "../../../fields/ScriptField"; +import { StrCast, ScriptCast, Cast } from "../../../fields/Types"; import { InteractionUtils } from "../../util/InteractionUtils"; import { CompileScript, isCompileError, ScriptParam } from "../../util/Scripting"; import { ViewBoxAnnotatableComponent } from "../DocComponent"; @@ -13,7 +13,7 @@ import { FieldView, FieldViewProps } from "../nodes/FieldView"; import "./ScriptingBox.scss"; import { OverlayView } from "../OverlayView"; import { DocumentIconContainer } from "./DocumentIcon"; -import { List } from "../../../new_fields/List"; +import { List } from "../../../fields/List"; const ScriptingSchema = createSchema({}); type ScriptingDocument = makeInterface<[typeof ScriptingSchema, typeof documentSchema]>; diff --git a/src/client/views/nodes/SliderBox.tsx b/src/client/views/nodes/SliderBox.tsx index cb2526769..9a1aefba9 100644 --- a/src/client/views/nodes/SliderBox.tsx +++ b/src/client/views/nodes/SliderBox.tsx @@ -4,10 +4,10 @@ import { runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Handles, Rail, Slider, Ticks, Tracks } from 'react-compound-slider'; -import { documentSchema } from '../../../new_fields/documentSchemas'; -import { createSchema, makeInterface } from '../../../new_fields/Schema'; -import { ScriptField } from '../../../new_fields/ScriptField'; -import { Cast, NumCast, StrCast } from '../../../new_fields/Types'; +import { documentSchema } from '../../../fields/documentSchemas'; +import { createSchema, makeInterface } from '../../../fields/Schema'; +import { ScriptField } from '../../../fields/ScriptField'; +import { Cast, NumCast, StrCast } from '../../../fields/Types'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { ViewBoxBaseComponent } from '../DocComponent'; diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 266b7f43f..ccf1f5588 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -5,12 +5,12 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, computed, IReactionDisposer, observable, reaction, runInAction, untracked } from "mobx"; import { observer } from "mobx-react"; import * as rp from 'request-promise'; -import { Doc } from "../../../new_fields/Doc"; -import { InkTool } from "../../../new_fields/InkField"; -import { createSchema, makeInterface } from "../../../new_fields/Schema"; -import { ScriptField } from "../../../new_fields/ScriptField"; -import { Cast, StrCast } from "../../../new_fields/Types"; -import { VideoField } from "../../../new_fields/URLField"; +import { Doc } from "../../../fields/Doc"; +import { InkTool } from "../../../fields/InkField"; +import { createSchema, makeInterface } from "../../../fields/Schema"; +import { ScriptField } from "../../../fields/ScriptField"; +import { Cast, StrCast } from "../../../fields/Types"; +import { VideoField } from "../../../fields/URLField"; import { Utils, emptyFunction, returnOne, returnZero } from "../../../Utils"; import { Docs, DocUtils } from "../../documents/Documents"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; @@ -21,7 +21,7 @@ import { DocumentDecorations } from "../DocumentDecorations"; import { InkingControl } from "../InkingControl"; import { FieldView, FieldViewProps } from './FieldView'; import "./VideoBox.scss"; -import { documentSchema } from "../../../new_fields/documentSchemas"; +import { documentSchema } from "../../../fields/documentSchemas"; const path = require('path'); export const timeSchema = createSchema({ @@ -337,9 +337,12 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD } @action.bound - addDocumentWithTimestamp(doc: Doc): boolean { - const curTime = (this.layoutDoc.currentTimecode || -1); - curTime !== -1 && (doc.displayTimecode = curTime); + addDocumentWithTimestamp(doc: Doc | Doc[]): boolean { + const docs = doc instanceof Doc ? [doc] : doc; + docs.forEach(doc => { + const curTime = (this.layoutDoc.currentTimecode || -1); + curTime !== -1 && (doc.displayTimecode = curTime); + }); return this.addDocument(doc); } diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss index af84a7d95..4623444b9 100644 --- a/src/client/views/nodes/WebBox.scss +++ b/src/client/views/nodes/WebBox.scss @@ -3,6 +3,38 @@ .webBox-container, .webBox-container-dragging { transform-origin: top left; + width: 100%; + height: 100%; + + .webBox-htmlSpan { + position: absolute; + top: 0; + left: 0; + } + .webBox-cont { + pointer-events: none; + } + .webBox-cont, .webBox-cont-interactive { + padding: 0vw; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + transform-origin: top left; + overflow: auto; + .webBox-iframe { + width: 100%; + height: 100%; + position: absolute; + top:0; + } + } + .webBox-cont-interactive { + span { + user-select: text !important; + } + } .webBox-outerContent { width: 100%; height: 100%; @@ -18,29 +50,7 @@ display:none; } } -.webBox-cont { - padding: 0vw; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - transform-origin: top left; - overflow: auto; - pointer-events: none; -} - -.webBox-cont-interactive { - span { - user-select: text !important; - } -} -#webBox-htmlSpan { - position: absolute; - top: 0; - left: 0; -} .webBox-overlay { width: 100%; @@ -66,8 +76,6 @@ opacity: 0.9; z-index: 9001; transition: top .5s; - padding: 10px; - .urlEditor { display: grid; diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 4e383e468..82f05012a 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -2,13 +2,13 @@ import { library } from "@fortawesome/fontawesome-svg-core"; import { faStickyNote, faPen, faMousePointer } from '@fortawesome/free-solid-svg-icons'; import { action, computed, observable, trace, IReactionDisposer, reaction } from "mobx"; import { observer } from "mobx-react"; -import { Doc, FieldResult } from "../../../new_fields/Doc"; -import { documentSchema } from "../../../new_fields/documentSchemas"; -import { HtmlField } from "../../../new_fields/HtmlField"; -import { InkTool } from "../../../new_fields/InkField"; -import { makeInterface } from "../../../new_fields/Schema"; -import { Cast, NumCast, BoolCast, StrCast } from "../../../new_fields/Types"; -import { WebField } from "../../../new_fields/URLField"; +import { Doc, FieldResult } from "../../../fields/Doc"; +import { documentSchema } from "../../../fields/documentSchemas"; +import { HtmlField } from "../../../fields/HtmlField"; +import { InkTool } from "../../../fields/InkField"; +import { makeInterface } from "../../../fields/Schema"; +import { Cast, NumCast, BoolCast, StrCast } from "../../../fields/Types"; +import { WebField } from "../../../fields/URLField"; import { Utils, returnOne, emptyFunction, returnZero } from "../../../Utils"; import { Docs } from "../../documents/Documents"; import { DragManager } from "../../util/DragManager"; @@ -48,10 +48,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean) => void); iframeLoaded = action((e: any) => { - this._iframeRef.current!.contentDocument?.addEventListener('pointerdown', this.iframedown, false); - this._iframeRef.current!.contentDocument?.addEventListener('scroll', this.iframeScrolled, false); - this.layoutDoc.scrollHeight = this._iframeRef.current!.contentDocument?.children?.[0].scrollHeight || 1000; - this._iframeRef.current!.contentDocument!.children[0].scrollTop = NumCast(this.layoutDoc.scrollTop); + if (this._iframeRef.current?.contentDocument) { + this._iframeRef.current.contentDocument.addEventListener('pointerdown', this.iframedown, false); + this._iframeRef.current.contentDocument.addEventListener('scroll', this.iframeScrolled, false); + this.layoutDoc.scrollHeight = this._iframeRef.current.contentDocument.children?.[0].scrollHeight || 1000; + this._iframeRef.current.contentDocument.children[0].scrollTop = NumCast(this.layoutDoc.scrollTop); + } this._reactionDisposer?.(); this._reactionDisposer = reaction(() => this.layoutDoc.scrollY, (scrollY) => { @@ -127,28 +129,25 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum } } - toggleNativeDimensions = () => { + toggleAnnotationMode = () => { if (!this.layoutDoc.isAnnotating) { - //DocumentView.unfreezeNativeDimensions(this.layoutDoc); this.layoutDoc.lockedTransform = false; this.layoutDoc.isAnnotating = true; } else { - //Doc.freezeNativeDimensions(this.layoutDoc, this.props.PanelWidth(), this.props.PanelHeight()); this.layoutDoc.lockedTransform = true; this.layoutDoc.isAnnotating = false; } } urlEditor() { - const frozen = this.layoutDoc._nativeWidth && this.layoutDoc.isAnnotating; return ( <div className="webBox-urlEditor" style={{ top: this._collapsed ? -70 : 0 }}> <div className="urlEditor"> <div className="editorBase"> <button className="editor-collapse" style={{ - top: this._collapsed ? 70 : 10, + top: this._collapsed ? 70 : 0, transform: `rotate(${this._collapsed ? 180 : 0}deg) scale(${this._collapsed ? 0.5 : 1}) translate(${this._collapsed ? "-100%, -100%" : "0, 0"})`, opacity: (this._collapsed && !this.props.isSelected()) ? 0 : 0.9, left: (this._collapsed ? 0 : "unset"), @@ -157,10 +156,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum <FontAwesomeIcon icon="caret-up" size="2x" /> </button> <div className="webBox-buttons" style={{ display: this._collapsed ? "none" : "flex" }}> - <div className="webBox-freeze" title={"Annotate"} style={{ background: frozen ? "lightBlue" : "gray" }} onClick={this.toggleNativeDimensions} > + <div className="webBox-freeze" title={"Annotate"} style={{ background: this.layoutDoc.isAnnotating ? "lightBlue" : "gray" }} onClick={this.toggleAnnotationMode} > <FontAwesomeIcon icon={faPen} size={"2x"} /> </div> - <div className="webBox-freeze" title={"Select"} style={{ background: !frozen ? "lightBlue" : "gray" }} onClick={this.toggleNativeDimensions} > + <div className="webBox-freeze" title={"Select"} style={{ background: !this.layoutDoc.isAnnotating ? "lightBlue" : "gray" }} onClick={this.toggleAnnotationMode} > <FontAwesomeIcon icon={faMousePointer} size={"2x"} /> </div> <input className="webpage-urlInput" @@ -309,33 +308,35 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum } } + //const href = "https://brown365-my.sharepoint.com/personal/bcz_ad_brown_edu/_layouts/15/Doc.aspx?sourcedoc={31aa3178-4c21-4474-b367-877d0a7135e4}&action=embedview&wdStartOn=1"; @computed - get content() { + get urlContent() { + const field = this.dataDoc[this.props.fieldKey]; let view; if (field instanceof HtmlField) { - view = <span id="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: field.html }} />; + view = <span className="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: field.html }} />; } else if (field instanceof WebField) { const url = this.layoutDoc.UseCors ? Utils.CorsProxy(field.url.href) : field.url.href; - view = <iframe ref={this._iframeRef} onLoad={this.iframeLoaded} src={url} style={{ position: "absolute", width: "100%", height: "100%", top: 0 }} />; + view = <iframe className="webBox-iframe" ref={this._iframeRef} src={url} onLoad={this.iframeLoaded} />; } else { - view = <iframe ref={this._iframeRef} src={"https://crossorigin.me/https://cs.brown.edu"} style={{ position: "absolute", width: "100%", height: "100%", top: 0 }} />; + view = <iframe className="webBox-iframe" ref={this._iframeRef} src={"https://crossorigin.me/https://cs.brown.edu"} />; } - const content = - <div style={{ width: "100%", height: "100%", position: "absolute" }} onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}> - {this.urlEditor()} - {view} - </div>; - + return view; + } + @computed + get content() { + const view = this.urlContent; const decInteracting = DocumentDecorations.Instance?.Interacting; const frozen = !this.props.isSelected() || decInteracting; return (<> - <div className={"webBox-cont" + (this.props.isSelected() && InkingControl.Instance.selectedTool === InkTool.None && !decInteracting ? "-interactive" : "")} > - {content} - </div> + <div className={"webBox-cont" + (this.props.isSelected() && InkingControl.Instance.selectedTool === InkTool.None && !decInteracting ? "-interactive" : "")} + onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}> + {view} + </div>; {!frozen ? (null) : <div className="webBox-overlay" style={{ pointerEvents: this.layoutDoc.isBackground ? undefined : "all" }} onWheel={this.onPreWheel} onPointerDown={this.onPrePointer} onPointerMove={this.onPrePointer} onPointerUp={this.onPrePointer}> @@ -344,6 +345,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum <div className="dragger" ref={this._iframeDragRef}></div> </div> </div>} + {this.urlEditor()} </>); } scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.props.Document.scrollTop)); @@ -351,8 +353,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum return (<div className={`webBox-container`} style={{ transform: `scale(${this.props.ContentScaling()})`, - width: `${100 / this.props.ContentScaling()}%`, - height: `${100 / this.props.ContentScaling()}%`, + width: Number.isFinite(this.props.ContentScaling()) ? `${100 / this.props.ContentScaling()}%` : "100%", + height: Number.isFinite(this.props.ContentScaling()) ? `${100 / this.props.ContentScaling()}%` : "100%", pointerEvents: this.layoutDoc.isBackground ? "none" : undefined }} > {this.content} diff --git a/src/client/views/nodes/formattedText/DashDocCommentView.tsx b/src/client/views/nodes/formattedText/DashDocCommentView.tsx index d94fe7fc6..d56b87ae5 100644 --- a/src/client/views/nodes/formattedText/DashDocCommentView.tsx +++ b/src/client/views/nodes/formattedText/DashDocCommentView.tsx @@ -8,14 +8,14 @@ import { EditorState, NodeSelection, Plugin, TextSelection } from "prosemirror-s import { StepMap } from "prosemirror-transform"; import { EditorView } from "prosemirror-view"; import * as ReactDOM from 'react-dom'; -import { Doc, DocListCast, Field, HeightSym, WidthSym } from "../../../../new_fields/Doc"; -import { Id } from "../../../../new_fields/FieldSymbols"; -import { List } from "../../../../new_fields/List"; -import { ObjectField } from "../../../../new_fields/ObjectField"; -import { listSpec } from "../../../../new_fields/Schema"; -import { SchemaHeaderField } from "../../../../new_fields/SchemaHeaderField"; -import { ComputedField } from "../../../../new_fields/ScriptField"; -import { BoolCast, Cast, NumCast, StrCast } from "../../../../new_fields/Types"; +import { Doc, DocListCast, Field, HeightSym, WidthSym } from "../../../../fields/Doc"; +import { Id } from "../../../../fields/FieldSymbols"; +import { List } from "../../../../fields/List"; +import { ObjectField } from "../../../../fields/ObjectField"; +import { listSpec } from "../../../../fields/Schema"; +import { SchemaHeaderField } from "../../../../fields/SchemaHeaderField"; +import { ComputedField } from "../../../../fields/ScriptField"; +import { BoolCast, Cast, NumCast, StrCast } from "../../../../fields/Types"; import { emptyFunction, returnEmptyString, returnFalse, returnOne, Utils, returnZero } from "../../../../Utils"; import { DocServer } from "../../../DocServer"; diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx index 7130fee2b..05e6a5959 100644 --- a/src/client/views/nodes/formattedText/DashDocView.tsx +++ b/src/client/views/nodes/formattedText/DashDocView.tsx @@ -1,10 +1,10 @@ import { IReactionDisposer, reaction } from "mobx"; import { NodeSelection } from "prosemirror-state"; -import { Doc, HeightSym, WidthSym } from "../../../../new_fields/Doc"; -import { Id } from "../../../../new_fields/FieldSymbols"; -import { ObjectField } from "../../../../new_fields/ObjectField"; -import { ComputedField } from "../../../../new_fields/ScriptField"; -import { BoolCast, Cast, NumCast, StrCast } from "../../../../new_fields/Types"; +import { Doc, HeightSym, WidthSym } from "../../../../fields/Doc"; +import { Id } from "../../../../fields/FieldSymbols"; +import { ObjectField } from "../../../../fields/ObjectField"; +import { ComputedField } from "../../../../fields/ScriptField"; +import { BoolCast, Cast, NumCast, StrCast } from "../../../../fields/Types"; import { emptyFunction, returnEmptyString, returnFalse, Utils, returnZero } from "../../../../Utils"; import { DocServer } from "../../../DocServer"; import { Docs } from "../../../documents/Documents"; diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx index d87d6e424..d05e8f1ea 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.tsx +++ b/src/client/views/nodes/formattedText/DashFieldView.tsx @@ -1,10 +1,10 @@ import { IReactionDisposer, observable, runInAction, computed, action } from "mobx"; -import { Doc, DocListCast, Field } from "../../../../new_fields/Doc"; -import { List } from "../../../../new_fields/List"; -import { listSpec } from "../../../../new_fields/Schema"; -import { SchemaHeaderField } from "../../../../new_fields/SchemaHeaderField"; -import { ComputedField } from "../../../../new_fields/ScriptField"; -import { Cast, StrCast } from "../../../../new_fields/Types"; +import { Doc, DocListCast, Field } from "../../../../fields/Doc"; +import { List } from "../../../../fields/List"; +import { listSpec } from "../../../../fields/Schema"; +import { SchemaHeaderField } from "../../../../fields/SchemaHeaderField"; +import { ComputedField } from "../../../../fields/ScriptField"; +import { Cast, StrCast } from "../../../../fields/Types"; import { DocServer } from "../../../DocServer"; import { CollectionViewType } from "../../collections/CollectionView"; import { FormattedTextBox } from "./FormattedTextBox"; @@ -34,6 +34,7 @@ export class DashFieldView { docid={node.attrs.docid} width={node.attrs.width} height={node.attrs.height} + hideKey={node.attrs.hideKey} tbox={tbox} />, this._fieldWrapper); (this as any).dom = this._fieldWrapper; @@ -47,6 +48,7 @@ export class DashFieldView { interface IDashFieldViewInternal { fieldKey: string; docid: string; + hideKey: boolean; tbox: FormattedTextBox; width: number; height: number; @@ -77,11 +79,13 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna this._reactionDisposer?.(); } + multiValueDelimeter = ";"; + // set the display of the field's value (checkbox for booleans, span of text for strings) @computed get fieldValueContent() { if (this._dashDoc) { - const dashVal = this._dashDoc[this._fieldKey]; - const fval = StrCast(dashVal).startsWith(":=") || dashVal === "" ? Doc.Layout(this._textBoxDoc)[this._fieldKey] : dashVal; + const dashVal = this._dashDoc[this._fieldKey] || (this._fieldKey === "PARAMS" ? this._textBoxDoc[this._fieldKey] : ""); + const fval = dashVal instanceof List ? dashVal.join(this.multiValueDelimeter) : StrCast(dashVal).startsWith(":=") || dashVal === "" ? Doc.Layout(this._textBoxDoc)[this._fieldKey] : dashVal; const boolVal = Cast(fval, "boolean", null); const strVal = Field.toString(fval as Field) || ""; @@ -151,7 +155,10 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna } else if (nodeText.startsWith("=:=")) { Doc.Layout(this._textBoxDoc)[this._fieldKey] = ComputedField.MakeFunction(nodeText.substring(3)); } else { - this._dashDoc![this._fieldKey] = newText; + const splits = newText.split(this.multiValueDelimeter); + if (this._fieldKey !== "PARAMS" || !this._textBoxDoc[this._fieldKey] || this._dashDoc?.PARAMS) { + this._dashDoc![this._fieldKey] = splits.length > 1 ? new List<string>(splits) : newText; + } } }); } @@ -192,9 +199,10 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna width: this.props.width, height: this.props.height, }}> - <span className="dashFieldView-labelSpan" title="click to see related tags" onPointerDown={this.onPointerDownLabelSpan}> - {this._fieldKey} - </span> + {this.props.hideKey ? (null) : + <span className="dashFieldView-labelSpan" title="click to see related tags" onPointerDown={this.onPointerDownLabelSpan}> + {this._fieldKey} + </span>} <div className="dashFieldView-fieldSpan"> {this.fieldValueContent} diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 658a55f51..5e33e7e70 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -12,18 +12,18 @@ import { Fragment, Mark, Node, Slice } from "prosemirror-model"; import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from "prosemirror-state"; import { ReplaceStep } from 'prosemirror-transform'; import { EditorView } from "prosemirror-view"; -import { DateField } from '../../../../new_fields/DateField'; -import { DataSym, Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym } from "../../../../new_fields/Doc"; -import { documentSchema } from '../../../../new_fields/documentSchemas'; -import { Id } from '../../../../new_fields/FieldSymbols'; -import { InkTool } from '../../../../new_fields/InkField'; -import { PrefetchProxy } from '../../../../new_fields/Proxy'; -import { RichTextField } from "../../../../new_fields/RichTextField"; -import { RichTextUtils } from '../../../../new_fields/RichTextUtils'; -import { createSchema, makeInterface } from "../../../../new_fields/Schema"; -import { Cast, DateCast, NumCast, StrCast } from "../../../../new_fields/Types"; -import { TraceMobx } from '../../../../new_fields/util'; -import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnOne, returnZero, Utils } from '../../../../Utils'; +import { DateField } from '../../../../fields/DateField'; +import { DataSym, Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym } from "../../../../fields/Doc"; +import { documentSchema } from '../../../../fields/documentSchemas'; +import { Id } from '../../../../fields/FieldSymbols'; +import { InkTool } from '../../../../fields/InkField'; +import { PrefetchProxy } from '../../../../fields/Proxy'; +import { RichTextField } from "../../../../fields/RichTextField"; +import { RichTextUtils } from '../../../../fields/RichTextUtils'; +import { createSchema, makeInterface } from "../../../../fields/Schema"; +import { Cast, DateCast, NumCast, StrCast } from "../../../../fields/Types"; +import { TraceMobx } 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"; import { Docs, DocUtils } from '../../../documents/Documents'; @@ -58,7 +58,8 @@ import { FieldView, FieldViewProps } from "../FieldView"; import "./FormattedTextBox.scss"; import { FormattedTextBoxComment, formattedTextBoxCommentPlugin } from './FormattedTextBoxComment'; import React = require("react"); -import { ScriptField } from '../../../../new_fields/ScriptField'; +import { ScriptField } from '../../../../fields/ScriptField'; +import GoogleAuthenticationManager from '../../../apis/GoogleAuthenticationManager'; library.add(faEdit); library.add(faSmile, faTextHeight, faUpload); @@ -71,7 +72,7 @@ export interface FormattedTextBoxProps { } const richTextSchema = createSchema({ - documentText: "string" + documentText: "string", }); export const GoogleRef = "googleDocId"; @@ -92,9 +93,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp private _editorView: Opt<EditorView>; private _applyingChange: boolean = false; private _searchIndex = 0; - private _sidebarMovement = 0; - private _lastX = 0; - private _lastY = 0; private _undoTyping?: UndoManager.Batch; private _disposers: { [name: string]: IReactionDisposer } = {}; private dropDisposer?: DragManager.DragDropDisposer; @@ -160,7 +158,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp 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.props.Document }, { doc: this.dataDoc[key] as Doc }, "link to named target", id); + else DocUtils.MakeLink({ doc: this.rootDoc }, { doc: this.dataDoc[key] as Doc }, "link to named target", id); }); }); }); @@ -198,15 +196,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const tsel = this._editorView.state.selection.$from; tsel.marks().filter(m => m.type === this._editorView!.state.schema.marks.user_mark).map(m => AudioBox.SetScrubTime(Math.max(0, m.attrs.modified * 1000))); const curText = state.doc.textBetween(0, state.doc.content.size, " \n"); - const curTemp = Cast(this.props.Document[this.props.fieldKey + "-textTemplate"], RichTextField); // the actual text in the text box + const curTemp = Cast(this.layoutDoc[this.props.fieldKey + "-textTemplate"], RichTextField); // the actual text in the text box 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._applyingChange && json.replace(/"selection":.*/, "") !== curProto?.Data.replace(/"selection":.*/, "")) { this._applyingChange = true; this.dataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now())); - if ((!curTemp && !curProto) || curText) { // 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 (curText !== curLayout?.Text) { + 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) { + !curText && tx.storedMarks?.map(m => m.type.name === "pFontSize" && (Doc.UserDoc().fontSize = this.layoutDoc._fontSize = m.attrs.fontSize)); 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 } @@ -240,7 +239,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp res.map(r => r.map(h => flattened.push(h))); const lastSel = Math.min(flattened.length - 1, this._searchIndex); this._searchIndex = ++this._searchIndex > flattened.length - 1 ? 0 : this._searchIndex; - const alink = DocUtils.MakeLink({ doc: this.props.Document }, { doc: target }, "automatic")!; + const alink = DocUtils.MakeLink({ doc: this.rootDoc }, { doc: target }, "automatic")!; const link = this._editorView.state.schema.marks.link.create({ href: Utils.prepend("/doc/" + alink[Id]), title: "a link", location: location, linkId: alink[Id], targetId: target[Id] @@ -279,7 +278,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp protected createDropTarget = (ele: HTMLDivElement) => { this.ProseRef = ele; this.dropDisposer?.(); - ele && (this.dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.props.Document)); + ele && (this.dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.layoutDoc)); } @undoBatch @@ -398,26 +397,20 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } sidebarDown = (e: React.PointerEvent) => { - this._lastX = e.clientX; - this._lastY = e.clientY; - this._sidebarMovement = 0; - document.addEventListener("pointermove", this.sidebarMove); - document.addEventListener("pointerup", this.sidebarUp); - e.stopPropagation(); - e.preventDefault(); // prevents text from being selected during drag + setupMoveUpEvents(this, e, this.sidebarMove, emptyFunction, + () => (this.layoutDoc._sidebarWidthPercent = StrCast(this.layoutDoc._sidebarWidthPercent, "0%") === "0%" ? "25%" : "0%")); } - sidebarMove = (e: PointerEvent) => { + sidebarMove = (e: PointerEvent, down: number[], delta: number[]) => { const bounds = this.CurrentDiv.getBoundingClientRect(); - this._sidebarMovement += Math.sqrt((e.clientX - this._lastX) * (e.clientX - this._lastX) + (e.clientY - this._lastY) * (e.clientY - this._lastY)); - this.props.Document.sidebarWidthPercent = "" + 100 * (1 - (e.clientX - bounds.left) / bounds.width) + "%"; + this.layoutDoc._sidebarWidthPercent = "" + 100 * (1 - (e.clientX - bounds.left) / bounds.width) + "%"; + return false; } - sidebarUp = (e: PointerEvent) => { - document.removeEventListener("pointermove", this.sidebarMove); - document.removeEventListener("pointerup", this.sidebarUp); + @undoBatch + @action + toggleNativeDimensions = () => { + Doc.toggleNativeDimensions(this.layoutDoc, this.props.ContentScaling(), this.props.NativeWidth(), this.props.NativeHeight()); } - toggleSidebar = () => this._sidebarMovement < 5 && (this.props.Document.sidebarWidthPercent = StrCast(this.props.Document.sidebarWidthPercent, "0%") === "0%" ? "25%" : "0%"); - public static get DefaultLayout(): Doc | string | undefined { return Cast(Doc.UserDoc().defaultTextLayout, Doc, null) || StrCast(Doc.UserDoc().defaultTextLayout, null); } @@ -425,12 +418,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const cm = ContextMenu.Instance; const funcs: ContextMenuProps[] = []; - this.rootDoc.isTemplateDoc && funcs.push({ description: "Make Default Layout", event: async () => Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.props.Document), icon: "eye" }); - funcs.push({ description: "Reset Default Layout", event: () => Doc.UserDoc().defaultTextLayout = undefined, icon: "eye" }); + this.rootDoc.isTemplateDoc && funcs.push({ description: "Make Default Layout", event: async () => Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.rootDoc), icon: "eye" }); !this.layoutDoc.isTemplateDoc && funcs.push({ - description: "Make Template", event: () => { + description: "Convert to use as a style", event: () => { this.rootDoc.isTemplateDoc = makeTemplate(this.rootDoc); - Doc.AddDocToList(Cast(Doc.UserDoc()["template-notes"], Doc, null), "data", this.props.Document); + Doc.AddDocToList(Cast(Doc.UserDoc()["template-notes"], Doc, null), "data", this.rootDoc); }, icon: "eye" }); this.layoutDoc.isTemplateDoc && funcs.push({ @@ -450,10 +442,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp Doc.AddDocToList(Cast(Doc.UserDoc()["template-notes"], Doc, null), "data", this.rootDoc); }, icon: "eye" }); - funcs.push({ description: "Toggle Single Line", event: () => this.props.Document._singleLine = !this.props.Document._singleLine, icon: "expand-arrows-alt" }); - funcs.push({ description: "Toggle Sidebar", event: () => this.props.Document._showSidebar = !this.props.Document._showSidebar, icon: "expand-arrows-alt" }); - funcs.push({ description: "Toggle Dictation Icon", event: () => this.props.Document._showAudio = !this.props.Document._showAudio, icon: "expand-arrows-alt" }); - funcs.push({ description: "Toggle Menubar", event: () => this.toggleMenubar(), icon: "expand-arrows-alt" }); + //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 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" }); + + funcs.push({ description: "UI Controls...", subitems: uicontrols, icon: "asterisk" }); const highlighting: ContextMenuProps[] = []; ["My Text", "Text from Others", "Todo Items", "Important Items", "Ignore Items", "Disagree Items", "By Recent Minute", "By Recent Hour"].forEach(option => @@ -479,26 +477,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp DocListCast(noteTypesDoc?.data).forEach(note => { changeItems.push({ description: StrCast(note.title), event: undoBatch(() => { - Doc.setNativeView(this.props.Document); + Doc.setNativeView(this.rootDoc); Doc.makeCustomViewClicked(this.rootDoc, Docs.Create.TreeDocument, StrCast(note.title), note); }), icon: "eye" }); }); changeItems.push({ description: "FreeForm", event: undoBatch(() => Doc.makeCustomViewClicked(this.rootDoc, Docs.Create.FreeformDocument, "freeform"), "change view"), icon: "eye" }); !change && cm.addItem({ description: "Change Perspective...", subitems: changeItems, icon: "external-link-alt" }); - - const open = cm.findByDescription("Add a Perspective..."); - const openItems: ContextMenuProps[] = open && "subitems" in open ? open.subitems : []; - - openItems.push({ - description: "FreeForm", event: undoBatch(() => { - const alias = Doc.MakeAlias(this.rootDoc); - Doc.makeCustomViewClicked(alias, Docs.Create.FreeformDocument, "freeform"); - this.props.addDocTab(alias, "onRight"); - }), icon: "eye" - }); - !open && cm.addItem({ description: "Add a Perspective...", subitems: openItems, icon: "external-link-alt" }); - } recordDictation = () => { @@ -516,7 +501,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp @action toggleMenubar = () => { - this.props.Document._chromeStatus = this.props.Document._chromeStatus === "disabled" ? "enabled" : "disabled"; + this.layoutDoc._chromeStatus = this.layoutDoc._chromeStatus === "disabled" ? "enabled" : "disabled"; } recordBullet = async () => { @@ -629,10 +614,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp ); this._disposers.editorState = reaction( () => { - if (this.dataDoc[this.props.fieldKey + "-noTemplate"] || !this.props.Document[this.props.fieldKey + "-textTemplate"]) { + if (this.dataDoc[this.props.fieldKey + "-noTemplate"] || !this.layoutDoc[this.props.fieldKey + "-textTemplate"]) { return Cast(this.dataDoc[this.props.fieldKey], RichTextField, null)?.Data; } - return Cast(this.props.Document[this.props.fieldKey + "-textTemplate"], RichTextField, null)?.Data; + return Cast(this.layoutDoc[this.props.fieldKey + "-textTemplate"], RichTextField, null)?.Data; }, incomingValue => { if (incomingValue !== undefined && this._editorView && !this._applyingChange) { @@ -661,9 +646,17 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } } ); - this._disposers.height = reaction( + this._disposers.autoHeight = reaction( () => [this.layoutDoc[WidthSym](), this.layoutDoc._autoHeight], - () => this.tryUpdateHeight() + () => setTimeout(() => this.tryUpdateHeight(), 0) + ); + this._disposers.height = reaction( + () => this.layoutDoc[HeightSym](), + action(height => { + if (height <= 20 && height < NumCast(this.layoutDoc._delayAutoHeight, 20)) { + this.layoutDoc._delayAutoHeight = height; + } + }) ); this.setupEditor(this.config, this.props.fieldKey); @@ -689,7 +682,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const nodes: Node[] = []; frag.forEach((node, index) => { const examinedNode = findLinkNode(node, editor); - if (examinedNode && examinedNode.textContent) { + if (examinedNode?.textContent) { nodes.push(examinedNode); start += index; } @@ -706,7 +699,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp return linkIndex !== -1 && scrollToLinkID === marks[linkIndex].attrs.href.replace(/.*\/doc\//, "") ? node : undefined; }; - let start = -1; + let start = 0; if (this._editorView && scrollToLinkID) { const editor = this._editorView; const ret = findLinkFrag(editor.state.doc.content, editor); @@ -727,7 +720,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp }, { fireImmediately: true } ); - this._disposers.scroll = reaction(() => NumCast(this.props.Document.scrollPos), + this._disposers.scroll = reaction(() => NumCast(this.layoutDoc.scrollPos), pos => this._scrollRef.current && this._scrollRef.current.scrollTo({ top: pos }), { fireImmediately: true } ); @@ -784,7 +777,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp let pullSuccess = false; if (exportState !== undefined) { pullSuccess = true; - dataDoc.data = new RichTextField(JSON.stringify(exportState.state.toJSON())); + dataDoc[this.props.fieldKey] = new RichTextField(JSON.stringify(exportState.state.toJSON())); setTimeout(() => { if (this._editorView) { const state = this._editorView.state; @@ -846,7 +839,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp targetAnnotations?.push(pdfRegion); }); - const link = DocUtils.MakeLink({ doc: this.props.Document }, { doc: pdfRegion }, "PDF pasted"); + const link = DocUtils.MakeLink({ doc: this.rootDoc }, { doc: pdfRegion }, "PDF pasted"); if (link) { cbe.clipboardData!.setData("dash/linkDoc", link[Id]); const linkId = link[Id]; @@ -883,8 +876,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp private setupEditor(config: any, fieldKey: string) { const curText = Cast(this.dataDoc[this.props.fieldKey], RichTextField, null); - const useTemplate = !curText?.Text && this.props.Document[this.props.fieldKey + "-textTemplate"]; - const rtfField = Cast((useTemplate && this.props.Document[this.props.fieldKey + "-textTemplate"]) || this.dataDoc[fieldKey], RichTextField); + const useTemplate = !curText?.Text && this.layoutDoc[this.props.fieldKey + "-textTemplate"]; + const rtfField = Cast((useTemplate && this.layoutDoc[this.props.fieldKey + "-textTemplate"]) || this.dataDoc[fieldKey], RichTextField); if (this.ProseRef) { const self = this; this._editorView?.destroy(); @@ -980,9 +973,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp if (this.props.onClick && e.button === 0 && !this.props.isSelected(false)) { e.preventDefault(); } - if (e.button === 0 && this.active(true) && !e.altKey && !e.ctrlKey && !e.metaKey) { - if (e.clientX < this.ProseRef!.getBoundingClientRect().right) { // don't stop propagation if clicking in the sidebar - e.stopPropagation(); + if (e.button === 0 && this.props.isSelected(true) && !e.altKey && !e.ctrlKey && !e.metaKey) { + if (e.clientX < this.ProseRef!.getBoundingClientRect().right) { // stop propagation if not in sidebar + e.stopPropagation(); // if the text box is selected, then it consumes all down events } } if (e.button === 2 || (e.button === 0 && e.ctrlKey)) { @@ -1020,7 +1013,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp // jump rich text menu to this textbox const bounds = this._ref.current?.getBoundingClientRect(); - if (bounds && this.props.Document._chromeStatus !== "disabled") { + if (bounds && this.layoutDoc._chromeStatus !== "disabled") { const x = Math.min(Math.max(bounds.left, 0), window.innerWidth - RichTextMenu.Instance.width); let y = Math.min(Math.max(0, bounds.top - RichTextMenu.Instance.height - 50), window.innerHeight - RichTextMenu.Instance.height); if (coords && coords.left > x && coords.left < x + RichTextMenu.Instance.width && coords.top > y && coords.top < y + RichTextMenu.Instance.height + 50) { @@ -1056,43 +1049,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); return; } (e.nativeEvent as any).formattedHandled = true; - // if (e.button === 0 && ((!this.props.isSelected(true) && !e.ctrlKey) || (this.props.isSelected(true) && e.ctrlKey)) && !e.metaKey && e.target) { - // let href = (e.target as any).href; - // let location: string; - // if ((e.target as any).attributes.location) { - // location = (e.target as any).attributes.location.value; - // } - // let pcords = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY }); - // let node = pcords && this._editorView!.state.doc.nodeAt(pcords.pos); - // if (node) { - // let link = node.marks.find(m => m.type === this._editorView!.state.schema.marks.link); - // if (link && !(link.attrs.docref && link.attrs.title)) { // bcz: getting hacky. this indicates that we clicked on a PDF excerpt quotation. In this case, we don't want to follow the link (we follow only the actual hyperlink for the quotation which is handled above). - // href = link && link.attrs.href; - // location = link && link.attrs.location; - // } - // } - // if (href) { - // if (href.indexOf(Utils.prepend("/doc/")) === 0) { - // let linkClicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0]; - // if (linkClicked) { - // DocServer.GetRefField(linkClicked).then(async linkDoc => { - // (linkDoc instanceof Doc) && - // DocumentManager.Instance.FollowLink(linkDoc, this.props.Document, document => this.props.addDocTab(document, location ? location : "inTab"), false); - // }); - // } - // } else { - // let webDoc = Docs.Create.WebDocument(href, { x: NumCast(this.layoutDoc.x, 0) + NumCast(this.layoutDoc.width, 0), y: NumCast(this.layoutDoc.y) }); - // this.props.addDocument && this.props.addDocument(webDoc); - // } - // e.stopPropagation(); - // e.preventDefault(); - // } - // } if (Math.abs(e.clientX - this._downX) < 4 && Math.abs(e.clientX - this._downX) < 4) { this.props.select(e.ctrlKey); this.hitBulletTargets(e.clientX, e.clientY, e.shiftKey, false); } + if (this.props.isSelected(true)) { // if text box is selected, then it consumes all click events + e.stopPropagation(); + } } // this hackiness handles clicking on the list item bullets to do expand/collapse. the bullets are ::before pseudo elements so there's no real way to hit test against them. @@ -1209,13 +1173,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } onscrolled = (ev: React.UIEvent) => { - this.props.Document.scrollPos = this._scrollRef.current!.scrollTop; + this.layoutDoc.scrollPos = this._scrollRef.current!.scrollTop; } @action tryUpdateHeight(limitHeight?: number) { let scrollHeight = this._ref.current?.scrollHeight; - if (this.layoutDoc._autoHeight && scrollHeight && - getComputedStyle(this._ref.current!.parentElement!).top === "0px") { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation + if (this.layoutDoc._autoHeight && !this.props.ignoreAutoHeight && scrollHeight) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation + scrollHeight = scrollHeight * NumCast(this.layoutDoc.scale, 1); if (limitHeight && scrollHeight > limitHeight) { scrollHeight = limitHeight; this.layoutDoc.limitHeight = undefined; @@ -1225,7 +1189,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const dh = NumCast(this.rootDoc._height, 0); const newHeight = Math.max(10, (nh ? dh / nh * scrollHeight : scrollHeight) + (this.props.ChromeHeight ? this.props.ChromeHeight() : 0)); if (Math.abs(newHeight - dh) > 1) { // bcz: Argh! without this, we get into a React crash if the same document is opened in a freeform view and in the treeview. no idea why, but after dragging the freeform document, selecting it, and selecting text, it will compute to 1 pixel higher than the treeview which causes a cycle - if (this.rootDoc !== this.layoutDoc && !this.layoutDoc.resolvedDataDoc) { + if (this.rootDoc !== this.layoutDoc.doc && !this.layoutDoc.resolvedDataDoc) { // if we have a template that hasn't been resolved yet, we can't set the height or we'd be setting it on the unresolved template. So set a timeout and hope its arrived... console.log("Delayed height adjustment..."); setTimeout(() => { @@ -1240,21 +1204,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } } - @computed get sidebarWidthPercent() { return StrCast(this.props.Document.sidebarWidthPercent, "0%"); } + @computed get sidebarWidthPercent() { return StrCast(this.layoutDoc._sidebarWidthPercent, "0%"); } sidebarWidth = () => Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100 * this.props.PanelWidth(); sidebarScreenToLocal = () => this.props.ScreenToLocalTransform().translate(-(this.props.PanelWidth() - this.sidebarWidth()), 0); @computed get sidebarColor() { return StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], "transparent")); } render() { TraceMobx(); - const style: { [key: string]: any } = {}; - const divKeys = ["width", "height", "background"]; - const replacer = (match: any, expr: string, offset: any, string: any) => { // bcz: this executes a script to convert a propery expression string: { script } into a value - return ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name })?.script.run({ self: this.rootDoc, this: this.layoutDoc }).result as string || ""; - }; - divKeys.map((prop: string) => { - const p = (this.props as any)[prop] as string; - p && (style[prop] = p?.replace(/{([^.'][^}']+)}/g, replacer)); - }); + const scale = this.props.ContentScaling() * NumCast(this.layoutDoc.scale, 1); const rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : ""; const interactive = InkingControl.Instance.selectedTool || this.layoutDoc.isBackground; if (this.props.isSelected()) { @@ -1263,83 +1219,90 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp FormattedTextBoxComment.Hide(); } return ( - - <div className={`formattedTextBox-cont`} ref={this._ref} - style={{ - height: this.props.height ? this.props.height : this.layoutDoc._autoHeight && this.props.renderDepth ? "max-content" : `calc(100% - ${this.props.ChromeHeight?.() || 0}px`, - background: this.props.background ? this.props.background : StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], this.props.hideOnLeave ? "rgba(0,0,0 ,0.4)" : ""), - 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 ? "none" : undefined, - fontSize: Cast(this.layoutDoc._fontSize, "number", null), - fontFamily: StrCast(this.layoutDoc._fontFamily, "inherit"), - ...style - }} - onContextMenu={this.specificContextMenu} - onKeyDown={this.onKeyPress} - onFocus={this.onFocused} - onClick={this.onClick} - onPointerMove={e => this.hitBulletTargets(e.clientX, e.clientY, e.shiftKey, true)} - onBlur={this.onBlur} - onPointerUp={this.onPointerUp} - onPointerDown={this.onPointerDown} - onMouseUp={this.onMouseUp} - onWheel={this.onPointerWheel} - onPointerEnter={action(() => this._entered = true)} - onPointerLeave={action((e: React.PointerEvent<HTMLDivElement>) => { - this._entered = false; - const target = document.elementFromPoint(e.nativeEvent.x, e.nativeEvent.y); - for (let child: any = target; child; child = child?.parentElement) { - if (child === this._ref.current!) { - this._entered = true; + <div className={"formattedTextBox-cont"} style={{ + transform: `scale(${scale})`, + transformOrigin: "top left", + width: `${100 / scale}%`, + height: `${100 / scale}%`, + ...this.styleFromLayoutString(scale) + }}> + <div className={`formattedTextBox-cont`} ref={this._ref} + style={{ + width: "100%", + height: this.props.height ? this.props.height : this.layoutDoc._autoHeight && this.props.renderDepth ? "max-content" : `calc(100% - ${this.props.ChromeHeight?.() || 0}px`, + background: this.props.background ? this.props.background : StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], this.props.hideOnLeave ? "rgba(0,0,0 ,0.4)" : ""), + 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 ? "none" : undefined, + fontSize: Cast(this.layoutDoc._fontSize, "number", null), + fontFamily: StrCast(this.layoutDoc._fontFamily, "inherit") + }} + onContextMenu={this.specificContextMenu} + onKeyDown={this.onKeyPress} + onFocus={this.onFocused} + onClick={this.onClick} + onPointerMove={e => this.hitBulletTargets(e.clientX, e.clientY, e.shiftKey, true)} + onBlur={this.onBlur} + onPointerUp={this.onPointerUp} + onPointerDown={this.onPointerDown} + onMouseUp={this.onMouseUp} + onWheel={this.onPointerWheel} + onPointerEnter={action(() => this._entered = true)} + onPointerLeave={action((e: React.PointerEvent<HTMLDivElement>) => { + this._entered = false; + const target = document.elementFromPoint(e.nativeEvent.x, e.nativeEvent.y); + for (let child: any = target; child; child = child?.parentElement) { + if (child === this._ref.current!) { + this._entered = true; + } } - } - })} - > - <div className={`formattedTextBox-outer`} style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, }} onScroll={this.onscrolled} ref={this._scrollRef}> - <div className={`formattedTextBox-inner${rounded}`} ref={this.createDropTarget} - style={{ - padding: `${NumCast(this.layoutDoc._yMargin, this.props.yMargin || 0)}px ${NumCast(this.layoutDoc._xMargin, this.props.xMargin || 0)}px`, - pointerEvents: ((this.layoutDoc.isLinkButton || this.props.onClick) && !this.props.isSelected()) ? "none" : undefined - }} /> + })} + > + <div className={`formattedTextBox-outer`} style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, }} onScroll={this.onscrolled} ref={this._scrollRef}> + <div className={`formattedTextBox-inner${rounded}`} ref={this.createDropTarget} + style={{ + padding: `${NumCast(this.layoutDoc._yMargin, this.props.yMargin || 0)}px ${NumCast(this.layoutDoc._xMargin, this.props.xMargin || 0)}px`, + pointerEvents: ((this.layoutDoc.isLinkButton || this.props.onClick) && !this.props.isSelected()) ? "none" : undefined + }} /> + </div> + {!this.layoutDoc._showSidebar ? (null) : this.sidebarWidthPercent === "0%" ? + <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} /> : + <div className={"formattedTextBox-sidebar" + (InkingControl.Instance.selectedTool !== InkTool.None ? "-inking" : "")} + style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> + <CollectionFreeFormView {...this.props} + PanelHeight={this.props.PanelHeight} + PanelWidth={this.sidebarWidth} + NativeHeight={returnZero} + NativeWidth={returnZero} + annotationsKey={this.annotationKey} + isAnnotationOverlay={false} + focus={this.props.focus} + isSelected={this.props.isSelected} + select={emptyFunction} + active={this.annotationsActive} + ContentScaling={returnOne} + whenActiveChanged={this.whenActiveChanged} + removeDocument={this.removeDocument} + moveDocument={this.moveDocument} + addDocument={this.addDocument} + CollectionView={undefined} + ScreenToLocalTransform={this.sidebarScreenToLocal} + renderDepth={this.props.renderDepth + 1} + ContainingCollectionDoc={this.props.ContainingCollectionDoc}> + </CollectionFreeFormView> + <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} /> + </div>} + {!this.layoutDoc._showAudio ? (null) : + <div className="formattedTextBox-dictation" + onPointerDown={e => { + runInAction(() => this._recording = !this._recording); + setTimeout(() => this._editorView!.focus(), 500); + e.stopPropagation(); + }} > + <FontAwesomeIcon className="formattedTExtBox-audioFont" + style={{ color: this._recording ? "red" : "blue", opacity: this._recording ? 1 : 0.5, display: this.props.isSelected() ? "" : "none" }} icon={"microphone"} size="sm" /> + </div>} </div> - {!this.props.Document._showSidebar ? (null) : this.sidebarWidthPercent === "0%" ? - <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} onClick={e => this.toggleSidebar()} /> : - <div className={"formattedTextBox-sidebar" + (InkingControl.Instance.selectedTool !== InkTool.None ? "-inking" : "")} - style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> - <CollectionFreeFormView {...this.props} - PanelHeight={this.props.PanelHeight} - PanelWidth={this.sidebarWidth} - NativeHeight={returnZero} - NativeWidth={returnZero} - annotationsKey={this.annotationKey} - isAnnotationOverlay={false} - focus={this.props.focus} - isSelected={this.props.isSelected} - select={emptyFunction} - active={this.annotationsActive} - ContentScaling={returnOne} - whenActiveChanged={this.whenActiveChanged} - removeDocument={this.removeDocument} - moveDocument={this.moveDocument} - addDocument={this.addDocument} - CollectionView={undefined} - ScreenToLocalTransform={this.sidebarScreenToLocal} - renderDepth={this.props.renderDepth + 1} - ContainingCollectionDoc={this.props.ContainingCollectionDoc}> - </CollectionFreeFormView> - <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} onClick={e => this.toggleSidebar()} /> - </div>} - {!this.props.Document._showAudio ? (null) : - <div className="formattedTextBox-dictation" - onPointerDown={e => { - runInAction(() => this._recording = !this._recording); - setTimeout(() => this._editorView!.focus(), 500); - e.stopPropagation(); - }} > - <FontAwesomeIcon className="formattedTExtBox-audioFont" - style={{ color: this._recording ? "red" : "blue", opacity: this._recording ? 1 : 0.5, display: this.props.isSelected() ? "" : "none" }} icon={"microphone"} size="sm" /> - </div>} </div> ); } diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx index 9ad5aafb8..d47ae63af 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx @@ -2,8 +2,8 @@ import { Mark, ResolvedPos } from "prosemirror-model"; import { EditorState, Plugin } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import * as ReactDOM from 'react-dom'; -import { Doc, DocCastAsync } from "../../../../new_fields/Doc"; -import { Cast, FieldValue, NumCast } from "../../../../new_fields/Types"; +import { Doc, DocCastAsync } from "../../../../fields/Doc"; +import { Cast, FieldValue, NumCast } from "../../../../fields/Types"; import { emptyFunction, returnEmptyString, returnFalse, Utils, emptyPath, returnZero, returnOne } from "../../../../Utils"; import { DocServer } from "../../../DocServer"; import { DocumentManager } from "../../../util/DocumentManager"; @@ -91,7 +91,7 @@ export class FormattedTextBoxComment { (doc: Doc, followLinkLocation: string) => textBox.props.addDocTab(doc, e.ctrlKey ? "inTab" : followLinkLocation)); } } else if (textBox && (FormattedTextBoxComment.tooltipText as any).href) { - textBox.props.addDocTab(Docs.Create.WebDocument((FormattedTextBoxComment.tooltipText as any).href, { title: (FormattedTextBoxComment.tooltipText as any).href, _width: 200, _height: 400 }), "onRight"); + textBox.props.addDocTab(Docs.Create.WebDocument((FormattedTextBoxComment.tooltipText as any).href, { title: (FormattedTextBoxComment.tooltipText as any).href, _width: 200, _height: 400, UseCors: true }), "onRight"); } keep && textBox && FormattedTextBoxComment.start !== undefined && textBox.adoptAnnotation( FormattedTextBoxComment.start, FormattedTextBoxComment.end, FormattedTextBoxComment.mark); @@ -211,8 +211,8 @@ export class FormattedTextBoxComment { NativeWidth={returnZero} NativeHeight={returnZero} />, FormattedTextBoxComment.tooltipText); - FormattedTextBoxComment.tooltip.style.width = NumCast(target.width) ? `${NumCast(target.width)}` : "100%"; - FormattedTextBoxComment.tooltip.style.height = NumCast(target.height) ? `${NumCast(target.height)}` : "100%"; + FormattedTextBoxComment.tooltip.style.width = NumCast(target._width) ? `${NumCast(target._width)}` : "100%"; + FormattedTextBoxComment.tooltip.style.height = NumCast(target._height) ? `${NumCast(target._height)}` : "100%"; } // let ext = (target && target.type !== DocumentType.PDFANNO && Doc.fieldExtensionDoc(target, "data")) || target; // try guessing that the target doc's data is in the 'data' field. probably need an 'overviewLayout' and then just display the target Document .... // let text = ext && StrCast(ext.text); diff --git a/src/client/views/nodes/formattedText/ImageResizeView.tsx b/src/client/views/nodes/formattedText/ImageResizeView.tsx index 8f98da0fd..401ecd7e6 100644 --- a/src/client/views/nodes/formattedText/ImageResizeView.tsx +++ b/src/client/views/nodes/formattedText/ImageResizeView.tsx @@ -1,5 +1,5 @@ import { NodeSelection } from "prosemirror-state"; -import { Doc } from "../../../../new_fields/Doc"; +import { Doc } from "../../../../fields/Doc"; import { DocServer } from "../../../DocServer"; import { DocumentManager } from "../../../util/DocumentManager"; import React = require("react"); diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts index a0b02880e..2f7d23021 100644 --- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts +++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts @@ -7,10 +7,10 @@ import { splitListItem, wrapInList, } from "prosemirror-schema-list"; import { EditorState, Transaction, TextSelection } from "prosemirror-state"; import { SelectionManager } from "../../../util/SelectionManager"; import { Docs } from "../../../documents/Documents"; -import { NumCast, BoolCast, Cast, StrCast } from "../../../../new_fields/Types"; -import { Doc } from "../../../../new_fields/Doc"; +import { NumCast, BoolCast, Cast, StrCast } from "../../../../fields/Types"; +import { Doc } from "../../../../fields/Doc"; import { FormattedTextBox } from "./FormattedTextBox"; -import { Id } from "../../../../new_fields/FieldSymbols"; +import { Id } from "../../../../fields/FieldSymbols"; const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false; diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index cc04e0d6d..fd1b26208 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -11,14 +11,14 @@ import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; import { faBold, faItalic, faChevronLeft, faUnderline, faStrikethrough, faSubscript, faSuperscript, faIndent, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink, faPaintRoller, faSleigh } from "@fortawesome/free-solid-svg-icons"; import { updateBullets } from "./ProsemirrorExampleTransfer"; import { FieldViewProps } from "../FieldView"; -import { Cast, StrCast } from "../../../../new_fields/Types"; +import { Cast, StrCast } from "../../../../fields/Types"; import { FormattedTextBoxProps } from "./FormattedTextBox"; import { unimplementedFunction, Utils } from "../../../../Utils"; import { wrapInList } from "prosemirror-schema-list"; -import { PastelSchemaPalette, DarkPastelSchemaPalette } from '../../../../new_fields/SchemaHeaderField'; +import { PastelSchemaPalette, DarkPastelSchemaPalette } from '../../../../fields/SchemaHeaderField'; import "./RichTextMenu.scss"; import { DocServer } from "../../../DocServer"; -import { Doc } from "../../../../new_fields/Doc"; +import { Doc } from "../../../../fields/Doc"; import { SelectionManager } from "../../../util/SelectionManager"; import { LinkManager } from "../../../util/LinkManager"; const { toggleMark, setBlockType } = require("prosemirror-commands"); @@ -197,9 +197,10 @@ export default class RichTextMenu extends AntimodeMenu { } else { toggleMark(mark.type, mark.attrs)(state, (tx: any) => { const { from, $from, to, empty } = tx.selection; - if (!tx.doc.rangeHasMark(from, to, mark.type)) { - toggleMark(mark.type, mark.attrs)({ tr: tx, doc: tx.doc, selection: tx.selection, storedMarks: tx.storedMarks }, dispatch); - } else dispatch(tx); + // if (!tx.doc.rangeHasMark(from, to, mark.type)) { + // toggleMark(mark.type, mark.attrs)({ tr: tx, doc: tx.doc, selection: tx.selection, storedMarks: tx.storedMarks }, dispatch); + // } else + dispatch(tx); }); } } diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index d619bc4a0..fbd6c87bb 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -1,9 +1,9 @@ import { ellipsis, emDash, InputRule, smartQuotes, textblockTypeInputRule } from "prosemirror-inputrules"; import { NodeSelection, TextSelection } from "prosemirror-state"; -import { DataSym, Doc } from "../../../../new_fields/Doc"; -import { Id } from "../../../../new_fields/FieldSymbols"; -import { ComputedField } from "../../../../new_fields/ScriptField"; -import { Cast, NumCast } from "../../../../new_fields/Types"; +import { DataSym, Doc } from "../../../../fields/Doc"; +import { Id } from "../../../../fields/FieldSymbols"; +import { ComputedField } from "../../../../fields/ScriptField"; +import { Cast, NumCast } from "../../../../fields/Types"; import { returnFalse, Utils } from "../../../../Utils"; import { DocServer } from "../../../DocServer"; import { Docs, DocUtils } from "../../../documents/Documents"; @@ -11,6 +11,7 @@ import { FormattedTextBox } from "./FormattedTextBox"; import { wrappingInputRule } from "./prosemirrorPatches"; import RichTextMenu from "./RichTextMenu"; import { schema } from "./schema_rts"; +import { List } from "../../../../fields/List"; export class RichTextRules { public Document: Doc; @@ -64,11 +65,12 @@ export class RichTextRules { // create an inline view of a tag stored under the '#' field new InputRule( - new RegExp(/#([a-zA-Z_\-]+[a-zA-Z_\-0-9]*)\s$/), + new RegExp(/#([a-zA-Z_\-]+[a-zA-Z_;\-0-9]*)\s$/), (state, match, start, end) => { const tag = match[1]; if (!tag) return state.tr; - this.Document[DataSym]["#"] = tag; + const multiple = tag.split(";"); + this.Document[DataSym]["#"] = multiple.length > 1 ? new List(multiple) : tag; const fieldView = state.schema.nodes.dashField.create({ fieldKey: "#" }); return state.tr.deleteRange(start, end).insert(start, fieldView); }), diff --git a/src/client/views/nodes/formattedText/RichTextSchema.tsx b/src/client/views/nodes/formattedText/RichTextSchema.tsx index cdb7374f8..91280dea4 100644 --- a/src/client/views/nodes/formattedText/RichTextSchema.tsx +++ b/src/client/views/nodes/formattedText/RichTextSchema.tsx @@ -8,14 +8,14 @@ import { EditorState, NodeSelection, Plugin, TextSelection } from "prosemirror-s import { StepMap } from "prosemirror-transform"; import { EditorView } from "prosemirror-view"; import * as ReactDOM from 'react-dom'; -import { Doc, DocListCast, Field, HeightSym, WidthSym } from "../../../../new_fields/Doc"; -import { Id } from "../../../../new_fields/FieldSymbols"; -import { List } from "../../../../new_fields/List"; -import { ObjectField } from "../../../../new_fields/ObjectField"; -import { listSpec } from "../../../../new_fields/Schema"; -import { SchemaHeaderField } from "../../../../new_fields/SchemaHeaderField"; -import { ComputedField } from "../../../../new_fields/ScriptField"; -import { BoolCast, Cast, NumCast, StrCast, FieldValue } from "../../../../new_fields/Types"; +import { Doc, DocListCast, Field, HeightSym, WidthSym } from "../../../../fields/Doc"; +import { Id } from "../../../../fields/FieldSymbols"; +import { List } from "../../../../fields/List"; +import { ObjectField } from "../../../../fields/ObjectField"; +import { listSpec } from "../../../../fields/Schema"; +import { SchemaHeaderField } from "../../../../fields/SchemaHeaderField"; +import { ComputedField } from "../../../../fields/ScriptField"; +import { BoolCast, Cast, NumCast, StrCast, FieldValue } from "../../../../fields/Types"; import { emptyFunction, returnEmptyString, returnFalse, returnOne, Utils, returnZero } from "../../../../Utils"; import { DocServer } from "../../../DocServer"; import { Docs } from "../../../documents/Documents"; @@ -217,6 +217,7 @@ export class DashDocView { this._dashSpan.style.height = node.attrs.height; this._dashSpan.style.position = "absolute"; this._dashSpan.style.display = "inline-block"; + this._dashSpan.style.whiteSpace = "normal"; this._dashSpan.onpointerleave = () => { const ele = document.getElementById("DashDocCommentView-" + node.attrs.docid); if (ele) { diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts index 46bf481fb..ebaa23e99 100644 --- a/src/client/views/nodes/formattedText/marks_rts.ts +++ b/src/client/views/nodes/formattedText/marks_rts.ts @@ -1,6 +1,6 @@ import React = require("react"); import { DOMOutputSpecArray, Fragment, MarkSpec, Node, NodeSpec, Schema, Slice } from "prosemirror-model"; -import { Doc } from "../../../../new_fields/Doc"; +import { Doc } from "../../../../fields/Doc"; const emDOM: DOMOutputSpecArray = ["em", 0]; diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts index e7bcf444a..af39ef9c7 100644 --- a/src/client/views/nodes/formattedText/nodes_rts.ts +++ b/src/client/views/nodes/formattedText/nodes_rts.ts @@ -166,7 +166,8 @@ export const nodes: { [index: string]: NodeSpec } = { inline: true, attrs: { fieldKey: { default: "" }, - docid: { default: "" } + docid: { default: "" }, + hideKey: { default: false } }, group: "inline", draggable: false, diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx index 672d3adb8..cb6a15f36 100644 --- a/src/client/views/pdf/Annotation.tsx +++ b/src/client/views/pdf/Annotation.tsx @@ -1,10 +1,10 @@ import React = require("react"); import { action, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCast, HeightSym, WidthSym } from "../../../new_fields/Doc"; -import { Id } from "../../../new_fields/FieldSymbols"; -import { List } from "../../../new_fields/List"; -import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types"; +import { Doc, DocListCast, HeightSym, WidthSym } from "../../../fields/Doc"; +import { Id } from "../../../fields/FieldSymbols"; +import { List } from "../../../fields/List"; +import { Cast, FieldValue, NumCast, StrCast } from "../../../fields/Types"; import { DocumentManager } from "../../util/DocumentManager"; import PDFMenu from "./PDFMenu"; import "./Annotation.scss"; diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx index 2a6eff7ff..ff328068b 100644 --- a/src/client/views/pdf/PDFMenu.tsx +++ b/src/client/views/pdf/PDFMenu.tsx @@ -5,7 +5,7 @@ import { observer } from "mobx-react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { unimplementedFunction, returnFalse } from "../../../Utils"; import AntimodeMenu from "../AntimodeMenu"; -import { Doc, Opt } from "../../../new_fields/Doc"; +import { Doc, Opt } from "../../../fields/Doc"; @observer export default class PDFMenu extends AntimodeMenu { diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index acaa4363e..c50969493 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -4,16 +4,16 @@ import * as Pdfjs from "pdfjs-dist"; import "pdfjs-dist/web/pdf_viewer.css"; import * as rp from "request-promise"; import { Dictionary } from "typescript-collections"; -import { Doc, DocListCast, FieldResult, HeightSym, Opt, WidthSym } from "../../../new_fields/Doc"; -import { documentSchema } from "../../../new_fields/documentSchemas"; -import { Id } from "../../../new_fields/FieldSymbols"; -import { InkTool } from "../../../new_fields/InkField"; -import { List } from "../../../new_fields/List"; -import { createSchema, makeInterface } from "../../../new_fields/Schema"; -import { ScriptField } from "../../../new_fields/ScriptField"; -import { Cast, NumCast } from "../../../new_fields/Types"; -import { PdfField } from "../../../new_fields/URLField"; -import { TraceMobx } from "../../../new_fields/util"; +import { Doc, DocListCast, FieldResult, HeightSym, Opt, WidthSym } from "../../../fields/Doc"; +import { documentSchema } from "../../../fields/documentSchemas"; +import { Id } from "../../../fields/FieldSymbols"; +import { InkTool } from "../../../fields/InkField"; +import { List } from "../../../fields/List"; +import { createSchema, makeInterface } from "../../../fields/Schema"; +import { ScriptField } from "../../../fields/ScriptField"; +import { Cast, NumCast } from "../../../fields/Types"; +import { PdfField } from "../../../fields/URLField"; +import { TraceMobx } from "../../../fields/util"; import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, emptyPath, intersectRect, returnZero, smoothScroll, Utils } from "../../../Utils"; import { Docs, DocUtils } from "../../documents/Documents"; import { DocumentType } from "../../documents/DocumentTypes"; diff --git a/src/client/views/presentationview/PresElementBox.tsx b/src/client/views/presentationview/PresElementBox.tsx index 6d7602268..280ba9093 100644 --- a/src/client/views/presentationview/PresElementBox.tsx +++ b/src/client/views/presentationview/PresElementBox.tsx @@ -1,11 +1,11 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, computed, IReactionDisposer, reaction } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DataSym, DocListCast } from "../../../new_fields/Doc"; -import { documentSchema } from '../../../new_fields/documentSchemas'; -import { Id } from "../../../new_fields/FieldSymbols"; -import { createSchema, makeInterface } from '../../../new_fields/Schema'; -import { Cast, NumCast, BoolCast, ScriptCast } from "../../../new_fields/Types"; +import { Doc, DataSym, DocListCast } from "../../../fields/Doc"; +import { documentSchema } from '../../../fields/documentSchemas'; +import { Id } from "../../../fields/FieldSymbols"; +import { createSchema, makeInterface } from '../../../fields/Schema'; +import { Cast, NumCast, BoolCast, ScriptCast } from "../../../fields/Types"; import { emptyFunction, emptyPath, returnFalse, returnTrue, returnOne, returnZero } from "../../../Utils"; import { Transform } from "../../util/Transform"; import { CollectionViewType } from '../collections/CollectionView'; @@ -41,8 +41,8 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc // these fields are conditionally computed fields on the layout document that take this document as a parameter @computed get indexInPres() { return Number(this.lookupField("indexInPres")); } // the index field is where this document is in the presBox display list (since this value is different for each presentation element, the value can't be stored on the layout template which is used by all display elements) @computed get collapsedHeight() { return Number(this.lookupField("presCollapsedHeight")); } // the collapsed height changes depending on the state of the presBox. We could store this on the presentation elemnt template if it's used by only one presentation - but if it's shared by multiple, then this value must be looked up - @computed get presStatus() { return BoolCast(this.layoutDoc.presStatus); } - @computed get currentIndex() { return NumCast(this.layoutDoc.currentIndex); } + @computed get presStatus() { return BoolCast(this.lookupField("presStatus")); } + @computed get itemIndex() { return NumCast(this.lookupField("_itemIndex")); } @computed get targetDoc() { return Cast(this.rootDoc.presentationTargetDoc, Doc, null) || this.rootDoc; } componentDidMount() { @@ -62,11 +62,11 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc e.stopPropagation(); this.rootDoc.presHideTillShownButton = !this.rootDoc.presHideTillShownButton; if (!this.rootDoc.presHideTillShownButton) { - if (this.indexInPres >= this.currentIndex && this.targetDoc) { + if (this.indexInPres >= this.itemIndex && this.targetDoc) { this.targetDoc.opacity = 1; } } else { - if (this.presStatus && this.indexInPres > this.currentIndex && this.targetDoc) { + if (this.presStatus && this.indexInPres > this.itemIndex && this.targetDoc) { this.targetDoc.opacity = 0; } } @@ -82,12 +82,12 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc e.stopPropagation(); this.rootDoc.presHideAfterButton = !this.rootDoc.presHideAfterButton; if (!this.rootDoc.presHideAfterButton) { - if (this.indexInPres <= this.currentIndex && this.targetDoc) { + if (this.indexInPres <= this.itemIndex && this.targetDoc) { this.targetDoc.opacity = 1; } } else { if (this.rootDoc.presFadeButton) this.rootDoc.presFadeButton = false; - if (this.presStatus && this.indexInPres < this.currentIndex && this.targetDoc) { + if (this.presStatus && this.indexInPres < this.itemIndex && this.targetDoc) { this.targetDoc.opacity = 0; } } @@ -103,12 +103,12 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc e.stopPropagation(); this.rootDoc.presFadeButton = !this.rootDoc.presFadeButton; if (!this.rootDoc.presFadeButton) { - if (this.indexInPres <= this.currentIndex && this.targetDoc) { + if (this.indexInPres <= this.itemIndex && this.targetDoc) { this.targetDoc.opacity = 1; } } else { this.rootDoc.presHideAfterButton = false; - if (this.presStatus && (this.indexInPres < this.currentIndex) && this.targetDoc) { + if (this.presStatus && (this.indexInPres < this.itemIndex) && this.targetDoc) { this.targetDoc.opacity = 0.5; } } @@ -123,7 +123,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc this.rootDoc.presNavButton = !this.rootDoc.presNavButton; if (this.rootDoc.presNavButton) { this.rootDoc.presZoomButton = false; - if (this.currentIndex === this.indexInPres) { + if (this.itemIndex === this.indexInPres) { this.props.focus(this.rootDoc); } } @@ -139,7 +139,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc this.rootDoc.presZoomButton = !this.rootDoc.presZoomButton; if (this.rootDoc.presZoomButton) { this.rootDoc.presNavButton = false; - if (this.currentIndex === this.indexInPres) { + if (this.itemIndex === this.indexInPres) { this.props.focus(this.rootDoc); } } @@ -189,7 +189,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc render() { const treecontainer = this.props.ContainingCollectionDoc?._viewType === CollectionViewType.Tree; - const className = "presElementBox-item" + (this.currentIndex === this.indexInPres ? " presElementBox-active" : ""); + const className = "presElementBox-item" + (this.itemIndex === this.indexInPres ? " presElementBox-active" : ""); const pbi = "presElementBox-interaction"; return !(this.rootDoc instanceof Doc) || this.targetDoc instanceof Promise ? (null) : ( <div className={className} key={this.props.Document[Id] + this.indexInPres} diff --git a/src/client/views/search/FilterBox.tsx b/src/client/views/search/FilterBox.tsx index 662b37d77..4b53963a5 100644 --- a/src/client/views/search/FilterBox.tsx +++ b/src/client/views/search/FilterBox.tsx @@ -4,10 +4,10 @@ import { observable, action } from 'mobx'; import "./SearchBox.scss"; import { faTimes, faCheckCircle, faObjectGroup } from '@fortawesome/free-solid-svg-icons'; import { library } from '@fortawesome/fontawesome-svg-core'; -import { Doc } from '../../../new_fields/Doc'; -import { Id } from '../../../new_fields/FieldSymbols'; +import { Doc } from '../../../fields/Doc'; +import { Id } from '../../../fields/FieldSymbols'; import { DocumentType } from "../../documents/DocumentTypes"; -import { Cast, StrCast } from '../../../new_fields/Types'; +import { Cast, StrCast } from '../../../fields/Types'; import * as _ from "lodash"; import { IconBar } from './IconBar'; import { FieldFilters } from './FieldFilters'; diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index e41b725b1..eea7b528b 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -5,9 +5,9 @@ import { action, computed, observable, runInAction, IReactionDisposer, reaction import { observer } from 'mobx-react'; import * as React from 'react'; import * as rp from 'request-promise'; -import { Doc } from '../../../new_fields/Doc'; -import { Id } from '../../../new_fields/FieldSymbols'; -import { Cast, NumCast, StrCast } from '../../../new_fields/Types'; +import { Doc } from '../../../fields/Doc'; +import { Id } from '../../../fields/FieldSymbols'; +import { Cast, NumCast, StrCast } from '../../../fields/Types'; import { Utils } from '../../../Utils'; import { Docs } from '../../documents/Documents'; import { SetupDrag } from '../../util/DragManager'; @@ -19,7 +19,7 @@ import { FieldView } from '../nodes/FieldView'; import { DocumentType } from "../../documents/DocumentTypes"; import { DocumentView } from '../nodes/DocumentView'; import { SelectionManager } from '../../util/SelectionManager'; -import { listSpec } from '../../../new_fields/Schema'; +import { listSpec } from '../../../fields/Schema'; library.add(faTimes); diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx index 96f43e931..24d6e9d6f 100644 --- a/src/client/views/search/SearchItem.tsx +++ b/src/client/views/search/SearchItem.tsx @@ -4,9 +4,9 @@ import { faCaretUp, faChartBar, faFile, faFilePdf, faFilm, faFingerprint, faGlob import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { Doc } from "../../../new_fields/Doc"; -import { Id } from "../../../new_fields/FieldSymbols"; -import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; +import { Doc } from "../../../fields/Doc"; +import { Id } from "../../../fields/FieldSymbols"; +import { Cast, NumCast, StrCast } from "../../../fields/Types"; import { emptyFunction, emptyPath, returnFalse, Utils, returnTrue, returnOne, returnZero } from "../../../Utils"; import { DocumentType } from "../../documents/DocumentTypes"; import { DocumentManager } from "../../util/DocumentManager"; diff --git a/src/debug/Repl.tsx b/src/debug/Repl.tsx index fd6b47ff0..d541c8009 100644 --- a/src/debug/Repl.tsx +++ b/src/debug/Repl.tsx @@ -3,9 +3,9 @@ import * as ReactDOM from 'react-dom'; import { observer } from 'mobx-react'; import { observable, computed } from 'mobx'; import { CompileScript } from '../client/util/Scripting'; -import { makeInterface } from '../new_fields/Schema'; -import { ObjectField } from '../new_fields/ObjectField'; -import { RefField } from '../new_fields/RefField'; +import { makeInterface } from '../fields/Schema'; +import { ObjectField } from '../fields/ObjectField'; +import { RefField } from '../fields/RefField'; import { DocServer } from '../client/DocServer'; @observer diff --git a/src/debug/Test.tsx b/src/debug/Test.tsx index 3baedce4b..17d3db8fd 100644 --- a/src/debug/Test.tsx +++ b/src/debug/Test.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { DocServer } from '../client/DocServer'; -import { Doc } from '../new_fields/Doc'; +import { Doc } from '../fields/Doc'; import * as Pdfjs from "pdfjs-dist"; import "pdfjs-dist/web/pdf_viewer.css"; import { Utils } from '../Utils'; diff --git a/src/debug/Viewer.tsx b/src/debug/Viewer.tsx index a26d2e06a..6ce39d533 100644 --- a/src/debug/Viewer.tsx +++ b/src/debug/Viewer.tsx @@ -3,17 +3,17 @@ import "normalize.css"; import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { observer } from 'mobx-react'; -import { Doc, Field, FieldResult, Opt } from '../new_fields/Doc'; +import { Doc, Field, FieldResult, Opt } from '../fields/Doc'; import { DocServer } from '../client/DocServer'; -import { Id } from '../new_fields/FieldSymbols'; -import { List } from '../new_fields/List'; -import { URLField } from '../new_fields/URLField'; +import { Id } from '../fields/FieldSymbols'; +import { List } from '../fields/List'; +import { URLField } from '../fields/URLField'; import { EditableView } from '../client/views/EditableView'; import { CompileScript } from '../client/util/Scripting'; -import { RichTextField } from '../new_fields/RichTextField'; -import { DateField } from '../new_fields/DateField'; -import { ScriptField } from '../new_fields/ScriptField'; -import CursorField from '../new_fields/CursorField'; +import { RichTextField } from '../fields/RichTextField'; +import { DateField } from '../fields/DateField'; +import { ScriptField } from '../fields/ScriptField'; +import CursorField from '../fields/CursorField'; DateField; URLField; diff --git a/src/new_fields/CursorField.ts b/src/fields/CursorField.ts index 28467377b..28467377b 100644 --- a/src/new_fields/CursorField.ts +++ b/src/fields/CursorField.ts diff --git a/src/new_fields/DateField.ts b/src/fields/DateField.ts index a925148c2..a925148c2 100644 --- a/src/new_fields/DateField.ts +++ b/src/fields/DateField.ts diff --git a/src/new_fields/Doc.ts b/src/fields/Doc.ts index 9efb14d03..a1e1e11b1 100644 --- a/src/new_fields/Doc.ts +++ b/src/fields/Doc.ts @@ -6,7 +6,7 @@ import { DocumentType } from "../client/documents/DocumentTypes"; import { Scripting, scriptingGlobal } from "../client/util/Scripting"; import { afterDocDeserialize, autoObject, Deserializable, SerializationHelper } from "../client/util/SerializationHelper"; import { UndoManager } from "../client/util/UndoManager"; -import { intersectRect } from "../Utils"; +import { intersectRect, Utils } from "../Utils"; import { HandleUpdate, Id, OnUpdate, Parent, Self, SelfProxy, ToScriptString, ToString, Update, Copy } from "./FieldSymbols"; import { List } from "./List"; import { ObjectField } from "./ObjectField"; @@ -19,6 +19,7 @@ import { Cast, FieldValue, NumCast, StrCast, ToConstructor, ScriptCast } from ". import { deleteProperty, getField, getter, makeEditable, makeReadOnly, setter, updateFunction } from "./util"; import { Docs, DocumentOptions } from "../client/documents/Documents"; import { PdfField, VideoField, AudioField, ImageField } from "./URLField"; +import { LinkManager } from "../client/util/LinkManager"; export namespace Field { export function toKeyValueString(doc: Doc, key: string): string { @@ -466,6 +467,7 @@ export namespace Doc { return (layoutDoc.isTemplateForField || layoutDoc.isTemplateDoc) && dataDoc && layoutDoc !== dataDoc; } + const _pendingMap: Map<string, boolean> = new Map(); // // Returns an expanded template layout for a target data document if there is a template relationship // between the two. If so, the layoutDoc is expanded into a new document that inherits the properties @@ -491,13 +493,16 @@ export namespace Doc { const layoutFielddKey = Doc.LayoutFieldKey(templateLayoutDoc); const expandedLayoutFieldKey = (templateField || layoutFielddKey) + "-layout[" + templateLayoutDoc[Id] + (args ? `(${args})` : "") + "]"; let expandedTemplateLayout = targetDoc?.[expandedLayoutFieldKey]; + if (templateLayoutDoc.resolvedDataDoc instanceof Promise) { expandedTemplateLayout = undefined; + _pendingMap.set(targetDoc[Id] + expandedLayoutFieldKey, true); } - else if (expandedTemplateLayout === undefined) { - if (templateLayoutDoc.resolvedDataDoc === Doc.GetProto(targetDoc) && templateLayoutDoc.PARAMS === StrCast(targetDoc.PARAMS)) { + else if (expandedTemplateLayout === undefined && !_pendingMap.get(targetDoc[Id] + expandedLayoutFieldKey + args)) { + if (templateLayoutDoc.resolvedDataDoc === (targetDoc.rootDocument || Doc.GetProto(targetDoc)) && templateLayoutDoc.PARAMS === StrCast(targetDoc.PARAMS)) { expandedTemplateLayout = templateLayoutDoc; // reuse an existing template layout if its for the same document with the same params } else { + _pendingMap.set(targetDoc[Id] + expandedLayoutFieldKey + args, true); templateLayoutDoc.resolvedDataDoc && (templateLayoutDoc = Cast(templateLayoutDoc.proto, Doc, null) || templateLayoutDoc); // if the template has already been applied (ie, a nested template), then use the template's prototype setTimeout(action(() => { if (!targetDoc[expandedLayoutFieldKey]) { @@ -512,6 +517,7 @@ export namespace Doc { if (dataDoc[templateField] === undefined && templateLayoutDoc[templateField] instanceof List) { dataDoc[templateField] = ComputedField.MakeFunction(`ObjectField.MakeCopy(templateLayoutDoc["${templateField}"] as List)`, { templateLayoutDoc: Doc.name }, { templateLayoutDoc }); } + _pendingMap.delete(targetDoc[Id] + expandedLayoutFieldKey + args); } }), 0); } @@ -584,46 +590,64 @@ export namespace Doc { return copy; } - export function MakeClone(doc: Doc, cloneMap: Map<Doc, Doc> = new Map()): Doc { + export function MakeClone(doc: Doc): Doc { + const cloneMap = new Map<string, Doc>(); + const rtfMap: { copy: Doc, key: string, field: RichTextField }[] = []; + const copy = Doc.makeClone(doc, cloneMap, rtfMap); + rtfMap.map(({ copy, key, field }) => { + const replacer = (match: any, attr: string, id: string, offset: any, string: any) => { + const mapped = cloneMap.get(id); + return attr + "\"" + (mapped ? mapped[Id] : id) + "\""; + }; + const replacer2 = (match: any, href: string, id: string, offset: any, string: any) => { + const mapped = cloneMap.get(id); + return href + (mapped ? mapped[Id] : id); + }; + const regex = `(${Utils.prepend("/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; + } + + export function makeClone(doc: Doc, cloneMap: Map<string, Doc>, rtfs: { copy: Doc, key: string, field: RichTextField }[]): Doc { if (Doc.IsBaseProto(doc)) return doc; - if (cloneMap.get(doc)) return cloneMap.get(doc)!; + if (cloneMap.get(doc[Id])) return cloneMap.get(doc[Id])!; const copy = new Doc(undefined, true); - cloneMap.set(doc, copy); + 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 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))); + } 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 + } else { + copy[key] = 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 }); + } + } + } + }; if (key === "proto") { if (doc[key] instanceof Doc) { - copy[key] = Doc.MakeClone(doc[key]!, cloneMap); + copy[key] = Doc.makeClone(doc[key]!, cloneMap, rtfs); } } else { if (field instanceof RefField) { copy[key] = field; } else if (cfield instanceof ComputedField) { copy[key] = ComputedField.MakeFunction(cfield.script.originalScript); - if (field instanceof ObjectField) { - const list = Cast(doc[key], listSpec(Doc)); - if (list !== undefined && !(list instanceof Promise)) { - copy[key] = new List<Doc>(list.filter(d => d instanceof Doc).map(d => Doc.MakeClone(d as Doc, cloneMap))); - } else { - copy[key] = doc[key] instanceof Doc ? - key.includes("layout[") ? - undefined : Doc.MakeClone(doc[key] as Doc, cloneMap) : // reference documents except copy documents that are expanded teplate fields - ObjectField.MakeCopy(field); - } - } + (key === "links" && field instanceof ObjectField) && copyObjectField(field); } else if (field instanceof ObjectField) { - const list = Cast(doc[key], listSpec(Doc)); - if (list !== undefined && !(list instanceof Promise)) { - copy[key] = new List<Doc>(list.filter(d => d instanceof Doc).map(d => Doc.MakeClone(d as Doc, cloneMap))); - } else { - copy[key] = doc[key] instanceof Doc ? - key.includes("layout[") ? - undefined : Doc.MakeClone(doc[key] as Doc, cloneMap) : // reference documents except copy documents that are expanded teplate fields - ObjectField.MakeCopy(field); - } + copyObjectField(field); } else if (field instanceof Promise) { debugger; //This shouldn't happend... } else { @@ -633,7 +657,7 @@ export namespace Doc { }); Doc.SetInPlace(copy, "title", "CLONE: " + doc.title, true); copy.cloneOf = doc; - cloneMap.set(doc, copy); + cloneMap.set(doc[Id], copy); return copy; } @@ -911,19 +935,28 @@ export namespace Doc { } } } - - export function freezeNativeDimensions(layoutDoc: Doc, width: number, height: number): void { - layoutDoc._autoHeight = false; - if (!layoutDoc._nativeWidth) { - layoutDoc._nativeWidth = NumCast(layoutDoc._width, width); - layoutDoc._nativeHeight = NumCast(layoutDoc._height, height); - } - } export function assignDocToField(doc: Doc, field: string, id: string) { DocServer.GetRefField(id).then(layout => layout instanceof Doc && (doc[field] = layout)); return id; } + export function toggleNativeDimensions(layoutDoc: Doc, contentScale: number, panelWidth: number, panelHeight: number) { + runInAction(() => { + if (layoutDoc._nativeWidth || layoutDoc._nativeHeight) { + layoutDoc.scale = NumCast(layoutDoc.scale, 1) * contentScale; + layoutDoc._nativeWidth = undefined; + layoutDoc._nativeHeight = undefined; + } + else { + layoutDoc._autoHeight = false; + if (!layoutDoc._nativeWidth) { + layoutDoc._nativeWidth = NumCast(layoutDoc._width, panelWidth); + layoutDoc._nativeHeight = NumCast(layoutDoc._height, panelHeight); + } + } + }); + } + export function isDocPinned(doc: Doc) { //add this new doc to props.Document const curPres = Cast(Doc.UserDoc().activePresentation, Doc) as Doc; diff --git a/src/new_fields/FieldSymbols.ts b/src/fields/FieldSymbols.ts index 8d040f493..8d040f493 100644 --- a/src/new_fields/FieldSymbols.ts +++ b/src/fields/FieldSymbols.ts diff --git a/src/new_fields/HtmlField.ts b/src/fields/HtmlField.ts index 6e8bba977..6e8bba977 100644 --- a/src/new_fields/HtmlField.ts +++ b/src/fields/HtmlField.ts diff --git a/src/new_fields/IconField.ts b/src/fields/IconField.ts index 76c4ddf1b..76c4ddf1b 100644 --- a/src/new_fields/IconField.ts +++ b/src/fields/IconField.ts diff --git a/src/new_fields/InkField.ts b/src/fields/InkField.ts index bb93de5ac..bb93de5ac 100644 --- a/src/new_fields/InkField.ts +++ b/src/fields/InkField.ts diff --git a/src/new_fields/List.ts b/src/fields/List.ts index a43f11e82..fdabea365 100644 --- a/src/new_fields/List.ts +++ b/src/fields/List.ts @@ -2,12 +2,13 @@ import { Deserializable, autoObject, afterDocDeserialize } from "../client/util/ import { Field } from "./Doc"; import { setter, getter, deleteProperty, updateFunction } from "./util"; import { serializable, alias, list } from "serializr"; -import { observable, action } from "mobx"; +import { observable, action, runInAction } from "mobx"; import { ObjectField } from "./ObjectField"; import { RefField } from "./RefField"; import { ProxyField } from "./Proxy"; -import { Self, Update, Parent, OnUpdate, SelfProxy, ToScriptString, ToString, Copy } from "./FieldSymbols"; +import { Self, Update, Parent, OnUpdate, SelfProxy, ToScriptString, ToString, Copy, Id } from "./FieldSymbols"; import { Scripting } from "../client/util/Scripting"; +import { DocServer } from "../client/DocServer"; const listHandlers: any = { /// Mutator methods @@ -54,11 +55,13 @@ const listHandlers: any = { return res; }, sort(cmpFunc: any) { + this[Self].__realFields(); // coerce retrieving entire array const res = this[Self].__fields.sort(cmpFunc ? (first: any, second: any) => cmpFunc(toRealField(first), toRealField(second)) : undefined); this[Update](); return res; }, splice: action(function (this: any, start: number, deleteCount: number, ...items: any[]) { + this[Self].__realFields(); // coerce retrieving entire array items = items.map(toObjectField); const list = this[Self]; for (let i = 0; i < items.length; i++) { @@ -94,102 +97,102 @@ const listHandlers: any = { }, /// Accessor methods concat: action(function (this: any, ...items: any[]) { + this[Self].__realFields(); return this[Self].__fields.map(toRealField).concat(...items); }), includes(valueToFind: any, fromIndex: number) { - const fields = this[Self].__fields; if (valueToFind instanceof RefField) { - return fields.map(toRealField).includes(valueToFind, fromIndex); + return this[Self].__realFields().includes(valueToFind, fromIndex); } else { - return fields.includes(valueToFind, fromIndex); + return this[Self].__fields.includes(valueToFind, fromIndex); } }, indexOf(valueToFind: any, fromIndex: number) { - const fields = this[Self].__fields; if (valueToFind instanceof RefField) { - return fields.map(toRealField).indexOf(valueToFind, fromIndex); + return this[Self].__realFields().indexOf(valueToFind, fromIndex); } else { - return fields.indexOf(valueToFind, fromIndex); + return this[Self].__fields.indexOf(valueToFind, fromIndex); } }, join(separator: any) { + this[Self].__realFields(); return this[Self].__fields.map(toRealField).join(separator); }, lastIndexOf(valueToFind: any, fromIndex: number) { - const fields = this[Self].__fields; if (valueToFind instanceof RefField) { - return fields.map(toRealField).lastIndexOf(valueToFind, fromIndex); + return this[Self].__realFields().lastIndexOf(valueToFind, fromIndex); } else { - return fields.lastIndexOf(valueToFind, fromIndex); + return this[Self].__fields.lastIndexOf(valueToFind, fromIndex); } }, slice(begin: number, end: number) { + this[Self].__realFields(); return this[Self].__fields.slice(begin, end).map(toRealField); }, /// Iteration methods entries() { - return this[Self].__fields.map(toRealField).entries(); + return this[Self].__realFields().entries(); }, every(callback: any, thisArg: any) { - return this[Self].__fields.map(toRealField).every(callback, thisArg); + return this[Self].__realFields().every(callback, thisArg); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead // return this[Self].__fields.every((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); }, filter(callback: any, thisArg: any) { - return this[Self].__fields.map(toRealField).filter(callback, thisArg); + return this[Self].__realFields().filter(callback, thisArg); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead // return this[Self].__fields.filter((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); }, find(callback: any, thisArg: any) { - return this[Self].__fields.map(toRealField).find(callback, thisArg); + return this[Self].__realFields().find(callback, thisArg); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead // return this[Self].__fields.find((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); }, findIndex(callback: any, thisArg: any) { - return this[Self].__fields.map(toRealField).findIndex(callback, thisArg); + return this[Self].__realFields().findIndex(callback, thisArg); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead // return this[Self].__fields.findIndex((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); }, forEach(callback: any, thisArg: any) { - return this[Self].__fields.map(toRealField).forEach(callback, thisArg); + return this[Self].__realFields().forEach(callback, thisArg); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead // return this[Self].__fields.forEach((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); }, map(callback: any, thisArg: any) { - return this[Self].__fields.map(toRealField).map(callback, thisArg); + return this[Self].__realFields().map(callback, thisArg); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead // return this[Self].__fields.map((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); }, reduce(callback: any, initialValue: any) { - return this[Self].__fields.map(toRealField).reduce(callback, initialValue); + return this[Self].__realFields().reduce(callback, initialValue); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead // return this[Self].__fields.reduce((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue); }, reduceRight(callback: any, initialValue: any) { - return this[Self].__fields.map(toRealField).reduceRight(callback, initialValue); + return this[Self].__realFields().reduceRight(callback, initialValue); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead // return this[Self].__fields.reduceRight((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue); }, some(callback: any, thisArg: any) { - return this[Self].__fields.map(toRealField).some(callback, thisArg); + return this[Self].__realFields().some(callback, thisArg); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead // return this[Self].__fields.some((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); }, values() { - return this[Self].__fields.map(toRealField).values(); + return this[Self].__realFields().values(); }, [Symbol.iterator]() { - return this[Self].__fields.map(toRealField).values(); + return this[Self].__realFields().values(); } }; @@ -254,6 +257,31 @@ class ListImpl<T extends Field> extends ObjectField { [key: number]: T | (T extends RefField ? Promise<T> : never); + // this requests all ProxyFields at the same time to avoid the overhead + // of separate network requests and separate updates to the React dom. + private __realFields() { + const waiting = this.__fields.filter(f => f instanceof ProxyField && f.promisedValue()); + const promised = waiting.map(f => f instanceof ProxyField ? f.promisedValue() : ""); + // if we find any ProxyFields that don't have a current value, then + // start the server request for all of them + if (promised.length) { + const promise = DocServer.GetRefFields(promised); + // as soon as we get the fields from the server, set all the list values in one + // action to generate one React dom update. + promise.then(fields => runInAction(() => { + waiting.map((w, i) => w instanceof ProxyField && w.setValue(fields[promised[i]])); + })); + // we also have to mark all lists items with this promise so that any calls to them + // will await the batch request. + // This counts on the handler for 'promise' in the call above being invoked before the + // handler for 'promise' in the lines below. + waiting.map((w, i) => { + w instanceof ProxyField && w.setPromise(promise.then(fields => fields[promised[i]])); + }); + } + return this.__fields.map(toRealField); + } + @serializable(alias("fields", list(autoObject(), { afterDeserialize: afterDocDeserialize }))) private get __fields() { return this.___fields; @@ -283,7 +311,7 @@ class ListImpl<T extends Field> extends ObjectField { // console.log(diff); const update = this[OnUpdate]; // update && update(diff); - update && update(); + update?.(); } private [Self] = this; diff --git a/src/new_fields/ListSpec.ts b/src/fields/ListSpec.ts index e69de29bb..e69de29bb 100644 --- a/src/new_fields/ListSpec.ts +++ b/src/fields/ListSpec.ts diff --git a/src/new_fields/ObjectField.ts b/src/fields/ObjectField.ts index 9aa1c9b04..9aa1c9b04 100644 --- a/src/new_fields/ObjectField.ts +++ b/src/fields/ObjectField.ts diff --git a/src/new_fields/PresField.ts b/src/fields/PresField.ts index f236a04fd..f236a04fd 100644 --- a/src/new_fields/PresField.ts +++ b/src/fields/PresField.ts diff --git a/src/new_fields/Proxy.ts b/src/fields/Proxy.ts index d50c0f14e..555faaad0 100644 --- a/src/new_fields/Proxy.ts +++ b/src/fields/Proxy.ts @@ -1,7 +1,7 @@ import { Deserializable } from "../client/util/SerializationHelper"; import { FieldWaiting } from "./Doc"; import { primitive, serializable } from "serializr"; -import { observable, action } from "mobx"; +import { observable, action, runInAction } from "mobx"; import { DocServer } from "../client/DocServer"; import { RefField } from "./RefField"; import { ObjectField } from "./ObjectField"; @@ -61,6 +61,11 @@ export class ProxyField<T extends RefField> extends ObjectField { return undefined; } if (!this.promise) { + const cached = DocServer.GetCachedRefField(this.fieldId); + if (cached !== undefined) { + runInAction(() => this.cache = cached as any); + return cached as any; + } this.promise = DocServer.GetRefField(this.fieldId).then(action((field: any) => { this.promise = undefined; this.cache = field; @@ -70,6 +75,17 @@ export class ProxyField<T extends RefField> extends ObjectField { } return this.promise as any; } + promisedValue(): string { return !this.cache && !this.failed && !this.promise ? this.fieldId : ""; } + setPromise(promise: any) { + this.promise = promise; + } + @action + setValue(field: any) { + this.promise = undefined; + this.cache = field; + if (field === undefined) this.failed = true; + return field; + } } export namespace ProxyField { diff --git a/src/new_fields/RefField.ts b/src/fields/RefField.ts index b6ef69750..b6ef69750 100644 --- a/src/new_fields/RefField.ts +++ b/src/fields/RefField.ts diff --git a/src/new_fields/RichTextField.ts b/src/fields/RichTextField.ts index 5cf0e0cc3..5cf0e0cc3 100644 --- a/src/new_fields/RichTextField.ts +++ b/src/fields/RichTextField.ts diff --git a/src/new_fields/RichTextUtils.ts b/src/fields/RichTextUtils.ts index c475d0d73..c475d0d73 100644 --- a/src/new_fields/RichTextUtils.ts +++ b/src/fields/RichTextUtils.ts diff --git a/src/new_fields/Schema.ts b/src/fields/Schema.ts index 72bce283d..72bce283d 100644 --- a/src/new_fields/Schema.ts +++ b/src/fields/Schema.ts diff --git a/src/new_fields/SchemaHeaderField.ts b/src/fields/SchemaHeaderField.ts index 07c90f5a2..07c90f5a2 100644 --- a/src/new_fields/SchemaHeaderField.ts +++ b/src/fields/SchemaHeaderField.ts diff --git a/src/new_fields/ScriptField.ts b/src/fields/ScriptField.ts index 8d0ddf94c..f05f431ac 100644 --- a/src/new_fields/ScriptField.ts +++ b/src/fields/ScriptField.ts @@ -1,10 +1,10 @@ import { ObjectField } from "./ObjectField"; -import { CompiledScript, CompileScript, scriptingGlobal, ScriptOptions } from "../client/util/Scripting"; +import { CompiledScript, CompileScript, scriptingGlobal, ScriptOptions, CompileError, CompileResult } from "../client/util/Scripting"; 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 "../new_fields/Doc"; -import { Plugins } from "./util"; +import { Doc, Field } from "./Doc"; +import { Plugins, setter } from "./util"; import { computedFn } from "mobx-utils"; import { ProxyField } from "./Proxy"; import { Cast } from "./Types"; @@ -59,18 +59,20 @@ async function deserializeScript(script: ScriptField) { export class ScriptField extends ObjectField { @serializable(object(scriptSchema)) readonly script: CompiledScript; + @serializable(object(scriptSchema)) + readonly setterscript: CompiledScript | undefined; @serializable(autoObject()) private captures?: ProxyField<Doc>; - constructor(script: CompiledScript) { + constructor(script: CompiledScript, setterscript?: CompileResult) { super(); - if (script && script.options.capturedVariables) { + if (script?.options.capturedVariables) { const doc = Doc.assign(new Doc, script.options.capturedVariables); this.captures = new ProxyField(doc); } - + this.setterscript = setterscript?.compiled ? setterscript : undefined; this.script = script; } @@ -136,13 +138,27 @@ export class ComputedField extends ScriptField { //TODO maybe add an observable cache based on what is passed in for doc, considering there shouldn't really be that many possible values for doc value = computedFn((doc: Doc) => this._valueOutsideReaction(doc)); _valueOutsideReaction = (doc: Doc) => this._lastComputedResult = this.script.run({ this: doc, self: Cast(doc.rootDocument, Doc, null) || doc, _last_: this._lastComputedResult }, console.log).result; + + + constructor(script: CompiledScript, setterscript?: CompiledScript) { + super(script, + !setterscript && script?.originalScript.includes("self.timecode") ? + ScriptField.CompileScript("self['x' + self.timecode] = value", { value: "any" }, true) : setterscript); + } + public static MakeScript(script: string, params: object = {}) { const compiled = ScriptField.CompileScript(script, params, false); return compiled.compiled ? new ComputedField(compiled) : undefined; } - public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Field }) { + public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Field }, setterScript?: string) { const compiled = ScriptField.CompileScript(script, params, true, capturedVariables); - return compiled.compiled ? new ComputedField(compiled) : undefined; + const setCompiled = setterScript ? ScriptField.CompileScript(setterScript, params, true, capturedVariables) : undefined; + return compiled.compiled ? new ComputedField(compiled, setCompiled?.compiled ? setCompiled : undefined) : undefined; + } + public static MakeInterpolated(fieldKey: string, interpolatorKey: string) { + const getField = ScriptField.CompileScript(`(self['${fieldKey}-indexed'])[self.${interpolatorKey}]`, {}, true, {}); + const setField = ScriptField.CompileScript(`(self['${fieldKey}-indexed'])[self.${interpolatorKey}] = value`, { value: "any" }, true, {}); + return getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined; } } diff --git a/src/new_fields/Types.ts b/src/fields/Types.ts index 3d784448d..3d784448d 100644 --- a/src/new_fields/Types.ts +++ b/src/fields/Types.ts diff --git a/src/new_fields/URLField.ts b/src/fields/URLField.ts index fb71160ca..fb71160ca 100644 --- a/src/new_fields/URLField.ts +++ b/src/fields/URLField.ts diff --git a/src/new_fields/documentSchemas.ts b/src/fields/documentSchemas.ts index e7d27d81e..cacba43b6 100644 --- a/src/new_fields/documentSchemas.ts +++ b/src/fields/documentSchemas.ts @@ -44,6 +44,7 @@ export const documentSchema = createSchema({ _chromeStatus: "string", // determines the state of the collection chrome. values allowed are 'replaced', 'enabled', 'disabled', 'collapsed' _fontSize: "number", _fontFamily: "string", + _sidebarWidthPercent: "string", // percent of text window width taken up by sidebar // appearance properties on the data document backgroundColor: "string", // background color of document diff --git a/src/new_fields/util.ts b/src/fields/util.ts index 8c719ccd8..a287b0210 100644 --- a/src/new_fields/util.ts +++ b/src/fields/util.ts @@ -7,6 +7,8 @@ import { ObjectField } from "./ObjectField"; 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"; function _readOnlySetter(): never { throw new Error("Documents can't be modified in read-only mode"); @@ -114,6 +116,9 @@ export function setter(target: any, in_prop: string | symbol | number, value: an return true; } } + if (target.__fields[prop] instanceof ComputedField && target.__fields[prop].setterscript) { + return ScriptCast(target.__fields[prop])?.setterscript?.run({ self: target[SelfProxy], this: target[SelfProxy], value }).success ? true : false; + } return _setter(target, prop, value, receiver); } diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx index 295e82142..231870531 100644 --- a/src/mobile/ImageUpload.tsx +++ b/src/mobile/ImageUpload.tsx @@ -4,16 +4,15 @@ import { Docs } from '../client/documents/Documents'; import "./ImageUpload.scss"; import React = require('react'); import { DocServer } from '../client/DocServer'; -import { Opt, Doc } from '../new_fields/Doc'; -import { Cast } from '../new_fields/Types'; -import { listSpec } from '../new_fields/Schema'; -import { List } from '../new_fields/List'; +import { Opt, Doc } from '../fields/Doc'; +import { Cast } from '../fields/Types'; +import { listSpec } from '../fields/Schema'; +import { List } from '../fields/List'; import { observer } from 'mobx-react'; import { observable } from 'mobx'; import { Utils } from '../Utils'; import MobileInterface from './MobileInterface'; -import { CurrentUserUtils } from '../server/authentication/models/current_user_utils'; -import { Scripting } from '../client/util/Scripting'; +import { CurrentUserUtils } from '../client/util/CurrentUserUtils'; diff --git a/src/mobile/MobileInkOverlay.tsx b/src/mobile/MobileInkOverlay.tsx index 1537ae034..973931615 100644 --- a/src/mobile/MobileInkOverlay.tsx +++ b/src/mobile/MobileInkOverlay.tsx @@ -4,11 +4,11 @@ import { MobileInkOverlayContent, GestureContent, UpdateMobileInkOverlayPosition import { observable, action } from "mobx"; import { GestureUtils } from "../pen-gestures/GestureUtils"; import "./MobileInkOverlay.scss"; -import { StrCast, Cast } from '../new_fields/Types'; +import { StrCast, Cast } from '../fields/Types'; import { DragManager } from "../client/util/DragManager"; import { DocServer } from '../client/DocServer'; -import { Doc, DocListCastAsync } from '../new_fields/Doc'; -import { listSpec } from '../new_fields/Schema'; +import { Doc, DocListCastAsync } from '../fields/Doc'; +import { listSpec } from '../fields/Schema'; @observer diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx index 69a80e1b4..6c2e797d6 100644 --- a/src/mobile/MobileInterface.tsx +++ b/src/mobile/MobileInterface.tsx @@ -10,22 +10,22 @@ import { DocumentManager } from '../client/util/DocumentManager'; import RichTextMenu from '../client/views/nodes/formattedText/RichTextMenu'; import { Scripting } from '../client/util/Scripting'; import { Transform } from '../client/util/Transform'; -import { CollectionView } from '../client/views/collections/CollectionView'; import { DocumentDecorations } from '../client/views/DocumentDecorations'; import GestureOverlay from '../client/views/GestureOverlay'; import { InkingControl } from '../client/views/InkingControl'; import { DocumentView } from '../client/views/nodes/DocumentView'; import { RadialMenu } from '../client/views/nodes/RadialMenu'; import { PreviewCursor } from '../client/views/PreviewCursor'; -import { Doc, DocListCast, FieldResult } from '../new_fields/Doc'; -import { Id } from '../new_fields/FieldSymbols'; -import { InkTool } from '../new_fields/InkField'; -import { listSpec } from '../new_fields/Schema'; -import { Cast, FieldValue } from '../new_fields/Types'; -import { WebField } from "../new_fields/URLField"; -import { CurrentUserUtils } from '../server/authentication/models/current_user_utils'; +import { Doc, DocListCast, FieldResult } from '../fields/Doc'; +import { Id } from '../fields/FieldSymbols'; +import { InkTool } from '../fields/InkField'; +import { listSpec } from '../fields/Schema'; +import { Cast, FieldValue } from '../fields/Types'; +import { WebField } from "../fields/URLField"; +import { CurrentUserUtils } from '../client/util/CurrentUserUtils'; import { emptyFunction, emptyPath, returnEmptyString, returnFalse, returnOne, returnTrue, returnZero } from '../Utils'; import "./MobileInterface.scss"; +import { CollectionView } from '../client/views/collections/CollectionView'; library.add(faLongArrowAltLeft); diff --git a/src/pen-gestures/GestureUtils.ts b/src/pen-gestures/GestureUtils.ts index b8a82ab4d..3b6170f68 100644 --- a/src/pen-gestures/GestureUtils.ts +++ b/src/pen-gestures/GestureUtils.ts @@ -1,9 +1,9 @@ import { NDollarRecognizer } from "./ndollar"; import { Type } from "typescript"; -import { InkField, PointData } from "../new_fields/InkField"; +import { InkField, PointData } from "../fields/InkField"; import { Docs } from "../client/documents/Documents"; -import { Doc, WidthSym, HeightSym } from "../new_fields/Doc"; -import { NumCast } from "../new_fields/Types"; +import { Doc, WidthSym, HeightSym } from "../fields/Doc"; +import { NumCast } from "../fields/Types"; import { CollectionFreeFormView } from "../client/views/collections/collectionFreeForm/CollectionFreeFormView"; import { Rect } from "react-measure"; import { Scripting } from "../client/util/Scripting"; diff --git a/src/scraping/buxton/final/BuxtonImporter.ts b/src/scraping/buxton/final/BuxtonImporter.ts index 94302c7b3..e55850b29 100644 --- a/src/scraping/buxton/final/BuxtonImporter.ts +++ b/src/scraping/buxton/final/BuxtonImporter.ts @@ -451,11 +451,23 @@ async function writeImages(zip: any): Promise<ImageData[]> { }); // if it's not an icon, by this rough heuristic, i.e. is it not square - if (Math.abs(width - height) > 10) { - valid.push({ width, height, type, mediaPath }); + const number = Number(/image(\d+)/.exec(mediaPath)![1]); + if (number > 5 || width - height > 10) { + valid.push({ width, height, type, mediaPath, number }); } } + valid.sort((a, b) => a.number - b.number); + + const [{ width: first_w, height: first_h }, { width: second_w, height: second_h }] = valid; + if (Math.abs(first_w / second_w - first_h / second_h) < 0.01) { + const first_size = first_w * first_h; + const second_size = second_w * second_h; + const target = first_size >= second_size ? 1 : 0; + valid.splice(target, 1); + console.log(`Heuristically removed image with size ${target ? second_size : first_size}`); + } + // for each valid image, output the _o, _l, _m, and _s files // THIS IS WHERE THE SCRIPT SPENDS MOST OF ITS TIME for (const { type, width, height, mediaPath } of valid) { diff --git a/src/scraping/buxton/scraper.py b/src/scraping/buxton/scraper.py index 1441a8621..ed122e544 100644 --- a/src/scraping/buxton/scraper.py +++ b/src/scraping/buxton/scraper.py @@ -16,7 +16,7 @@ filesPath = "../../server/public/files" image_dist = filesPath + "/images/buxton" db = MongoClient("localhost", 27017)["Dash"] -target_collection = db.newDocuments +target_collection = db.documents target_doc_title = "Collection 1" schema_guids = [] common_proto_id = "" diff --git a/src/server/ApiManagers/ApiManager.ts b/src/server/ApiManagers/ApiManager.ts index e2b01d585..27e9de065 100644 --- a/src/server/ApiManagers/ApiManager.ts +++ b/src/server/ApiManagers/ApiManager.ts @@ -1,4 +1,4 @@ -import RouteManager, { RouteInitializer } from "../RouteManager"; +import { RouteInitializer } from "../RouteManager"; export type Registration = (initializer: RouteInitializer) => void; diff --git a/src/server/ApiManagers/DeleteManager.ts b/src/server/ApiManagers/DeleteManager.ts index 9e70af2eb..7fbb37658 100644 --- a/src/server/ApiManagers/DeleteManager.ts +++ b/src/server/ApiManagers/DeleteManager.ts @@ -1,12 +1,12 @@ import ApiManager, { Registration } from "./ApiManager"; -import { Method, _permission_denied, PublicHandler } from "../RouteManager"; -import { WebSocket } from "../Websocket/Websocket"; +import { Method, _permission_denied } from "../RouteManager"; +import { WebSocket } from "../websocket"; import { Database } from "../database"; import rimraf = require("rimraf"); -import { pathToDirectory, Directory } from "./UploadManager"; import { filesDirectory } from ".."; import { DashUploadUtils } from "../DashUploadUtils"; import { mkdirSync } from "fs"; +import RouteSubscriber from "../RouteSubscriber"; export default class DeleteManager extends ApiManager { @@ -14,68 +14,39 @@ export default class DeleteManager extends ApiManager { register({ method: Method.GET, - subscription: "/delete", - secureHandler: async ({ res, isRelease }) => { + subscription: new RouteSubscriber("delete").add("target?"), + secureHandler: async ({ req, res, isRelease }) => { if (isRelease) { - return _permission_denied(res, deletionPermissionError); + return _permission_denied(res, "Cannot perform a delete operation outside of the development environment!"); } - await WebSocket.deleteFields(); - res.redirect("/home"); - } - }); - register({ - method: Method.GET, - subscription: "/deleteAll", - secureHandler: async ({ res, isRelease }) => { - if (isRelease) { - return _permission_denied(res, deletionPermissionError); + const { target } = req.params; + const { doDelete } = WebSocket; + + if (!target) { + await doDelete(); + } else { + let all = false; + switch (target) { + case "all": + all = true; + case "database": + await doDelete(false); + if (!all) break; + case "files": + rimraf.sync(filesDirectory); + mkdirSync(filesDirectory); + await DashUploadUtils.buildFileDirectories(); + break; + default: + await Database.Instance.dropSchema(target); + } } - await WebSocket.deleteAll(); - res.redirect("/home"); - } - }); - register({ - method: Method.GET, - subscription: "/deleteAssets", - secureHandler: async ({ res, isRelease }) => { - if (isRelease) { - return _permission_denied(res, deletionPermissionError); - } - rimraf.sync(filesDirectory); - mkdirSync(filesDirectory); - await DashUploadUtils.buildFileDirectories(); - res.redirect("/delete"); - } - }); - - register({ - method: Method.GET, - subscription: "/deleteWithAux", - secureHandler: async ({ res, isRelease }) => { - if (isRelease) { - return _permission_denied(res, deletionPermissionError); - } - await Database.Auxiliary.DeleteAll(); - res.redirect("/delete"); - } - }); - - register({ - method: Method.GET, - subscription: "/deleteWithGoogleCredentials", - secureHandler: async ({ res, isRelease }) => { - if (isRelease) { - return _permission_denied(res, deletionPermissionError); - } - await Database.Auxiliary.GoogleAuthenticationToken.DeleteAll(); - res.redirect("/delete"); + res.redirect("/home"); } }); } -} - -const deletionPermissionError = "Cannot perform a delete operation outside of the development environment!"; +}
\ No newline at end of file diff --git a/src/server/ApiManagers/GeneralGoogleManager.ts b/src/server/ApiManagers/GeneralGoogleManager.ts index a5240edbc..f94b77cac 100644 --- a/src/server/ApiManagers/GeneralGoogleManager.ts +++ b/src/server/ApiManagers/GeneralGoogleManager.ts @@ -1,10 +1,8 @@ import ApiManager, { Registration } from "./ApiManager"; import { Method, _permission_denied } from "../RouteManager"; import { GoogleApiServerUtils } from "../apis/google/GoogleApiServerUtils"; -import { Database } from "../database"; import RouteSubscriber from "../RouteSubscriber"; - -const deletionPermissionError = "Cannot perform specialized delete outside of the development environment!"; +import { Database } from "../database"; const EndpointHandlerMap = new Map<GoogleApiServerUtils.Action, GoogleApiServerUtils.ApiRouter>([ ["create", (api, params) => api.create(params)], @@ -20,11 +18,11 @@ export default class GeneralGoogleManager extends ApiManager { method: Method.GET, subscription: "/readGoogleAccessToken", secureHandler: async ({ user, res }) => { - const token = await GoogleApiServerUtils.retrieveAccessToken(user.id); - if (!token) { + const { credentials } = (await GoogleApiServerUtils.retrieveCredentials(user.id)); + if (!credentials?.access_token) { return res.send(GoogleApiServerUtils.generateAuthenticationUrl()); } - return res.send(token); + return res.send(credentials); } }); @@ -37,6 +35,15 @@ export default class GeneralGoogleManager extends ApiManager { }); register({ + method: Method.GET, + subscription: "/revokeGoogleAccessToken", + secureHandler: async ({ user, res }) => { + await Database.Auxiliary.GoogleAccessToken.Revoke(user.id); + res.send(); + } + }); + + register({ method: Method.POST, subscription: new RouteSubscriber("googleDocs").add("sector", "action"), secureHandler: async ({ req, res, user }) => { diff --git a/src/server/ApiManagers/GooglePhotosManager.ts b/src/server/ApiManagers/GooglePhotosManager.ts index 88219423d..be17b698e 100644 --- a/src/server/ApiManagers/GooglePhotosManager.ts +++ b/src/server/ApiManagers/GooglePhotosManager.ts @@ -3,7 +3,7 @@ import { Method, _error, _success, _invalid } from "../RouteManager"; import * as path from "path"; import { GoogleApiServerUtils } from "../apis/google/GoogleApiServerUtils"; import { BatchedArray, TimeUnit } from "array-batcher"; -import { Opt } from "../../new_fields/Doc"; +import { Opt } from "../../fields/Doc"; import { DashUploadUtils, InjectSize, SizeSuffix } from "../DashUploadUtils"; import { Database } from "../database"; import { red } from "colors"; @@ -56,7 +56,7 @@ export default class GooglePhotosManager extends ApiManager { const { media } = req.body; // first we need to ensure that we know the google account to which these photos will be uploaded - const token = await GoogleApiServerUtils.retrieveAccessToken(user.id); + const token = (await GoogleApiServerUtils.retrieveCredentials(user.id))?.credentials?.access_token; if (!token) { return _error(res, authenticationError); } diff --git a/src/server/ApiManagers/SessionManager.ts b/src/server/ApiManagers/SessionManager.ts index bcaa6598f..fa2f6002a 100644 --- a/src/server/ApiManagers/SessionManager.ts +++ b/src/server/ApiManagers/SessionManager.ts @@ -55,7 +55,7 @@ export default class SessionManager extends ApiManager { register({ method: Method.GET, - subscription: this.secureSubscriber("delete"), + subscription: this.secureSubscriber("deleteSession"), secureHandler: this.authorizedAction(async ({ res }) => { const { error } = await sessionAgent.serverWorker.emit("delete"); res.send(error ? error.message : "Your request was successful: the server successfully deleted the database. Return to /home."); diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts index 98f029c7d..b185d3b55 100644 --- a/src/server/ApiManagers/UploadManager.ts +++ b/src/server/ApiManagers/UploadManager.ts @@ -171,7 +171,7 @@ export default class UploadManager extends ApiManager { await Promise.all(docs.map((doc: any) => new Promise(res => Database.Instance.replace(doc.id, doc, (err, r) => { err && console.log(err); res(); - }, true, "newDocuments")))); + }, true)))); } catch (e) { console.log(e); } unlink(path_2, () => { }); } diff --git a/src/server/ApiManagers/UserManager.ts b/src/server/ApiManagers/UserManager.ts index d9d346cc1..0d1d8f218 100644 --- a/src/server/ApiManagers/UserManager.ts +++ b/src/server/ApiManagers/UserManager.ts @@ -3,7 +3,7 @@ import { Method } from "../RouteManager"; import { Database } from "../database"; import { msToTime } from "../ActionUtilities"; import * as bcrypt from "bcrypt-nodejs"; -import { Opt } from "../../new_fields/Doc"; +import { Opt } from "../../fields/Doc"; export const timeMap: { [id: string]: number } = {}; interface ActivityUnit { @@ -89,8 +89,6 @@ export default class UserManager extends ApiManager { } }); - - register({ method: Method.GET, subscription: "/activity", diff --git a/src/server/DashSession/DashSessionAgent.ts b/src/server/DashSession/DashSessionAgent.ts index 5cbba13de..ab3dfffcc 100644 --- a/src/server/DashSession/DashSessionAgent.ts +++ b/src/server/DashSession/DashSessionAgent.ts @@ -2,7 +2,7 @@ import { Email, pathFromRoot } from "../ActionUtilities"; import { red, yellow, green, cyan } from "colors"; import { get } from "request-promise"; import { Utils } from "../../Utils"; -import { WebSocket } from "../Websocket/Websocket"; +import { WebSocket } from "../websocket"; import { MessageStore } from "../Message"; import { launchServer, onWindows } from ".."; import { readdirSync, statSync, createWriteStream, readFileSync, unlinkSync } from "fs"; @@ -37,7 +37,7 @@ export class DashSessionAgent extends AppliedSessionAgent { monitor.addReplCommand("debug", [/\S+\@\S+/], async ([to]) => this.dispatchZippedDebugBackup(to)); monitor.on("backup", this.backup); monitor.on("debug", async ({ to }) => this.dispatchZippedDebugBackup(to)); - monitor.on("delete", WebSocket.deleteFields); + monitor.on("delete", WebSocket.doDelete); monitor.coreHooks.onCrashDetected(this.dispatchCrashReport); return sessionKey; } diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts index 8567631cd..b74904ada 100644 --- a/src/server/DashUploadUtils.ts +++ b/src/server/DashUploadUtils.ts @@ -4,7 +4,7 @@ import * as path from 'path'; import * as sharp from 'sharp'; import request = require('request-promise'); import { ExifImage } from 'exif'; -import { Opt } from '../new_fields/Doc'; +import { Opt } from '../fields/Doc'; import { AcceptibleMedia, Upload } from './SharedMediaTypes'; import { filesDirectory, publicDirectory } from '.'; import { File } from 'formidable'; diff --git a/src/server/GarbageCollector.ts b/src/server/GarbageCollector.ts index 5729c3ee5..24745cbb4 100644 --- a/src/server/GarbageCollector.ts +++ b/src/server/GarbageCollector.ts @@ -76,7 +76,7 @@ async function GarbageCollect(full: boolean = true) { if (!fetchIds.length) { continue; } - const docs = await new Promise<{ [key: string]: any }[]>(res => Database.Instance.getDocuments(fetchIds, res, "newDocuments")); + const docs = await new Promise<{ [key: string]: any }[]>(res => Database.Instance.getDocuments(fetchIds, res)); for (const doc of docs) { const id = doc.id; if (doc === undefined) { @@ -116,10 +116,10 @@ async function GarbageCollect(full: boolean = true) { const count = Math.min(toDelete.length, 5000); const toDeleteDocs = toDelete.slice(i, i + count); i += count; - const result = await Database.Instance.delete({ _id: { $in: toDeleteDocs } }, "newDocuments"); + const result = await Database.Instance.delete({ _id: { $in: toDeleteDocs } }); deleted += result.deletedCount || 0; } - // const result = await Database.Instance.delete({ _id: { $in: toDelete } }, "newDocuments"); + // const result = await Database.Instance.delete({ _id: { $in: toDelete } }); console.log(`${deleted} documents deleted`); await Search.deleteDocuments(toDelete); diff --git a/src/server/IDatabase.ts b/src/server/IDatabase.ts index 6a63df485..dd4968579 100644 --- a/src/server/IDatabase.ts +++ b/src/server/IDatabase.ts @@ -2,7 +2,6 @@ import * as mongodb from 'mongodb'; import { Transferable } from './Message'; export const DocumentsCollection = 'documents'; -export const NewDocumentsCollection = 'newDocuments'; export interface IDatabase { update(id: string, value: any, callback: (err: mongodb.MongoError, res: mongodb.UpdateWriteOpResult) => void, upsert?: boolean, collectionName?: string): Promise<void>; updateMany(query: any, update: any, collectionName?: string): Promise<mongodb.WriteOpResult>; @@ -12,12 +11,13 @@ export interface IDatabase { delete(query: any, collectionName?: string): Promise<mongodb.DeleteWriteOpResultObject>; delete(id: string, collectionName?: string): Promise<mongodb.DeleteWriteOpResultObject>; - deleteAll(collectionName?: string, persist?: boolean): Promise<any>; + dropSchema(...schemaNames: string[]): Promise<any>; insert(value: any, collectionName?: string): Promise<void>; getDocument(id: string, fn: (result?: Transferable) => void, collectionName?: string): void; getDocuments(ids: string[], fn: (result: Transferable[]) => void, collectionName?: string): void; + getCollectionNames(): Promise<string[]>; visit(ids: string[], fn: (result: any) => string[] | Promise<string[]>, collectionName?: string): Promise<void>; query(query: { [key: string]: any }, projection?: { [key: string]: 0 | 1 }, collectionName?: string): Promise<mongodb.Cursor>; diff --git a/src/server/MemoryDatabase.ts b/src/server/MemoryDatabase.ts index 543f96e7f..1f1d702d9 100644 --- a/src/server/MemoryDatabase.ts +++ b/src/server/MemoryDatabase.ts @@ -1,4 +1,4 @@ -import { IDatabase, DocumentsCollection, NewDocumentsCollection } from './IDatabase'; +import { IDatabase, DocumentsCollection } from './IDatabase'; import { Transferable } from './Message'; import * as mongodb from 'mongodb'; @@ -15,6 +15,10 @@ export class MemoryDatabase implements IDatabase { } } + public getCollectionNames() { + return Promise.resolve(Object.keys(this.db)); + } + public update(id: string, value: any, callback: (err: mongodb.MongoError, res: mongodb.UpdateWriteOpResult) => void, _upsert?: boolean, collectionName = DocumentsCollection): Promise<void> { const collection = this.getCollection(collectionName); const set = "$set"; @@ -41,7 +45,7 @@ export class MemoryDatabase implements IDatabase { return Promise.resolve(undefined); } - public updateMany(query: any, update: any, collectionName = NewDocumentsCollection): Promise<mongodb.WriteOpResult> { + public updateMany(query: any, update: any, collectionName = DocumentsCollection): Promise<mongodb.WriteOpResult> { throw new Error("Can't updateMany a MemoryDatabase"); } @@ -58,8 +62,15 @@ export class MemoryDatabase implements IDatabase { return Promise.resolve({} as any); } - public deleteAll(collectionName = DocumentsCollection, _persist = true): Promise<any> { - delete this.db[collectionName]; + public async dropSchema(...schemaNames: string[]): Promise<any> { + const existing = await this.getCollectionNames(); + let valid: string[]; + if (schemaNames.length) { + valid = schemaNames.filter(collection => existing.includes(collection)); + } else { + valid = existing; + } + valid.forEach(schemaName => delete this.db[schemaName]); return Promise.resolve(); } @@ -69,14 +80,14 @@ export class MemoryDatabase implements IDatabase { return Promise.resolve(); } - public getDocument(id: string, fn: (result?: Transferable) => void, collectionName = NewDocumentsCollection): void { + public getDocument(id: string, fn: (result?: Transferable) => void, collectionName = DocumentsCollection): void { fn(this.getCollection(collectionName)[id]); } public getDocuments(ids: string[], fn: (result: Transferable[]) => void, collectionName = DocumentsCollection): void { fn(ids.map(id => this.getCollection(collectionName)[id])); } - public async visit(ids: string[], fn: (result: any) => string[] | Promise<string[]>, collectionName = NewDocumentsCollection): Promise<void> { + public async visit(ids: string[], fn: (result: any) => string[] | Promise<string[]>, collectionName = DocumentsCollection): Promise<void> { const visited = new Set<string>(); while (ids.length) { const count = Math.min(ids.length, 1000); diff --git a/src/server/Message.ts b/src/server/Message.ts index 01aae5de7..80f372733 100644 --- a/src/server/Message.ts +++ b/src/server/Message.ts @@ -1,6 +1,6 @@ import { Utils } from "../Utils"; import { Point } from "../pen-gestures/ndollar"; -import { Doc } from "../new_fields/Doc"; +import { Doc } from "../fields/Doc"; import { Image } from "canvas"; import { AnalysisResult, ImportResults } from "../scraping/buxton/final/BuxtonImporter"; diff --git a/src/server/Recommender.ts b/src/server/Recommender.ts index aacdb4053..423ce9b46 100644 --- a/src/server/Recommender.ts +++ b/src/server/Recommender.ts @@ -1,6 +1,6 @@ -// //import { Doc } from "../new_fields/Doc"; -// //import { StrCast } from "../new_fields/Types"; -// //import { List } from "../new_fields/List"; +// //import { Doc } from "../fields/Doc"; +// //import { StrCast } from "../fields/Types"; +// //import { List } from "../fields/List"; // //import { CognitiveServices } from "../client/cognitive_services/CognitiveServices"; // // var w2v = require('word2vec'); diff --git a/src/server/RouteManager.ts b/src/server/RouteManager.ts index 80e4a6741..b23215996 100644 --- a/src/server/RouteManager.ts +++ b/src/server/RouteManager.ts @@ -1,5 +1,5 @@ import RouteSubscriber from "./RouteSubscriber"; -import { DashUserModel } from "./authentication/models/user_model"; +import { DashUserModel } from "./authentication/DashUserModel"; import { Request, Response, Express } from 'express'; import { cyan, red, green } from 'colors'; diff --git a/src/server/credentials/CredentialsLoader.ts b/src/server/apis/google/CredentialsLoader.ts index e3f4d167b..e3f4d167b 100644 --- a/src/server/credentials/CredentialsLoader.ts +++ b/src/server/apis/google/CredentialsLoader.ts diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index 0f75833ee..20f96f432 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -1,11 +1,11 @@ import { google } from "googleapis"; import { OAuth2Client, Credentials, OAuth2ClientOptions } from "google-auth-library"; -import { Opt } from "../../../new_fields/Doc"; +import { Opt } from "../../../fields/Doc"; import { GaxiosResponse } from "gaxios"; import request = require('request-promise'); -import * as qs from 'query-string'; +import * as qs from "query-string"; import { Database } from "../../database"; -import { GoogleCredentialsLoader } from "../../credentials/CredentialsLoader"; +import { GoogleCredentialsLoader } from "./CredentialsLoader"; /** * Scopes give Google users fine granularity of control @@ -149,26 +149,6 @@ export namespace GoogleApiServerUtils { } /** - * Returns the lengthy string or access token that can be passed into - * the headers of an API request or into the constructor of the Photos - * client API wrapper. - * @param userId the Dash user id of the user requesting his/her associated - * access_token - * @returns the current access_token associated with the requesting - * Dash user. The access_token is valid for only an hour, and - * is then refreshed. - */ - export async function retrieveAccessToken(userId: string): Promise<string> { - return new Promise(async resolve => { - const { credentials } = await retrieveCredentials(userId); - if (!credentials) { - return resolve(); - } - resolve(credentials.access_token!); - }); - } - - /** * Manipulates a mapping such that, in the limit, each Dash user has * an associated authenticated OAuth2 client at their disposal. This * function ensures that the client's credentials always remain up to date @@ -217,18 +197,6 @@ export namespace GoogleApiServerUtils { } /** - * This is what we return to the server in processNewUser(), after the - * worker OAuth2Client has used the user-pasted authentication code - * to retrieve an access token and an info token. The avatar is the - * URL to the Google-hosted mono-color, single white letter profile 'image'. - */ - export interface GoogleAuthenticationResult { - access_token: string; - avatar: string; - name: string; - } - - /** * This method receives the authentication code that the * user pasted into the overlay in the client side and uses the worker * and the authentication code to fetch the full set of credentials that @@ -245,7 +213,7 @@ export namespace GoogleApiServerUtils { * and display basic user information in the overlay on successful authentication. * This can be expanded as needed by adding properties to the interface GoogleAuthenticationResult. */ - export async function processNewUser(userId: string, authenticationCode: string): Promise<GoogleAuthenticationResult> { + export async function processNewUser(userId: string, authenticationCode: string): Promise<EnrichedCredentials> { const credentials = await new Promise<Credentials>((resolve, reject) => { worker.getToken(authenticationCode, async (err, credentials) => { if (err || !credentials) { @@ -256,13 +224,8 @@ export namespace GoogleApiServerUtils { }); }); const enriched = injectUserInfo(credentials); - await Database.Auxiliary.GoogleAuthenticationToken.Write(userId, enriched); - const { given_name, picture } = enriched.userInfo; - return { - access_token: enriched.access_token!, - avatar: picture, - name: given_name - }; + await Database.Auxiliary.GoogleAccessToken.Write(userId, enriched); + return enriched; } /** @@ -316,15 +279,15 @@ export namespace GoogleApiServerUtils { * @returns the credentials, or undefined if the user has no stored associated credentials, * and a flag indicating whether or not they were refreshed during retrieval */ - async function retrieveCredentials(userId: string): Promise<{ credentials: Opt<Credentials>, refreshed: boolean }> { - let credentials: Opt<Credentials> = await Database.Auxiliary.GoogleAuthenticationToken.Fetch(userId); + export async function retrieveCredentials(userId: string): Promise<{ credentials: Opt<EnrichedCredentials>, refreshed: boolean }> { + let credentials = await Database.Auxiliary.GoogleAccessToken.Fetch(userId); let refreshed = false; if (!credentials) { return { credentials: undefined, refreshed }; } // check for token expiry if (credentials.expiry_date! <= new Date().getTime()) { - credentials = await refreshAccessToken(credentials, userId); + credentials = { ...credentials, ...(await refreshAccessToken(credentials, userId)) }; refreshed = true; } return { credentials, refreshed }; @@ -355,7 +318,7 @@ export namespace GoogleApiServerUtils { }); // expires_in is in seconds, but we're building the new expiry date in milliseconds const expiry_date = new Date().getTime() + (expires_in * 1000); - await Database.Auxiliary.GoogleAuthenticationToken.Update(userId, access_token, expiry_date); + await Database.Auxiliary.GoogleAccessToken.Update(userId, access_token, expiry_date); // update the relevant properties credentials.access_token = access_token; credentials.expiry_date = expiry_date; diff --git a/src/server/credentials/google_project_credentials.json b/src/server/apis/google/google_project_credentials.json index 955c5a3c1..955c5a3c1 100644 --- a/src/server/credentials/google_project_credentials.json +++ b/src/server/apis/google/google_project_credentials.json diff --git a/src/server/apis/youtube/youtubeApiSample.js b/src/server/apis/youtube/youtubeApiSample.js index 50b3c7b38..d535bd9ff 100644 --- a/src/server/apis/youtube/youtubeApiSample.js +++ b/src/server/apis/youtube/youtubeApiSample.js @@ -1,6 +1,8 @@ const fs = require('fs'); const readline = require('readline'); -const { google } = require('googleapis'); +const { + google +} = require('googleapis'); const OAuth2 = google.auth.OAuth2; @@ -19,21 +21,27 @@ module.exports.readApiKey = (callback) => { } callback(content); }); -} +}; module.exports.authorizedGetChannel = (apiKey) => { //this didnt get called // Authorize a client with the loaded credentials, then call the YouTube API. authorize(JSON.parse(apiKey), getChannel); -} +}; module.exports.authorizedGetVideos = (apiKey, userInput, callBack) => { - authorize(JSON.parse(apiKey), getVideos, { userInput: userInput, callBack: callBack }); -} + authorize(JSON.parse(apiKey), getVideos, { + userInput: userInput, + callBack: callBack + }); +}; module.exports.authorizedGetVideoDetails = (apiKey, videoIds, callBack) => { - authorize(JSON.parse(apiKey), getVideoDetails, { videoIds: videoIds, callBack: callBack }); -} + authorize(JSON.parse(apiKey), getVideoDetails, { + videoIds: videoIds, + callBack: callBack + }); +}; /** diff --git a/src/server/authentication/controllers/user_controller.ts b/src/server/authentication/AuthenticationManager.ts index f0086d4ea..00f1fe44e 100644 --- a/src/server/authentication/controllers/user_controller.ts +++ b/src/server/authentication/AuthenticationManager.ts @@ -1,13 +1,13 @@ -import { default as User, DashUserModel, AuthToken } from "../models/user_model"; +import { default as User, DashUserModel } from "./DashUserModel"; import { Request, Response, NextFunction } from "express"; import * as passport from "passport"; import { IVerifyOptions } from "passport-local"; -import "../config/passport"; +import "./Passport"; import flash = require("express-flash"); import * as async from 'async'; import * as nodemailer from 'nodemailer'; import c = require("crypto"); -import { Utils } from "../../../Utils"; +import { Utils } from "../../Utils"; import { MailOptions } from "nodemailer/lib/stream-transport"; /** @@ -111,7 +111,7 @@ export let postLogin = (req: Request, res: Response, next: NextFunction) => { return res.redirect("/signup"); } - passport.authenticate("local", (err: Error, user: DashUserModel, info: IVerifyOptions) => { + passport.authenticate("local", (err: Error, user: DashUserModel, _info: IVerifyOptions) => { if (err) { next(err); return; } if (!user) { return res.redirect("/signup"); diff --git a/src/server/authentication/models/user_model.ts b/src/server/authentication/DashUserModel.ts index 78e39dbc1..51d920a8f 100644 --- a/src/server/authentication/models/user_model.ts +++ b/src/server/authentication/DashUserModel.ts @@ -58,11 +58,11 @@ userSchema.pre("save", function save(next) { if (!user.isModified("password")) { return next(); } - bcrypt.genSalt(10, (err, salt) => { + bcrypt.genSalt(10, (err: any, salt: string) => { if (err) { return next(err); } - bcrypt.hash(user.password, salt, () => void {}, (err: mongoose.Error, hash) => { + bcrypt.hash(user.password, salt, () => void {}, (err: mongoose.Error, hash: string) => { if (err) { return next(err); } @@ -74,9 +74,9 @@ userSchema.pre("save", function save(next) { const comparePassword: comparePasswordFunction = function (this: DashUserModel, candidatePassword, cb) { // Choose one of the following bodies for authentication logic. - // secure + // secure (expected, default) bcrypt.compare(candidatePassword, this.password, cb); - // bypass password + // bypass password (debugging) // cb(undefined, true); }; diff --git a/src/server/authentication/config/passport.ts b/src/server/authentication/Passport.ts index 286209b20..9b0069414 100644 --- a/src/server/authentication/config/passport.ts +++ b/src/server/authentication/Passport.ts @@ -1,6 +1,6 @@ import * as passport from 'passport'; import * as passportLocal from 'passport-local'; -import { default as User } from '../models/user_model'; +import { default as User } from './DashUserModel'; const LocalStrategy = passportLocal.Strategy; diff --git a/src/server/database.ts b/src/server/database.ts index ad285765b..a5f23c4b1 100644 --- a/src/server/database.ts +++ b/src/server/database.ts @@ -1,10 +1,10 @@ import * as mongodb from 'mongodb'; import { Transferable } from './Message'; -import { Opt } from '../new_fields/Doc'; +import { Opt } from '../fields/Doc'; import { Utils, emptyFunction } from '../Utils'; import { Credentials } from 'google-auth-library'; import { GoogleApiServerUtils } from './apis/google/GoogleApiServerUtils'; -import { IDatabase } from './IDatabase'; +import { IDatabase, DocumentsCollection } from './IDatabase'; import { MemoryDatabase } from './MemoryDatabase'; import * as mongoose from 'mongoose'; import { Upload } from './SharedMediaTypes'; @@ -14,7 +14,7 @@ export namespace Database { export let disconnect: Function; const schema = 'Dash'; const port = 27017; - export const url = `mongodb://localhost:${port}/`; + export const url = `mongodb://localhost:${port}/${schema}`; enum ConnectionStates { disconnected = 0, @@ -47,28 +47,29 @@ export namespace Database { } export class Database implements IDatabase { - public static DocumentsCollection = 'documents'; private MongoClient = mongodb.MongoClient; private currentWrites: { [id: string]: Promise<void> } = {}; private db?: mongodb.Db; private onConnect: (() => void)[] = []; - doConnect() { + async doConnect() { console.error(`\nConnecting to Mongo with URL : ${url}\n`); - this.MongoClient.connect(url, { connectTimeoutMS: 30000, socketTimeoutMS: 30000, useUnifiedTopology: true }, (_err, client) => { - console.error("mongo connect response\n"); - if (!client) { - console.error("\nMongo connect failed with the error:\n"); - console.log(_err); - process.exit(0); - } - this.db = client.db(); - this.onConnect.forEach(fn => fn()); + return new Promise<void>(resolve => { + this.MongoClient.connect(url, { connectTimeoutMS: 30000, socketTimeoutMS: 30000, useUnifiedTopology: true }, (_err, client) => { + console.error("mongo connect response\n"); + if (!client) { + console.error("\nMongo connect failed with the error:\n"); + console.log(_err); + process.exit(0); + } + this.db = client.db(); + this.onConnect.forEach(fn => fn()); + resolve(); + }); }); } - public async update(id: string, value: any, callback: (err: mongodb.MongoError, res: mongodb.UpdateWriteOpResult) => void, upsert = true, collectionName = Database.DocumentsCollection) { - + public async update(id: string, value: any, callback: (err: mongodb.MongoError, res: mongodb.UpdateWriteOpResult) => void, upsert = true, collectionName = DocumentsCollection) { if (this.db) { const collection = this.db.collection(collectionName); const prom = this.currentWrites[id]; @@ -93,7 +94,7 @@ export namespace Database { } } - public replace(id: string, value: any, callback: (err: mongodb.MongoError, res: mongodb.UpdateWriteOpResult) => void, upsert = true, collectionName = Database.DocumentsCollection) { + public replace(id: string, value: any, callback: (err: mongodb.MongoError, res: mongodb.UpdateWriteOpResult) => void, upsert = true, collectionName = DocumentsCollection) { if (this.db) { const collection = this.db.collection(collectionName); const prom = this.currentWrites[id]; @@ -117,9 +118,21 @@ export namespace Database { } } + public async getCollectionNames() { + const cursor = this.db?.listCollections(); + const collectionNames: string[] = []; + if (cursor) { + while (await cursor.hasNext()) { + const collection: any = await cursor.next(); + collection && collectionNames.push(collection.name); + } + } + return collectionNames; + } + public delete(query: any, collectionName?: string): Promise<mongodb.DeleteWriteOpResultObject>; public delete(id: string, collectionName?: string): Promise<mongodb.DeleteWriteOpResultObject>; - public delete(id: any, collectionName = Database.DocumentsCollection) { + public delete(id: any, collectionName = DocumentsCollection) { if (typeof id === "string") { id = { _id: id }; } @@ -131,25 +144,26 @@ export namespace Database { } } - public async deleteAll(collectionName = Database.DocumentsCollection, persist = true): Promise<any> { - return new Promise(resolve => { - const executor = async (database: mongodb.Db) => { - if (persist) { - await database.collection(collectionName).deleteMany({}); - } else { - await database.dropCollection(collectionName); - } - resolve(); - }; - if (this.db) { - executor(this.db); + public async dropSchema(...targetSchemas: string[]): Promise<any> { + const executor = async (database: mongodb.Db) => { + const existing = await Instance.getCollectionNames(); + let valid: string[]; + if (targetSchemas.length) { + valid = targetSchemas.filter(collection => existing.includes(collection)); } else { - this.onConnect.push(() => this.db && executor(this.db)); + valid = existing; } - }); + const pending = Promise.all(valid.map(schemaName => database.dropCollection(schemaName))); + return (await pending).every(dropOutcome => dropOutcome); + }; + if (this.db) { + return executor(this.db); + } else { + this.onConnect.push(() => this.db && executor(this.db)); + } } - public async insert(value: any, collectionName = Database.DocumentsCollection) { + public async insert(value: any, collectionName = DocumentsCollection) { if (this.db) { if ("id" in value) { value._id = value.id; @@ -177,7 +191,7 @@ export namespace Database { } } - public getDocument(id: string, fn: (result?: Transferable) => void, collectionName = "newDocuments") { + public getDocument(id: string, fn: (result?: Transferable) => void, collectionName = DocumentsCollection) { if (this.db) { this.db.collection(collectionName).findOne({ _id: id }, (err, result) => { if (result) { @@ -193,7 +207,7 @@ export namespace Database { } } - public getDocuments(ids: string[], fn: (result: Transferable[]) => void, collectionName = Database.DocumentsCollection) { + public getDocuments(ids: string[], fn: (result: Transferable[]) => void, collectionName = DocumentsCollection) { if (this.db) { this.db.collection(collectionName).find({ _id: { "$in": ids } }).toArray((err, docs) => { if (err) { @@ -211,7 +225,7 @@ export namespace Database { } } - public async visit(ids: string[], fn: (result: any) => string[] | Promise<string[]>, collectionName = "newDocuments"): Promise<void> { + public async visit(ids: string[], fn: (result: any) => string[] | Promise<string[]>, collectionName = DocumentsCollection): Promise<void> { if (this.db) { const visited = new Set<string>(); while (ids.length) { @@ -238,7 +252,7 @@ export namespace Database { } } - public query(query: { [key: string]: any }, projection?: { [key: string]: 0 | 1 }, collectionName = "newDocuments"): Promise<mongodb.Cursor> { + public query(query: { [key: string]: any }, projection?: { [key: string]: 0 | 1 }, collectionName = DocumentsCollection): Promise<mongodb.Cursor> { if (this.db) { let cursor = this.db.collection(collectionName).find(query); if (projection) { @@ -252,7 +266,7 @@ export namespace Database { } } - public updateMany(query: any, update: any, collectionName = "newDocuments") { + public updateMany(query: any, update: any, collectionName = DocumentsCollection) { if (this.db) { const db = this.db; return new Promise<mongodb.WriteOpResult>(res => db.collection(collectionName).update(query, update, (_, result) => res(result))); @@ -277,14 +291,28 @@ export namespace Database { } } - export const Instance: IDatabase = getDatabase(); + export const Instance = getDatabase(); + /** + * Provides definitions and apis for working with + * portions of the database not dedicated to storing documents + * or Dash-internal user data. + */ export namespace Auxiliary { + /** + * All the auxiliary MongoDB collections (schemas) + */ export enum AuxiliaryCollections { - GooglePhotosUploadHistory = "uploadedFromGooglePhotos" + GooglePhotosUploadHistory = "uploadedFromGooglePhotos", + GoogleAccess = "googleAuthentication" } + /** + * Searches for the @param query in the specified @param collection, + * and returns at most the first @param cap results. If @param removeId is true, + * as it is by default, each object will be stripped of its database id. + */ const SanitizedCappedQuery = async (query: { [key: string]: any }, collection: string, cap: number, removeId = true) => { const cursor = await Instance.query(query, undefined, collection); const results = await cursor.toArray(); @@ -295,54 +323,88 @@ export namespace Database { }) : slice; }; + /** + * Searches for the @param query in the specified @param collection, + * and returns at most the first result. If @param removeId is true, + * as it is by default, each object will be stripped of its database id. + * Worth the special case since it converts the Array return type to a single + * object of the specified type. + */ const SanitizedSingletonQuery = async <T>(query: { [key: string]: any }, collection: string, removeId = true): Promise<Opt<T>> => { const results = await SanitizedCappedQuery(query, collection, 1, removeId); return results.length ? results[0] : undefined; }; + /** + * Checks to see if an image with the given @param contentSize + * already exists in the aux database, i.e. has already been downloaded from Google Photos. + */ export const QueryUploadHistory = async (contentSize: number) => { return SanitizedSingletonQuery<Upload.ImageInformation>({ contentSize }, AuxiliaryCollections.GooglePhotosUploadHistory); }; - export namespace GoogleAuthenticationToken { - - const GoogleAuthentication = "googleAuthentication"; - - export type StoredCredentials = Credentials & { _id: string }; + /** + * Records the uploading of the image with the given @param information, + * using the given content size as a seed for the database id. + */ + export const LogUpload = async (information: Upload.ImageInformation) => { + const bundle = { + _id: Utils.GenerateDeterministicGuid(String(information.contentSize)), + ...information + }; + return Instance.insert(bundle, AuxiliaryCollections.GooglePhotosUploadHistory); + }; + /** + * Manages the storage, retrieval and updating of the access token that + * facilitates interactions with all their APIs for a given account. + */ + export namespace GoogleAccessToken { + + /** + * Format stored in database. + */ + type StoredCredentials = GoogleApiServerUtils.EnrichedCredentials & { _id: string }; + + /** + * Retrieves the credentials associaed with @param userId + * and optionally removes their database id according to @param removeId. + */ export const Fetch = async (userId: string, removeId = true): Promise<Opt<StoredCredentials>> => { - return SanitizedSingletonQuery<StoredCredentials>({ userId }, GoogleAuthentication, removeId); + return SanitizedSingletonQuery<StoredCredentials>({ userId }, AuxiliaryCollections.GoogleAccess, removeId); }; + /** + * Writes the @param enrichedCredentials to the database, associated + * with @param userId for later retrieval and updating. + */ export const Write = async (userId: string, enrichedCredentials: GoogleApiServerUtils.EnrichedCredentials) => { - return Instance.insert({ userId, canAccess: [], ...enrichedCredentials }, GoogleAuthentication); + return Instance.insert({ userId, canAccess: [], ...enrichedCredentials }, AuxiliaryCollections.GoogleAccess); }; + /** + * Updates the @param access_token and @param expiry_date fields + * in the stored credentials associated with @param userId. + */ export const Update = async (userId: string, access_token: string, expiry_date: number) => { const entry = await Fetch(userId, false); if (entry) { const parameters = { $set: { access_token, expiry_date } }; - return Instance.update(entry._id, parameters, emptyFunction, true, GoogleAuthentication); + return Instance.update(entry._id, parameters, emptyFunction, true, AuxiliaryCollections.GoogleAccess); } }; - export const DeleteAll = () => Instance.deleteAll(GoogleAuthentication, false); - - } - - export const LogUpload = async (information: Upload.ImageInformation) => { - const bundle = { - _id: Utils.GenerateDeterministicGuid(String(information.contentSize)), - ...information + /** + * Revokes the credentials associated with @param userId. + */ + export const Revoke = async (userId: string) => { + const entry = await Fetch(userId, false); + if (entry) { + Instance.delete({ _id: entry._id }, AuxiliaryCollections.GoogleAccess); + } }; - return Instance.insert(bundle, AuxiliaryCollections.GooglePhotosUploadHistory); - }; - export const DeleteAll = async (persist = false) => { - const collectionNames = Object.values(AuxiliaryCollections); - const pendingDeletions = collectionNames.map(name => Instance.deleteAll(name, persist)); - return Promise.all(pendingDeletions); - }; + } } diff --git a/src/server/index.ts b/src/server/index.ts index f26c8a6ab..af8f95a5e 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -11,9 +11,9 @@ import * as qs from 'query-string'; import UtilManager from './ApiManagers/UtilManager'; import { SearchManager } from './ApiManagers/SearchManager'; import UserManager from './ApiManagers/UserManager'; -import { WebSocket } from './Websocket/Websocket'; +import { WebSocket } from './websocket'; import DownloadManager from './ApiManagers/DownloadManager'; -import { GoogleCredentialsLoader } from './credentials/CredentialsLoader'; +import { GoogleCredentialsLoader } from './apis/google/CredentialsLoader'; import DeleteManager from "./ApiManagers/DeleteManager"; import PDFManager from "./ApiManagers/PDFManager"; import UploadManager from "./ApiManagers/UploadManager"; @@ -25,7 +25,6 @@ import { yellow } from "colors"; import { DashSessionAgent } from "./DashSession/DashSessionAgent"; import SessionManager from "./ApiManagers/SessionManager"; import { AppliedSessionAgent } from "./DashSession/Session/agents/applied_session_agent"; -import { Utils } from "../Utils"; export const onWindows = process.platform === "win32"; export let sessionAgent: AppliedSessionAgent; @@ -125,7 +124,7 @@ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }: // initialize the web socket (bidirectional communication: if a user changes // a field on one client, that change must be broadcast to all other clients) - WebSocket.start(isRelease); + WebSocket.initialize(isRelease); } diff --git a/src/server/remapUrl.ts b/src/server/remapUrl.ts index 45d2fdd33..91a3cb6bf 100644 --- a/src/server/remapUrl.ts +++ b/src/server/remapUrl.ts @@ -1,6 +1,4 @@ import { Database } from "./database"; -import { Search } from "./Search"; -import * as path from 'path'; //npx ts-node src/server/remapUrl.ts @@ -50,7 +48,7 @@ async function update() { return new Promise(res => Database.Instance.update(doc[0], doc[1], () => { console.log("wrote " + JSON.stringify(doc[1])); res(); - }, false, "newDocuments")); + }, false)); })); console.log("Done"); // await Promise.all(updates.map(update => { diff --git a/src/server/server_Initialization.ts b/src/server/server_Initialization.ts index add607761..4b3094616 100644 --- a/src/server/server_Initialization.ts +++ b/src/server/server_Initialization.ts @@ -7,7 +7,7 @@ import * as cookieParser from 'cookie-parser'; import expressFlash = require('express-flash'); import flash = require('connect-flash'); import { Database } from './database'; -import { getForgot, getLogin, getLogout, getReset, getSignup, postForgot, postLogin, postReset, postSignup } from './authentication/controllers/user_controller'; +import { getForgot, getLogin, getLogout, getReset, getSignup, postForgot, postLogin, postReset, postSignup } from './authentication/AuthenticationManager'; const MongoStore = require('connect-mongo')(session); import RouteManager from './RouteManager'; import * as webpack from 'webpack'; @@ -94,6 +94,8 @@ function determineEnvironment() { const label = isRelease ? "release" : "development"; console.log(`\nrunning server in ${color(label)} mode`); + // swilkins: I don't think we need to read from ClientUtils.RELEASE anymore. Should be able to invoke process.env.RELEASE + // on the client side, thanks to dotenv in webpack.config.js let clientUtils = fs.readFileSync("./src/client/util/ClientUtils.ts.temp", "utf8"); clientUtils = `//AUTO-GENERATED FILE: DO NOT EDIT\n${clientUtils.replace('"mode"', String(isRelease))}`; fs.writeFileSync("./src/client/util/ClientUtils.ts", clientUtils, "utf8"); diff --git a/src/server/Websocket/Websocket.ts b/src/server/websocket.ts index be895c4bc..7278bdc32 100644 --- a/src/server/Websocket/Websocket.ts +++ b/src/server/websocket.ts @@ -1,17 +1,18 @@ -import { Utils } from "../../Utils"; -import { MessageStore, Transferable, Types, Diff, YoutubeQueryInput, YoutubeQueryTypes, GestureContent, MobileInkOverlayContent, UpdateMobileInkOverlayPositionContent, MobileDocumentUploadContent, RoomMessage } from "../Message"; -import { Client } from "../Client"; +import { Utils } from "../Utils"; +import { MessageStore, Transferable, Types, Diff, YoutubeQueryInput, YoutubeQueryTypes, GestureContent, MobileInkOverlayContent, UpdateMobileInkOverlayPositionContent, MobileDocumentUploadContent, RoomMessage } from "./Message"; +import { Client } from "./Client"; import { Socket } from "socket.io"; -import { Database } from "../database"; -import { Search } from "../Search"; +import { Database } from "./database"; +import { Search } from "./Search"; import * as io from 'socket.io'; -import YoutubeApi from "../apis/youtube/youtubeApiSample"; -import { GoogleCredentialsLoader } from "../credentials/CredentialsLoader"; -import { logPort } from "../ActionUtilities"; -import { timeMap } from "../ApiManagers/UserManager"; +import YoutubeApi from "./apis/youtube/youtubeApiSample"; +import { GoogleCredentialsLoader } from "./apis/google/CredentialsLoader"; +import { logPort } from "./ActionUtilities"; +import { timeMap } from "./ApiManagers/UserManager"; import { green } from "colors"; import { networkInterfaces } from "os"; -import executeImport from "../../scraping/buxton/final/BuxtonImporter"; +import executeImport from "../scraping/buxton/final/BuxtonImporter"; +import { DocumentsCollection } from "./IDatabase"; export namespace WebSocket { @@ -20,15 +21,7 @@ export namespace WebSocket { export const socketMap = new Map<SocketIO.Socket, string>(); export let disconnect: Function; - - export async function start(isRelease: boolean) { - await preliminaryFunctions(); - initialize(isRelease); - } - - async function preliminaryFunctions() { - } - function initialize(isRelease: boolean) { + export function initialize(isRelease: boolean) { const endpoint = io(); endpoint.on("connection", function (socket: Socket) { _socket = socket; @@ -96,7 +89,7 @@ export namespace WebSocket { Utils.AddServerHandlerCallback(socket, MessageStore.GetField, getField); Utils.AddServerHandlerCallback(socket, MessageStore.GetFields, getFields); if (isRelease) { - Utils.AddServerHandler(socket, MessageStore.DeleteAll, deleteFields); + Utils.AddServerHandler(socket, MessageStore.DeleteAll, () => doDelete(false)); } Utils.AddServerHandler(socket, MessageStore.CreateField, CreateField); @@ -163,24 +156,10 @@ export namespace WebSocket { } } - export async function deleteFields() { - await Database.Instance.deleteAll(); - if (process.env.DISABLE_SEARCH !== "true") { - await Search.clear(); - } - await Database.Instance.deleteAll('newDocuments'); - } - - // export async function deleteUserDocuments() { - // await Database.Instance.deleteAll(); - // await Database.Instance.deleteAll('newDocuments'); - // } - - export async function deleteAll() { - await Database.Instance.deleteAll(); - await Database.Instance.deleteAll('newDocuments'); - await Database.Instance.deleteAll('sessions'); - await Database.Instance.deleteAll('users'); + export async function doDelete(onlyFields = true) { + const target: string[] = []; + onlyFields && target.push(DocumentsCollection); + await Database.Instance.dropSchema(...target); if (process.env.DISABLE_SEARCH !== "true") { await Search.clear(); } @@ -210,11 +189,11 @@ export namespace WebSocket { } function GetRefField([id, callback]: [string, (result?: Transferable) => void]) { - Database.Instance.getDocument(id, callback, "newDocuments"); + Database.Instance.getDocument(id, callback); } function GetRefFields([ids, callback]: [string[], (result?: Transferable[]) => void]) { - Database.Instance.getDocuments(ids, callback, "newDocuments"); + Database.Instance.getDocuments(ids, callback); } const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = { @@ -271,7 +250,7 @@ export namespace WebSocket { function UpdateField(socket: Socket, diff: Diff) { Database.Instance.update(diff.id, diff.diff, - () => socket.broadcast.emit(MessageStore.UpdateField.Message, diff), false, "newDocuments"); + () => socket.broadcast.emit(MessageStore.UpdateField.Message, diff), false); const docfield = diff.diff.$set || diff.diff.$unset; if (!docfield) { return; @@ -296,7 +275,7 @@ export namespace WebSocket { } function DeleteField(socket: Socket, id: string) { - Database.Instance.delete({ _id: id }, "newDocuments").then(() => { + Database.Instance.delete({ _id: id }).then(() => { socket.broadcast.emit(MessageStore.DeleteField.Message, id); }); @@ -304,14 +283,14 @@ export namespace WebSocket { } function DeleteFields(socket: Socket, ids: string[]) { - Database.Instance.delete({ _id: { $in: ids } }, "newDocuments").then(() => { + Database.Instance.delete({ _id: { $in: ids } }).then(() => { socket.broadcast.emit(MessageStore.DeleteFields.Message, ids); }); Search.deleteDocuments(ids); } function CreateField(newValue: any) { - Database.Instance.insert(newValue, "newDocuments"); + Database.Instance.insert(newValue); } } diff --git a/test/test.ts b/test/test.ts index 245733e9b..5fc156b46 100644 --- a/test/test.ts +++ b/test/test.ts @@ -8,10 +8,10 @@ const dom = new JSDOM("", { import { autorun, reaction } from "mobx"; -import { Doc } from '../src/new_fields/Doc'; -import { Cast } from '../src/new_fields/Types'; -import { createSchema, makeInterface, defaultSpec } from '../src/new_fields/Schema'; -import { ImageField } from '../src/new_fields/URLField'; +import { Doc } from '../src/fields/Doc'; +import { Cast } from '../src/fields/Types'; +import { createSchema, makeInterface, defaultSpec } from '../src/fields/Schema'; +import { ImageField } from '../src/fields/URLField'; describe("Document", () => { it('should hold fields', () => { const key = "Test"; diff --git a/webpack.config.js b/webpack.config.js index 6265883fd..67d492e1f 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -16,22 +16,21 @@ const plugins = [ new webpack.HotModuleReplacementPlugin(), ]; -const dotenv = require('dotenv'); - -function transferEnvironmentVariables() { +(function transferEnvironmentVariables() { const prefix = "_CLIENT_"; - const env = dotenv.config().parsed; - if (env) { - plugins.push(new webpack.DefinePlugin(Object.keys(env).reduce((mapping, envKey) => { - if (envKey.startsWith(prefix)) { - mapping[`process.env.${envKey.replace(prefix, "")}`] = JSON.stringify(env[envKey]); - } - return mapping; - }, {}))); + const { + parsed + } = require('dotenv').config(); + if (!parsed) { + return; } -} - -transferEnvironmentVariables(); + plugins.push(new webpack.DefinePlugin(Object.keys(parsed).reduce((mapping, envKey) => { + if (envKey.startsWith(prefix)) { + mapping[`process.env.${envKey.replace(prefix, "")}`] = JSON.stringify(parsed[envKey]); + } + return mapping; + }, {}))); +})(); module.exports = { mode: 'development', |