diff options
50 files changed, 1106 insertions, 351 deletions
diff --git a/.vscode/launch.json b/.vscode/launch.json index e4196600e..d2c18d6f1 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -12,6 +12,9 @@ "breakOnLoad": true, "url": "http://localhost:1050/login", "webRoot": "${workspaceFolder}", + "runtimeArgs": [ + "--experimental-modules" + ] }, { "type": "firefox", diff --git a/package.json b/package.json index 0ff9306b3..d699d1e6f 100644 --- a/package.json +++ b/package.json @@ -117,8 +117,8 @@ "bluebird": "^3.5.3", "body-parser": "^1.18.3", "bootstrap": "^4.3.1", - "child_process": "^1.0.2", "canvas": "^2.5.0", + "child_process": "^1.0.2", "class-transformer": "^0.2.0", "connect-flash": "^0.1.1", "connect-mongo": "^2.0.3", @@ -160,7 +160,7 @@ "nodemailer": "^5.1.1", "nodemon": "^1.18.10", "normalize.css": "^8.0.1", - "npm": "^6.9.0", + "npm": "^6.10.3", "p-limit": "^2.2.0", "passport": "^0.4.0", "passport-local": "^1.0.0", @@ -214,6 +214,7 @@ "typescript-collections": "^1.3.2", "url-loader": "^1.1.2", "uuid": "^3.3.2", + "words-to-numbers": "^1.5.1", "xoauth2": "^1.2.0", "youtube": "^0.1.0" } diff --git a/src/Utils.ts b/src/Utils.ts index bad875f0d..959b89fe5 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -3,6 +3,7 @@ import v5 = require("uuid/v5"); import { Socket } from 'socket.io'; import { Message } from './server/Message'; import { RouteStore } from './server/RouteStore'; +import requestPromise = require('request-promise'); export class Utils { @@ -175,4 +176,14 @@ export namespace JSONUtils { return results; } +} + +export function PostToServer(relativeRoute: string, body: any) { + let options = { + method: "POST", + uri: Utils.prepend(relativeRoute), + json: true, + body: body + }; + return requestPromise.post(options); }
\ No newline at end of file diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index 87a87be92..977eb7772 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -1,6 +1,6 @@ import * as OpenSocket from 'socket.io-client'; import { MessageStore, Diff, YoutubeQueryTypes } from "./../server/Message"; -import { Opt } from '../new_fields/Doc'; +import { Opt, Doc } from '../new_fields/Doc'; import { Utils, emptyFunction } from '../Utils'; import { SerializationHelper } from './util/SerializationHelper'; import { RefField } from '../new_fields/RefField'; @@ -26,6 +26,41 @@ export namespace DocServer { let GUID: string; // indicates whether or not a document is currently being udpated, and, if so, its id + export enum WriteMode { + Always = 0, + None = 1, + SameUser = 2, + } + + const fieldWriteModes: { [field: string]: WriteMode } = {}; + const docsWithUpdates: { [field: string]: Set<Doc> } = {}; + + export function setFieldWriteMode(field: string, writeMode: WriteMode) { + fieldWriteModes[field] = writeMode; + if (writeMode === WriteMode.Always) { + const docs = docsWithUpdates[field]; + if (docs) { + docs.forEach(doc => Doc.RunCachedUpdate(doc, field)); + delete docsWithUpdates[field]; + } + } + } + + export function getFieldWriteMode(field: string) { + return fieldWriteModes[field]; + } + + export function registerDocWithCachedUpdate(doc: Doc, field: string, oldValue: any) { + let list = docsWithUpdates[field]; + if (!list) { + list = docsWithUpdates[field] = new Set; + } + if (!list.has(doc)) { + Doc.AddCachedUpdate(doc, field, oldValue); + list.add(doc); + } + } + export function init(protocol: string, hostname: string, port: number, identifier: string) { _cache = {}; GUID = identifier; diff --git a/src/client/cognitive_services/CognitiveServices.ts b/src/client/cognitive_services/CognitiveServices.ts index c118d91d3..08fcb4883 100644 --- a/src/client/cognitive_services/CognitiveServices.ts +++ b/src/client/cognitive_services/CognitiveServices.ts @@ -7,9 +7,9 @@ import { Utils } from "../../Utils"; import { InkData } from "../../new_fields/InkField"; import { UndoManager } from "../util/UndoManager"; -type APIManager<D> = { converter: BodyConverter<D>, requester: RequestExecutor, analyzer: AnalysisApplier }; +type APIManager<D> = { converter: BodyConverter<D>, requester: RequestExecutor }; type RequestExecutor = (apiKey: string, body: string, service: Service) => Promise<string>; -type AnalysisApplier = (target: Doc, relevantKeys: string[], ...args: any) => any; +type AnalysisApplier<D> = (target: Doc, relevantKeys: string[], data: D, ...args: any) => any; type BodyConverter<D> = (data: D) => string; type Converter = (results: any) => Field; @@ -38,7 +38,7 @@ export enum Confidence { */ export namespace CognitiveServices { - const ExecuteQuery = async <D, R>(service: Service, manager: APIManager<D>, data: D): Promise<Opt<R>> => { + const ExecuteQuery = async <D>(service: Service, manager: APIManager<D>, data: D): Promise<any> => { return fetch(Utils.prepend(`${RouteStore.cognitiveServices}/${service}`)).then(async response => { let apiKey = await response.text(); if (!apiKey) { @@ -46,7 +46,7 @@ export namespace CognitiveServices { return undefined; } - let results: Opt<R>; + let results: any; try { results = await manager.requester(apiKey, manager.converter(data), service).then(json => JSON.parse(json)); } catch { @@ -99,7 +99,11 @@ export namespace CognitiveServices { return request.post(options); }, - analyzer: async (target: Doc, keys: string[], url: string, service: Service, converter: Converter) => { + }; + + export namespace Appliers { + + export const ProcessImage: AnalysisApplier<string> = async (target: Doc, keys: string[], url: string, service: Service, converter: Converter) => { let batch = UndoManager.StartBatch("Image Analysis"); let storageKey = keys[0]; @@ -107,7 +111,7 @@ export namespace CognitiveServices { return; } let toStore: any; - let results = await ExecuteQuery<string, any>(service, Manager, url); + let results = await ExecuteQuery(service, Manager, url); if (!results) { toStore = "Cognitive Services could not process the given image URL."; } else { @@ -120,9 +124,9 @@ export namespace CognitiveServices { target[storageKey] = toStore; batch.end(); - } + }; - }; + } export type Face = { faceAttributes: any, faceId: string, faceRectangle: Rectangle }; @@ -179,10 +183,14 @@ export namespace CognitiveServices { return new Promise<any>(promisified); }, - analyzer: async (target: Doc, keys: string[], inkData: InkData) => { + }; + + export namespace Appliers { + + export const ConcatenateHandwriting: AnalysisApplier<InkData> = async (target: Doc, keys: string[], inkData: InkData) => { let batch = UndoManager.StartBatch("Ink Analysis"); - let results = await ExecuteQuery<InkData, any>(Service.Handwriting, Manager, inkData); + let results = await ExecuteQuery(Service.Handwriting, Manager, inkData); if (results) { results.recognitionUnits && (results = results.recognitionUnits); target[keys[0]] = Docs.Get.DocumentHierarchyFromJson(results, "Ink Analysis"); @@ -192,9 +200,9 @@ export namespace CognitiveServices { } batch.end(); - } + }; - }; + } export interface AzureStrokeData { id: number; diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index e804d5440..7dd853156 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -1,3 +1,24 @@ +export enum DocumentType { + NONE = "none", + TEXT = "text", + HIST = "histogram", + IMG = "image", + WEB = "web", + COL = "collection", + KVP = "kvp", + VID = "video", + AUDIO = "audio", + PDF = "pdf", + ICON = "icon", + IMPORT = "import", + LINK = "link", + LINKDOC = "linkdoc", + BUTTON = "button", + TEMPLATE = "template", + EXTENSION = "extension", + YOUTUBE = "youtube", +} + import { HistogramField } from "../northstar/dash-fields/HistogramField"; import { HistogramBox } from "../northstar/dash-nodes/HistogramBox"; import { HistogramOperation } from "../northstar/operations/HistogramOperation"; @@ -25,14 +46,13 @@ import { OmitKeys, JSONUtils } from "../../Utils"; 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, ToConstructor, InterfaceValue, FieldValue } from "../../new_fields/Types"; +import { Cast, NumCast } from "../../new_fields/Types"; import { IconField } from "../../new_fields/IconField"; import { listSpec } from "../../new_fields/Schema"; import { DocServer } from "../DocServer"; import { dropActionType } from "../util/DragManager"; import { DateField } from "../../new_fields/DateField"; import { UndoManager } from "../util/UndoManager"; -import { RouteStore } from "../../server/RouteStore"; import { YoutubeBox } from "../apis/youtube/YoutubeBox"; import { CollectionDockingView } from "../views/collections/CollectionDockingView"; import { LinkManager } from "../util/LinkManager"; @@ -46,27 +66,6 @@ import { ProxyField } from "../../new_fields/Proxy"; var requestImageSize = require('../util/request-image-size'); var path = require('path'); -export enum DocumentType { - NONE = "none", - TEXT = "text", - HIST = "histogram", - IMG = "image", - WEB = "web", - COL = "collection", - KVP = "kvp", - VID = "video", - AUDIO = "audio", - PDF = "pdf", - ICON = "icon", - IMPORT = "import", - LINK = "link", - LINKDOC = "linkdoc", - BUTTON = "button", - TEMPLATE = "template", - EXTENSION = "extension", - YOUTUBE = "youtube", -} - export interface DocumentOptions { x?: number; y?: number; @@ -84,6 +83,7 @@ export interface DocumentOptions { templates?: List<string>; viewType?: number; backgroundColor?: string; + opacity?: number; defaultBackgroundColor?: string; dropAction?: dropActionType; backgroundLayout?: string; @@ -596,7 +596,7 @@ export namespace Docs { export namespace DocUtils { - export function MakeLink(source: Doc, target: Doc, targetContext?: Doc, title: string = "", description: string = "", tags: string = "Default", sourceContext?: Doc) { + export function MakeLink(source: Doc, target: Doc, targetContext?: Doc, title: string = "", description: string = "", sourceContext?: Doc) { if (LinkManager.Instance.doesLinkExist(source, target)) return undefined; let sv = DocumentManager.Instance.getDocumentView(source); if (sv && sv.props.ContainingCollectionView && sv.props.ContainingCollectionView.props.Document === target) return; @@ -610,7 +610,6 @@ export namespace DocUtils { linkDocProto.sourceContext = sourceContext; linkDocProto.title = title === "" ? source.title + " to " + target.title : title; linkDocProto.linkDescription = description; - linkDocProto.linkTags = tags; linkDocProto.type = DocumentType.LINK; linkDocProto.anchor1 = source; @@ -622,7 +621,7 @@ export namespace DocUtils { LinkManager.Instance.addLink(linkDocProto); - let script = `return links(this)};`; + let script = `return links(this);`; let computed = CompileScript(script, { params: { this: "Doc" }, typecheck: false }); computed.compiled && (Doc.GetProto(source).links = new ComputedField(computed)); computed.compiled && (Doc.GetProto(target).links = new ComputedField(computed)); diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts index b58bdb6c7..9c61fe125 100644 --- a/src/client/util/DictationManager.ts +++ b/src/client/util/DictationManager.ts @@ -1,38 +1,348 @@ -namespace CORE { - export interface IWindow extends Window { - webkitSpeechRecognition: any; +import { SelectionManager } from "./SelectionManager"; +import { DocumentView } from "../views/nodes/DocumentView"; +import { UndoManager } from "./UndoManager"; +import * as interpreter from "words-to-numbers"; +import { Doc } from "../../new_fields/Doc"; +import { List } from "../../new_fields/List"; +import { Docs, DocumentType } from "../documents/Documents"; +import { CollectionViewType } from "../views/collections/CollectionBaseView"; +import { Cast, CastCtor } from "../../new_fields/Types"; +import { listSpec } from "../../new_fields/Schema"; +import { AudioField, ImageField } from "../../new_fields/URLField"; +import { HistogramField } from "../northstar/dash-fields/HistogramField"; +import { MainView } from "../views/MainView"; +import { Utils } from "../../Utils"; + +/** + * This namespace provides a singleton instance of a manager that + * handles the listening and text-conversion of user speech. + * + * The basic manager functionality can be attained by the DictationManager.Controls namespace, which provide + * a simple recording operation that returns the interpreted text as a string. + * + * Additionally, however, the DictationManager also exposes the ability to execute voice commands within Dash. + * It stores a default library of registered commands that can be triggered by listen()'ing for a phrase and then + * passing the results into the execute() function. + * + * In addition to compile-time default commands, you can invoke DictationManager.Commands.Register(Independent|Dependent) + * to add new commands as classes or components are constructed. + */ +export namespace DictationManager { + + /** + * Some type maneuvering to access Webkit's built-in + * speech recognizer. + */ + namespace CORE { + export interface IWindow extends Window { + webkitSpeechRecognition: any; + } } -} + const { webkitSpeechRecognition }: CORE.IWindow = window as CORE.IWindow; + export const placeholder = "Listening..."; -const { webkitSpeechRecognition }: CORE.IWindow = window as CORE.IWindow; + export namespace Controls { -export default class DictationManager { - public static Instance = new DictationManager(); - private isListening = false; - private recognizer: any; + const infringe = "unable to process: dictation manager still involved in previous session"; + const intraSession = ". "; + const interSession = " ... "; - constructor() { - this.recognizer = new webkitSpeechRecognition(); - this.recognizer.interimResults = false; - this.recognizer.continuous = true; - } + let isListening = false; + let isManuallyStopped = false; - finish = (handler: any, data: any) => { - handler(data); - this.isListening = false; - this.recognizer.stop(); - } + let current: string | undefined = undefined; + let sessionResults: string[] = []; + + const recognizer: SpeechRecognition = new webkitSpeechRecognition() || new SpeechRecognition(); + recognizer.onstart = () => console.log("initiating speech recognition session..."); + + export type InterimResultHandler = (results: string) => any; + export type ContinuityArgs = { indefinite: boolean } | false; + export type DelimiterArgs = { inter: string, intra: string }; + export type ListeningUIStatus = { interim: boolean } | false; - listen = () => { - if (this.isListening) { - return undefined; + export interface ListeningOptions { + language: string; + continuous: ContinuityArgs; + delimiters: DelimiterArgs; + interimHandler: InterimResultHandler; + tryExecute: boolean; } - this.isListening = true; - this.recognizer.start(); - return new Promise<string>((resolve, reject) => { - this.recognizer.onresult = (e: any) => this.finish(resolve, e.results[0][0].transcript); - this.recognizer.onerror = (e: any) => this.finish(reject, e); - }); + + export const listen = async (options?: Partial<ListeningOptions>) => { + let results: string | undefined; + let main = MainView.Instance; + + main.dictationOverlayVisible = true; + main.isListening = { interim: false }; + + try { + results = await listenImpl(options); + if (results) { + Utils.CopyText(results); + main.isListening = false; + let execute = options && options.tryExecute; + main.dictatedPhrase = execute ? results.toLowerCase() : results; + main.dictationSuccess = execute ? await DictationManager.Commands.execute(results) : true; + } + } catch (e) { + main.isListening = false; + main.dictatedPhrase = results = `dictation error: ${"error" in e ? e.error : "unknown error"}`; + main.dictationSuccess = false; + } finally { + main.initiateDictationFade(); + } + + return results; + }; + + const listenImpl = (options?: Partial<ListeningOptions>) => { + if (isListening) { + return infringe; + } + isListening = true; + + let handler = options ? options.interimHandler : undefined; + let continuous = options ? options.continuous : undefined; + let indefinite = continuous && continuous.indefinite; + let language = options ? options.language : undefined; + let intra = options && options.delimiters ? options.delimiters.intra : undefined; + let inter = options && options.delimiters ? options.delimiters.inter : undefined; + + recognizer.interimResults = handler !== undefined; + recognizer.continuous = continuous === undefined ? false : continuous !== false; + recognizer.lang = language === undefined ? "en-US" : language; + + recognizer.start(); + + return new Promise<string>((resolve, reject) => { + + recognizer.onerror = (e: SpeechRecognitionError) => { + if (!(indefinite && e.error === "no-speech")) { + recognizer.stop(); + reject(e); + } + }; + + recognizer.onresult = (e: SpeechRecognitionEvent) => { + current = synthesize(e, intra); + handler && handler(current); + isManuallyStopped && complete(); + }; + + recognizer.onend = (e: Event) => { + if (!indefinite || isManuallyStopped) { + return complete(); + } + + if (current) { + sessionResults.push(current); + current = undefined; + } + recognizer.start(); + }; + + let complete = () => { + if (indefinite) { + current && sessionResults.push(current); + sessionResults.length && resolve(sessionResults.join(inter || interSession)); + } else { + resolve(current); + } + reset(); + }; + + }); + }; + + export const stop = (salvageSession = true) => { + if (!isListening) { + return; + } + isManuallyStopped = true; + salvageSession ? recognizer.stop() : recognizer.abort(); + let main = MainView.Instance; + if (main.dictationOverlayVisible) { + main.cancelDictationFade(); + main.dictationOverlayVisible = false; + main.dictationSuccess = undefined; + setTimeout(() => main.dictatedPhrase = placeholder, 500); + } + }; + + const synthesize = (e: SpeechRecognitionEvent, delimiter?: string) => { + let results = e.results; + let transcripts: string[] = []; + for (let i = 0; i < results.length; i++) { + transcripts.push(results.item(i).item(0).transcript.trim()); + } + return transcripts.join(delimiter || intraSession); + }; + + const reset = () => { + current = undefined; + sessionResults = []; + isListening = false; + isManuallyStopped = false; + recognizer.onresult = null; + recognizer.onerror = null; + recognizer.onend = null; + }; + + } + + export namespace Commands { + + export const dictationFadeDuration = 2000; + + export type IndependentAction = (target: DocumentView) => any | Promise<any>; + export type IndependentEntry = { action: IndependentAction, restrictTo?: DocumentType[] }; + + export type DependentAction = (target: DocumentView, matches: RegExpExecArray) => any | Promise<any>; + export type DependentEntry = { expression: RegExp, action: DependentAction, restrictTo?: DocumentType[] }; + + export const RegisterIndependent = (key: string, value: IndependentEntry) => Independent.set(key, value); + export const RegisterDependent = (entry: DependentEntry) => Dependent.push(entry); + + export const execute = async (phrase: string) => { + return UndoManager.RunInBatch(async () => { + let targets = SelectionManager.SelectedDocuments(); + if (!targets || !targets.length) { + return; + } + + phrase = phrase.toLowerCase(); + let entry = Independent.get(phrase); + + if (entry) { + let success = false; + let restrictTo = entry.restrictTo; + for (let target of targets) { + if (!restrictTo || validate(target, restrictTo)) { + await entry.action(target); + success = true; + } + } + return success; + } + + for (let entry of Dependent) { + let regex = entry.expression; + let matches = regex.exec(phrase); + regex.lastIndex = 0; + if (matches !== null) { + let success = false; + let restrictTo = entry.restrictTo; + for (let target of targets) { + if (!restrictTo || validate(target, restrictTo)) { + await entry.action(target, matches); + success = true; + } + } + return success; + } + } + + return false; + }, "Execute Command"); + }; + + const ConstructorMap = new Map<DocumentType, CastCtor>([ + [DocumentType.COL, listSpec(Doc)], + [DocumentType.AUDIO, AudioField], + [DocumentType.IMG, ImageField], + [DocumentType.HIST, HistogramField], + [DocumentType.IMPORT, listSpec(Doc)], + [DocumentType.TEXT, "string"] + ]); + + const tryCast = (view: DocumentView, type: DocumentType) => { + let ctor = ConstructorMap.get(type); + if (!ctor) { + return false; + } + return Cast(Doc.GetProto(view.props.Document).data, ctor) !== undefined; + }; + + const validate = (target: DocumentView, types: DocumentType[]) => { + for (let type of types) { + if (tryCast(target, type)) { + return true; + } + } + return false; + }; + + const interpretNumber = (number: string) => { + let initial = parseInt(number); + if (!isNaN(initial)) { + return initial; + } + let converted = interpreter.wordsToNumbers(number, { fuzzy: true }); + if (converted === null) { + return NaN; + } + return typeof converted === "string" ? parseInt(converted) : converted; + }; + + const Independent = new Map<string, IndependentEntry>([ + + ["clear", { + action: (target: DocumentView) => Doc.GetProto(target.props.Document).data = new List(), + restrictTo: [DocumentType.COL] + }], + + ["open fields", { + action: (target: DocumentView) => { + let kvp = Docs.Create.KVPDocument(target.props.Document, { width: 300, height: 300 }); + target.props.addDocTab(kvp, target.dataDoc, "onRight"); + } + }], + + ["promote", { + action: (target: DocumentView) => { + console.log(target); + }, + restrictTo: [DocumentType.TEXT] + }] + + ]); + + const Dependent = new Array<DependentEntry>( + + { + expression: /create (\w+) documents of type (image|nested collection)/g, + action: (target: DocumentView, matches: RegExpExecArray) => { + let count = interpretNumber(matches[1]); + let what = matches[2]; + let dataDoc = Doc.GetProto(target.props.Document); + let fieldKey = "data"; + for (let i = 0; i < count; i++) { + let created: Doc | undefined; + switch (what) { + case "image": + created = Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg"); + break; + case "nested collection": + created = Docs.Create.FreeformDocument([], {}); + break; + } + created && Doc.AddDocToList(dataDoc, fieldKey, created); + } + }, + restrictTo: [DocumentType.COL] + }, + + { + expression: /view as (freeform|stacking|masonry|schema|tree)/g, + action: (target: DocumentView, matches: RegExpExecArray) => { + let mode = CollectionViewType.valueOf(matches[1]); + mode && (target.props.Document.viewType = mode); + }, + restrictTo: [DocumentType.COL] + } + + ); } diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 32f728c71..7f526b247 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 } from '../../new_fields/Doc'; +import { Doc, DocListCastAsync } from '../../new_fields/Doc'; import { Id } from '../../new_fields/FieldSymbols'; -import { BoolCast, Cast, NumCast } from '../../new_fields/Types'; +import { Cast, NumCast } from '../../new_fields/Types'; import { CollectionDockingView } from '../views/collections/CollectionDockingView'; import { CollectionPDFView } from '../views/collections/CollectionPDFView'; import { CollectionVideoView } from '../views/collections/CollectionVideoView'; @@ -104,7 +104,7 @@ export class DocumentManager { @computed public get LinkedDocumentViews() { - let pairs = DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush)).reduce((pairs, dv) => { + let pairs = DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || Doc.IsBrushed(dv.props.Document)).reduce((pairs, dv) => { let linksList = LinkManager.Instance.getAllRelatedLinks(dv.props.Document); pairs.push(...linksList.reduce((pairs, link) => { if (link) { @@ -138,15 +138,17 @@ export class DocumentManager { let docView: DocumentView | null; // using forceDockFunc as a flag for splitting linked to doc to the right...can change later if needed if (!forceDockFunc && (docView = DocumentManager.Instance.getDocumentView(doc))) { - docView.props.Document.libraryBrush = true; + Doc.BrushDoc(docView.props.Document); if (linkPage !== undefined) docView.props.Document.curPage = linkPage; - UndoManager.RunInBatch(() => { - docView!.props.focus(docView!.props.Document, willZoom); - }, "focus"); + UndoManager.RunInBatch(() => docView!.props.focus(docView!.props.Document, willZoom), "focus"); } else { if (!contextDoc) { - if (docContext) { + let docs = docContext ? await DocListCastAsync(docContext.data) : undefined; + let found = false; + docs && docs.map(d => found = found || Doc.AreProtosEqual(d, docDelegate)); + if (docContext && found) { let targetContextView: DocumentView | null; + if (!forceDockFunc && docContext && (targetContextView = DocumentManager.Instance.getDocumentView(docContext))) { docContext.panTransformType = "Ease"; targetContextView.props.focus(docDelegate, willZoom); @@ -158,13 +160,13 @@ export class DocumentManager { } } else { const actualDoc = Doc.MakeAlias(docDelegate); - actualDoc.libraryBrush = true; + Doc.BrushDoc(actualDoc); if (linkPage !== undefined) actualDoc.curPage = linkPage; (dockFunc || CollectionDockingView.Instance.AddRightSplit)(actualDoc, undefined); } } else { let contextView: DocumentView | null; - docDelegate.libraryBrush = true; + Doc.BrushDoc(docDelegate); if (!forceDockFunc && (contextView = DocumentManager.Instance.getDocumentView(contextDoc))) { contextDoc.panTransformType = "Ease"; contextView.props.focus(docDelegate, willZoom); diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index 448a8e9cf..8a668e8d8 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -252,3 +252,4 @@ export class LinkManager { Scripting.addGlobal(function links(doc: any) { return new List(LinkManager.Instance.getAllRelatedLinks(doc)); }); + diff --git a/src/client/util/type_decls.d b/src/client/util/type_decls.d index 79a4e50d5..622e10960 100644 --- a/src/client/util/type_decls.d +++ b/src/client/util/type_decls.d @@ -74,6 +74,7 @@ interface String { normalize(form: "NFC" | "NFD" | "NFKC" | "NFKD"): string; normalize(form?: string): string; repeat(count: number): string; + replace(a:any, b:any):string; // bcz: fix this startsWith(searchString: string, position?: number): boolean; anchor(name: string): string; big(): string; diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx index a1787e78f..90f7be33f 100644 --- a/src/client/views/ContextMenuItem.tsx +++ b/src/client/views/ContextMenuItem.tsx @@ -39,13 +39,13 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select handleEvent = async (e: React.MouseEvent<HTMLDivElement>) => { if ("event" in this.props) { + this.props.closeMenu && this.props.closeMenu(); let batch: UndoManager.Batch | undefined; if (this.props.undoable !== false) { batch = UndoManager.StartBatch(`Context menu event: ${this.props.description}`); } await this.props.event(); batch && batch.end(); - this.props.closeMenu && this.props.closeMenu(); } } diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index ea2e3e196..e773014e3 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -3,12 +3,9 @@ import { SelectionManager } from "../util/SelectionManager"; import { CollectionDockingView } from "./collections/CollectionDockingView"; import { MainView } from "./MainView"; import { DragManager } from "../util/DragManager"; -import { action } from "mobx"; +import { action, runInAction } from "mobx"; import { Doc } from "../../new_fields/Doc"; -import { CognitiveServices } from "../cognitive_services/CognitiveServices"; -import DictationManager from "../util/DictationManager"; -import { ContextMenu } from "./ContextMenu"; -import { ContextMenuProps } from "./ContextMenuItem"; +import { DictationManager } from "../util/DictationManager"; const modifiers = ["control", "meta", "shift", "alt"]; type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo | Promise<KeyControlInfo>; @@ -62,7 +59,8 @@ export default class KeyManager { private unmodified = action((keyname: string, e: KeyboardEvent) => { switch (keyname) { case "escape": - if (MainView.Instance.isPointerDown) { + let main = MainView.Instance; + if (main.isPointerDown) { DragManager.AbortDrag(); } else { if (CollectionDockingView.Instance.HasFullScreen()) { @@ -71,8 +69,9 @@ export default class KeyManager { SelectionManager.DeselectAll(); } } - MainView.Instance.toggleColorPicker(true); + main.toggleColorPicker(true); SelectionManager.DeselectAll(); + DictationManager.Controls.stop(); break; case "delete": case "backspace": @@ -106,13 +105,9 @@ export default class KeyManager { switch (keyname) { case " ": - let transcript = await DictationManager.Instance.listen(); - console.log(`I heard${transcript ? `: ${transcript.toLowerCase()}` : " nothing: I thought I was still listening from an earlier session."}`); - let command: ContextMenuProps | undefined; - transcript && (command = ContextMenu.Instance.findByDescription(transcript, true)) && "event" in command && command.event(); + DictationManager.Controls.listen({ tryExecute: true }); stopPropagation = true; preventDefault = true; - break; } return { diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss index eed2ae4fa..f76abaff3 100644 --- a/src/client/views/Main.scss +++ b/src/client/views/Main.scss @@ -266,4 +266,33 @@ ul#add-options-list { height: 25%; position: relative; display: flex; +} + +.dictation-prompt { + position: absolute; + z-index: 1000; + text-align: center; + justify-content: center; + align-self: center; + align-content: center; + padding: 20px; + background: gainsboro; + border-radius: 10px; + border: 3px solid black; + box-shadow: #00000044 5px 5px 10px; + transform: translate(-50%, -50%); + top: 50%; + font-style: italic; + left: 50%; + transition: 0.5s all ease; + pointer-events: none; +} + +.dictation-prompt-overlay { + width: 100%; + height: 100%; + position: absolute; + z-index: 999; + transition: 0.5s all ease; + pointer-events: none; }
\ No newline at end of file diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 5fd42c0df..0e687737d 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -41,7 +41,7 @@ let swapDocs = async () => { await swapDocs(); document.getElementById('root')!.addEventListener('wheel', event => { if (event.ctrlKey) { - event.preventDefault() + event.preventDefault(); } }, true); ReactDOM.render(<MainView />, document.getElementById('root')); diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 53f589684..7629a0906 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -1,7 +1,7 @@ import { IconName, library } from '@fortawesome/fontawesome-svg-core'; import { faArrowDown, faCloudUploadAlt, faArrowUp, faClone, faCheck, faPlay, faPause, faCaretUp, faLongArrowAltRight, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faPortrait, faMusic, faObjectGroup, faPenNib, faRedoAlt, faTable, faThumbtack, faTree, faUndoAlt, faCat, faBolt } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, configure, observable, runInAction, reaction, trace } from 'mobx'; +import { action, computed, configure, observable, runInAction, reaction, trace, autorun } from 'mobx'; import { observer } from 'mobx-react'; import "normalize.css"; import * as React from 'react'; @@ -39,6 +39,7 @@ import { FilterBox } from './search/FilterBox'; import { CollectionTreeView } from './collections/CollectionTreeView'; import { ClientUtils } from '../util/ClientUtils'; import { SchemaHeaderField, RandomPastel } from '../../new_fields/SchemaHeaderField'; +import { DictationManager } from '../util/DictationManager'; @observer export class MainView extends React.Component { @@ -47,6 +48,30 @@ export class MainView extends React.Component { @observable private _workspacesShown: boolean = false; @observable public pwidth: number = 0; @observable public pheight: number = 0; + + @observable private dictationState = DictationManager.placeholder; + @observable private dictationSuccessState: boolean | undefined = undefined; + @observable private dictationDisplayState = false; + @observable private dictationListeningState: DictationManager.Controls.ListeningUIStatus = false; + + public overlayTimeout: NodeJS.Timeout | undefined; + + public initiateDictationFade = () => { + let duration = DictationManager.Commands.dictationFadeDuration; + this.overlayTimeout = setTimeout(() => { + this.dictationOverlayVisible = false; + this.dictationSuccess = undefined; + setTimeout(() => this.dictatedPhrase = DictationManager.placeholder, 500); + }, duration); + } + + public cancelDictationFade = () => { + if (this.overlayTimeout) { + clearTimeout(this.overlayTimeout); + this.overlayTimeout = undefined; + } + } + @computed private get mainContainer(): Opt<Doc> { return FieldValue(Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc)); } @@ -64,6 +89,38 @@ export class MainView extends React.Component { } } + @computed public get dictatedPhrase() { + return this.dictationState; + } + + public set dictatedPhrase(value: string) { + runInAction(() => this.dictationState = value); + } + + @computed public get dictationSuccess() { + return this.dictationSuccessState; + } + + public set dictationSuccess(value: boolean | undefined) { + runInAction(() => this.dictationSuccessState = value); + } + + @computed public get dictationOverlayVisible() { + return this.dictationDisplayState; + } + + public set dictationOverlayVisible(value: boolean) { + runInAction(() => this.dictationDisplayState = value); + } + + @computed public get isListening() { + return this.dictationListeningState; + } + + public set isListening(value: DictationManager.Controls.ListeningUIStatus) { + runInAction(() => this.dictationListeningState = value); + } + componentWillMount() { var tag = document.createElement('script'); @@ -378,6 +435,7 @@ export class MainView extends React.Component { } + private mode: DocServer.WriteMode = DocServer.WriteMode.Always; @observable private _colorPickerDisplay = false; /* for the expandable add nodes menu. Not included with the miscbuttons because once it expands it expands the whole div with it, making canvas interactions limited. */ nodesMenu() { @@ -422,6 +480,13 @@ export class MainView extends React.Component { </button> </div></li>)} <li key="undoTest"><button className="add-button round-button" title="Click if undo isn't working" onClick={() => UndoManager.TraceOpenBatches()}><FontAwesomeIcon icon="exclamation" size="sm" /></button></li> + <li key="test"><button className="add-button round-button" title="asdf" onClick={() => { + this.mode++; + this.mode = this.mode % 3; + console.log(DocServer.WriteMode[this.mode]); + DocServer.setFieldWriteMode("x", this.mode); + DocServer.setFieldWriteMode("y", this.mode); + }}><FontAwesomeIcon icon="exclamation" size="sm" /></button></li> <li key="color"><button className="add-button round-button" title="Select Color" style={{ zIndex: 1000 }} onClick={() => this.toggleColorPicker()}><div className="toolbar-color-button" style={{ backgroundColor: InkingControl.Instance.selectedColor }} > <div className="toolbar-color-picker" onClick={this.onColorClick} style={this._colorPickerDisplay ? { color: "black", display: "block" } : { color: "black", display: "none" }}> <SketchPicker color={InkingControl.Instance.selectedColor} onChange={InkingControl.Instance.switchColor} /> @@ -464,9 +529,35 @@ export class MainView extends React.Component { this.isSearchVisible = !this.isSearchVisible; } + private get dictationOverlay() { + let display = this.dictationOverlayVisible; + let success = this.dictationSuccess; + let result = this.isListening && !this.isListening.interim ? DictationManager.placeholder : `"${this.dictatedPhrase}"`; + return ( + <div> + <div + className={"dictation-prompt"} + style={{ + opacity: display ? 1 : 0, + background: success === undefined ? "gainsboro" : success ? "lawngreen" : "red", + borderColor: this.isListening ? "red" : "black", + }} + >{result}</div> + <div + className={"dictation-prompt-overlay"} + style={{ + opacity: display ? 0.4 : 0, + backgroundColor: this.isListening ? "red" : "darkslategrey" + }} + /> + </div> + ); + } + render() { return ( <div id="main-div"> + {this.dictationOverlay} <DocumentDecorations /> {this.mainContent} <PreviewCursor /> diff --git a/src/client/views/SearchItem.tsx b/src/client/views/SearchItem.tsx index 13e4b88f7..fd4b2420d 100644 --- a/src/client/views/SearchItem.tsx +++ b/src/client/views/SearchItem.tsx @@ -37,12 +37,10 @@ export class SearchItem extends React.Component<SearchProps> { return <FontAwesomeIcon icon={button} className="documentView-minimizedIcon" />; } onPointerEnter = (e: React.PointerEvent) => { - this.props.doc.libraryBrush = true; - Doc.SetOnPrototype(this.props.doc, "protoBrush", true); + Doc.BrushDoc(this.props.doc); } onPointerLeave = (e: React.PointerEvent) => { - this.props.doc.libraryBrush = false; - Doc.SetOnPrototype(this.props.doc, "protoBrush", false); + Doc.UnBrushDoc(this.props.doc); } collectionRef = React.createRef<HTMLDivElement>(); diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx index 6801b94fd..cad87ebcc 100644 --- a/src/client/views/collections/CollectionBaseView.tsx +++ b/src/client/views/collections/CollectionBaseView.tsx @@ -22,6 +22,24 @@ export enum CollectionViewType { Masonry } +export namespace CollectionViewType { + + const stringMapping = new Map<string, CollectionViewType>([ + ["invalid", CollectionViewType.Invalid], + ["freeform", CollectionViewType.Freeform], + ["schema", CollectionViewType.Schema], + ["docking", CollectionViewType.Docking], + ["tree", CollectionViewType.Tree], + ["stacking", CollectionViewType.Stacking], + ["masonry", CollectionViewType.Masonry] + ]); + + export const valueOf = (value: string) => { + return stringMapping.get(value.toLowerCase()); + }; + +} + export interface CollectionRenderProps { addDocument: (document: Doc, allowDuplicates?: boolean) => boolean; removeDocument: (document: Doc) => boolean; @@ -81,7 +99,7 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { addDocument(doc: Doc, allowDuplicates: boolean = false): boolean { var curPage = NumCast(this.props.Document.curPage, -1); Doc.GetProto(doc).page = curPage; - if (curPage >= 0) { + if (this.props.fieldExt) { // bcz: fieldExt !== undefined means this is an overlay layer Doc.GetProto(doc).annotationOn = this.props.Document; } allowDuplicates = true; @@ -108,8 +126,7 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { let value = Cast(targetDataDoc[targetField], listSpec(Doc), []); let index = value.reduce((p, v, i) => (v instanceof Doc && v[Id] === doc[Id]) ? i : p, -1); PromiseValue(Cast(doc.annotationOn, Doc)).then(annotationOn => - annotationOn === this.dataDoc.Document && (doc.annotationOn = undefined) - ); + annotationOn === this.dataDoc.Document && (doc.annotationOn = undefined)); if (index !== -1) { value.splice(index, 1); diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index f559480ed..77b698a07 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -18,7 +18,6 @@ import { SelectionManager } from '../../util/SelectionManager'; import { Transform } from '../../util/Transform'; import { undoBatch, UndoManager } from "../../util/UndoManager"; import { DocumentView } from "../nodes/DocumentView"; -import { CollectionViewType } from './CollectionBaseView'; import "./CollectionDockingView.scss"; import { SubCollectionViewProps } from "./CollectionSubView"; import { ParentDocSelector } from './ParentDocumentSelector'; @@ -410,10 +409,10 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp tab.reactComponents = [dragSpan, upDiv]; tab.element.append(dragSpan); tab.element.append(upDiv); - tab.reactionDisposer = reaction(() => [doc.title], - () => { - tab.titleElement[0].textContent = doc.title; - }, { fireImmediately: true }); + tab.reactionDisposer = reaction(() => [doc.title, Doc.IsBrushedDegree(doc)], () => { + tab.titleElement[0].textContent = doc.title, { fireImmediately: true }; + tab.titleElement[0].style.outline = `${["transparent", "white", "white"][Doc.IsBrushedDegree(doc)]} ${["none", "dashed", "solid"][Doc.IsBrushedDegree(doc)]} 1px`; + }); //TODO why can't this just be doc instead of the id? tab.titleElement[0].DashDocId = tab.contentItem.config.props.documentId; } @@ -421,9 +420,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp tab.titleElement[0].Tab = tab; tab.closeElement.off('click') //unbind the current click handler .click(async function () { - if (tab.reactionDisposer) { - tab.reactionDisposer(); - } + tab.reactionDisposer && tab.reactionDisposer(); let doc = await DocServer.GetRefField(tab.contentItem.config.props.documentId); if (doc instanceof Doc) { let theDoc = doc; @@ -511,7 +508,7 @@ interface DockedFrameProps { } @observer export class DockedFrameRenderer extends React.Component<DockedFrameProps> { - _mainCont = React.createRef<HTMLDivElement>(); + _mainCont: HTMLDivElement | undefined = undefined; @observable private _panelWidth = 0; @observable private _panelHeight = 0; @observable private _document: Opt<Doc>; @@ -551,6 +548,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { private onActiveContentItemChanged() { if (this.props.glContainer.tab) { this._isActive = this.props.glContainer.tab.isActive; + !this._isActive && this._document && Doc.UnBrushDoc(this._document); // bcz: bad -- trying to simulate a pointer leave event when a new tab is opened up on top of an existing one. } } @@ -569,9 +567,9 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { } ScreenToLocalTransform = () => { - if (this._mainCont.current && this._mainCont.current.children) { - let { scale, translateX, translateY } = Utils.GetScreenTransform(this._mainCont.current.children[0].firstChild as HTMLElement); - scale = Utils.GetScreenTransform(this._mainCont.current).scale; + if (this._mainCont && this._mainCont!.children) { + let { scale, translateX, translateY } = Utils.GetScreenTransform(this._mainCont.children[0].firstChild as HTMLElement); + scale = Utils.GetScreenTransform(this._mainCont).scale; return CollectionDockingView.Instance.props.ScreenToLocalTransform().translate(-translateX, -translateY).scale(1 / this.contentScaling() / scale); } return Transform.Identity(); @@ -616,7 +614,13 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { @computed get content() { return ( - <div className="collectionDockingView-content" ref={this._mainCont} + <div className="collectionDockingView-content" ref={action((ref: HTMLDivElement) => { + this._mainCont = ref; + if (ref) { + this._panelWidth = Number(getComputedStyle(ref).width!.replace("px", "")); + this._panelHeight = Number(getComputedStyle(ref).height!.replace("px", "")); + } + })} style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px)` }}> {this.docView} </div >); diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 75787c0a8..ebfa737be 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -50,7 +50,7 @@ const columnTypes: Map<string, ColumnType> = new Map([ ["title", ColumnType.String], ["x", ColumnType.Number], ["y", ColumnType.Number], ["width", ColumnType.Number], ["height", ColumnType.Number], ["nativeWidth", ColumnType.Number], ["nativeHeight", ColumnType.Number], ["isPrototype", ColumnType.Boolean], - ["page", ColumnType.Number], ["curPage", ColumnType.Number], ["libraryBrush", ColumnType.Boolean], ["zIndex", ColumnType.Number] + ["page", ColumnType.Number], ["curPage", ColumnType.Number], ["zIndex", ColumnType.Number] ]); @observer diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index e03acea10..cfd4df9fc 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -127,19 +127,19 @@ class TreeView extends React.Component<TreeViewProps> { onPointerDown = (e: React.PointerEvent) => e.stopPropagation(); onPointerEnter = (e: React.PointerEvent): void => { - this.props.active() && (this.props.document.libraryBrush = true); + this.props.active() && Doc.BrushDoc(this.dataDoc); if (e.buttons === 1 && SelectionManager.GetIsDragging()) { this._header!.current!.className = "treeViewItem-header"; document.addEventListener("pointermove", this.onDragMove, true); } } onPointerLeave = (e: React.PointerEvent): void => { - this.props.document.libraryBrush = false; + Doc.UnBrushDoc(this.dataDoc); this._header!.current!.className = "treeViewItem-header"; document.removeEventListener("pointermove", this.onDragMove, true); } onDragMove = (e: PointerEvent): void => { - this.props.document.libraryBrush = false; + Doc.UnBrushDoc(this.dataDoc); let x = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY); let rect = this._header!.current!.getBoundingClientRect(); let bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2); @@ -359,7 +359,7 @@ class TreeView extends React.Component<TreeViewProps> { return <> <div className="docContainer" id={`docContainer-${this.props.parentKey}`} ref={reference} onPointerDown={onItemDown} style={{ - background: BoolCast(this.props.document.libraryBrush) ? "#06121212" : "0", + background: Doc.IsBrushed(this.props.document) ? "#06121212" : "0", outline: BoolCast(this.props.document.workspaceBrush) ? "dashed 1px #06123232" : undefined, pointerEvents: this.props.active() || SelectionManager.GetIsDragging() ? "all" : "none" }} > diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index f59fee985..7a402798e 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -1,6 +1,6 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faEye } from '@fortawesome/free-regular-svg-icons'; -import { faColumns, faEllipsisV, faFingerprint, faImage, faProjectDiagram, faSignature, faSquare, faTh, faThList, faTree } from '@fortawesome/free-solid-svg-icons'; +import { faColumns, faEllipsisV, faFingerprint, faImage, faProjectDiagram, faSignature, faSquare, faTh, faThList, faTree, faCopy } from '@fortawesome/free-solid-svg-icons'; import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; import { observer } from "mobx-react"; import * as React from 'react'; @@ -20,7 +20,7 @@ import { CollectionTreeView } from "./CollectionTreeView"; import { CollectionViewBaseChrome } from './CollectionViewChromes'; export const COLLECTION_BORDER_WIDTH = 2; -library.add(faTh, faTree, faSquare, faProjectDiagram, faSignature, faThList, faFingerprint, faColumns, faEllipsisV, faImage, faEye as any); +library.add(faTh, faTree, faSquare, faProjectDiagram, faSignature, faThList, faFingerprint, faColumns, faEllipsisV, faImage, faEye as any, faCopy); @observer export class CollectionView extends React.Component<FieldViewProps> { @@ -86,7 +86,12 @@ export class CollectionView extends React.Component<FieldViewProps> { onContextMenu = (e: React.MouseEvent): void => { if (!this.isAnnotationOverlay && !e.isPropagationStopped() && this.props.Document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 let subItems: ContextMenuProps[] = []; - subItems.push({ description: "Freeform", event: () => this.props.Document.viewType = CollectionViewType.Freeform, icon: "signature" }); + subItems.push({ + description: "Freeform", event: () => { + this.props.Document.viewType = CollectionViewType.Freeform; + delete this.props.Document.usePivotLayout; + }, icon: "signature" + }); if (CollectionBaseView.InSafeMode()) { ContextMenu.Instance.addItem({ description: "Test Freeform", event: () => this.props.Document.viewType = CollectionViewType.Invalid, icon: "project-diagram" }); } @@ -97,6 +102,7 @@ export class CollectionView extends React.Component<FieldViewProps> { switch (this.props.Document.viewType) { case CollectionViewType.Freeform: { subItems.push({ description: "Custom", icon: "fingerprint", event: CollectionFreeFormView.AddCustomLayout(this.props.Document, this.props.fieldKey) }); + subItems.push({ description: "Pivot", icon: "copy", event: () => this.props.Document.usePivotLayout = true }); break; } } diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx index b0ba62173..7787a8eed 100644 --- a/src/client/views/collections/CollectionViewChromes.tsx +++ b/src/client/views/collections/CollectionViewChromes.tsx @@ -3,7 +3,7 @@ import { CollectionView } from "./CollectionView"; import "./CollectionViewChromes.scss"; import { CollectionViewType } from "./CollectionBaseView"; import { undoBatch } from "../../util/UndoManager"; -import { action, observable, runInAction, computed, IObservable, IObservableValue } from "mobx"; +import { action, observable, runInAction, computed, IObservable, IObservableValue, reaction, autorun } from "mobx"; import { observer } from "mobx-react"; import { Doc, DocListCast } from "../../../new_fields/Doc"; import { DocLike } from "../MetadataEntryMenu"; @@ -193,6 +193,36 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro } } + private get document() { + return this.props.CollectionView.props.Document; + } + + private get pivotKey() { + return StrCast(this.document.pivotField); + } + + private set pivotKey(value: string) { + this.document.pivotField = value; + } + + @observable private pivotKeyDisplay = this.pivotKey; + getPivotInput = () => { + if (!this.document.usePivotLayout) { + return (null); + } + return (<input className="collectionViewBaseChrome-viewSpecsInput" + placeholder="PIVOT ON..." + value={this.pivotKeyDisplay} + onChange={action((e: React.ChangeEvent<HTMLInputElement>) => this.pivotKeyDisplay = e.currentTarget.value)} + onKeyPress={action((e: React.KeyboardEvent<HTMLInputElement>) => { + let value = e.currentTarget.value; + if (e.which === 13) { + this.pivotKey = value; + this.pivotKeyDisplay = ""; + } + })} />); + } + render() { return ( <div className="collectionViewChrome-cont" style={{ top: this._collapsed ? -70 : 0 }}> @@ -225,6 +255,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro value={this.filterValue ? this.filterValue.script.originalScript : ""} onChange={(e) => { }} onPointerDown={this.openViewSpecs} /> + {this.getPivotInput()} <div className="collectionViewBaseChrome-viewSpecsMenu" onPointerDown={this.openViewSpecs} style={{ diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 8322625f1..ba8dcff98 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -3,7 +3,7 @@ import { faEye } from "@fortawesome/free-regular-svg-icons"; import { faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faPaintBrush, faTable, faUpload, faChalkboard, faBraille } from "@fortawesome/free-solid-svg-icons"; import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCastAsync, HeightSym, WidthSym } from "../../../../new_fields/Doc"; +import { Doc, DocListCastAsync, HeightSym, WidthSym, DocListCast, FieldResult, Field, Opt } from "../../../../new_fields/Doc"; import { Id } from "../../../../new_fields/FieldSymbols"; import { InkField, StrokeData } from "../../../../new_fields/InkField"; import { createSchema, makeInterface } from "../../../../new_fields/Schema"; @@ -29,14 +29,16 @@ import { DocumentViewProps, positionSchema } from "../../nodes/DocumentView"; import { pageSchema } from "../../nodes/ImageBox"; import { OverlayElementOptions, OverlayView } from "../../OverlayView"; import PDFMenu from "../../pdf/PDFMenu"; -import { ScriptBox } from "../../ScriptBox"; import { CollectionSubView } from "../CollectionSubView"; +import { ScriptBox } from "../../ScriptBox"; import { CollectionFreeFormLinksView } from "./CollectionFreeFormLinksView"; import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors"; import "./CollectionFreeFormView.scss"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); import { DocumentType, Docs } from "../../../documents/Documents"; +import { RouteStore } from "../../../../server/RouteStore"; +import { string, number, elementType } from "prop-types"; library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard); @@ -48,6 +50,139 @@ export const panZoomSchema = createSchema({ arrangeInit: ScriptField, }); +export interface ViewDefBounds { + x: number; + y: number; + z?: number; + width: number; + height: number; +} + +export interface ViewDefResult { + ele: JSX.Element; + bounds?: ViewDefBounds; +} + +export namespace PivotView { + + export interface PivotData { + type: string; + text: string; + x: number; + y: number; + width: number; + height: number; + fontSize: number; + } + + export const elements = (target: CollectionFreeFormView) => { + let collection = target.Document; + const field = StrCast(collection.pivotField) || "title"; + const width = NumCast(collection.pivotWidth) || 200; + + const groups = new Map<FieldResult<Field>, Doc[]>(); + + for (const doc of target.childDocs) { + const val = doc[field]; + if (val === undefined) continue; + + const l = groups.get(val); + if (l) { + l.push(doc); + } else { + groups.set(val, [doc]); + } + + } + + let minSize = Infinity; + + groups.forEach((val, key) => { + minSize = Math.min(minSize, val.length); + }); + + const numCols = NumCast(collection.pivotNumColumns) || Math.ceil(Math.sqrt(minSize)); + const fontSize = NumCast(collection.pivotFontSize); + + const docMap = new Map<Doc, ViewDefBounds>(); + const groupNames: PivotData[] = []; + + let x = 0; + groups.forEach((val, key) => { + let y = 0; + let xCount = 0; + groupNames.push({ + type: "text", + text: String(key), + x, + y: width + 50, + width: width * 1.25 * numCols, + height: 100, fontSize: fontSize + }); + for (const doc of val) { + docMap.set(doc, { + x: x + xCount * width * 1.25, + y: -y, + width, + height: width + }); + xCount++; + if (xCount >= numCols) { + xCount = 0; + y += width * 1.25; + } + } + x += width * 1.25 * (numCols + 1); + }); + + let elements = target.viewDefsToJSX(groupNames); + let curPage = FieldValue(target.Document.curPage, -1); + + let docViews = target.childDocs.filter(doc => doc instanceof Doc).reduce((prev, doc) => { + var page = NumCast(doc.page, -1); + if ((Math.abs(Math.round(page) - Math.round(curPage)) < 3) || page === -1) { + let minim = BoolCast(doc.isMinimized); + if (minim === undefined || !minim) { + let defaultPosition = (): ViewDefBounds => { + return { + x: NumCast(doc.x), + y: NumCast(doc.y), + z: NumCast(doc.z), + width: NumCast(doc.width), + height: NumCast(doc.height) + }; + }; + const pos = docMap.get(doc) || defaultPosition(); + prev.push({ + ele: ( + <CollectionFreeFormDocumentView + key={doc[Id]} + x={pos.x} + y={pos.y} + width={pos.width} + height={pos.height} + {...target.getChildDocumentViewProps(doc)} + />), + bounds: { + x: pos.x, + y: pos.y, + z: pos.z, + width: NumCast(pos.width), + height: NumCast(pos.height) + } + }); + } + } + return prev; + }, elements); + + target.resetSelectOnLoaded(); + + return docViews; + }; + +} + type PanZoomDocument = makeInterface<[typeof panZoomSchema, typeof positionSchema, typeof pageSchema]>; const PanZoomDocument = makeInterface(panZoomSchema, positionSchema, pageSchema); @@ -488,9 +623,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { getScale = () => this.Document.scale ? this.Document.scale : 1; - getChildDocumentViewProps(childDocLayout: Doc): DocumentViewProps { - let self = this; let pair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, childDocLayout); return { DataDoc: pair.data, @@ -549,7 +682,19 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { return result.result === undefined ? { x: Cast(doc.x, "number"), y: Cast(doc.y, "number"), z: Cast(doc.z, "number"), width: Cast(doc.width, "number"), height: Cast(doc.height, "number") } : result.result; } - private viewDefToJSX(viewDef: any): { ele: JSX.Element, bounds?: { x: number, y: number, z?: number, width: number, height: number } } | undefined { + viewDefsToJSX = (views: any[]) => { + let elements: ViewDefResult[] = []; + if (Array.isArray(views)) { + elements = views.reduce<typeof elements>((prev, ele) => { + const jsx = this.viewDefToJSX(ele); + jsx && prev.push(jsx); + return prev; + }, elements); + } + return elements; + } + + private viewDefToJSX(viewDef: any): Opt<ViewDefResult> { if (viewDef.type === "text") { const text = Cast(viewDef.text, "string"); const x = Cast(viewDef.x, "number"); @@ -578,20 +723,14 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { const script = this.Document.arrangeScript; let state: any = undefined; const docs = this.childDocs; - let elements: { ele: JSX.Element, bounds?: { x: number, y: number, z?: number, width: number, height: number } }[] = []; + let elements: ViewDefResult[] = []; if (initScript) { const initResult = initScript.script.run({ docs, collection: this.Document }); if (initResult.success) { const result = initResult.result; const { state: scriptState, views } = result; state = scriptState; - if (Array.isArray(views)) { - elements = views.reduce<typeof elements>((prev, ele) => { - const jsx = this.viewDefToJSX(ele); - jsx && prev.push(jsx); - return prev; - }, elements); - } + elements = this.viewDefsToJSX(views); } } let docviews = docs.filter(doc => doc instanceof Doc).reduce((prev, doc) => { @@ -613,14 +752,17 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { return prev; }, elements); - setTimeout(() => this._selectOnLoaded = "", 600);// bcz: surely there must be a better way .... + this.resetSelectOnLoaded(); return docviews; } + resetSelectOnLoaded = () => setTimeout(() => this._selectOnLoaded = "", 600);// bcz: surely there must be a better way .... + @computed.struct get views() { - return this.elements.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele); + let source = this.Document.usePivotLayout === true ? PivotView.elements(this) : this.elements; + return source.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele); } @computed.struct get overlayViews() { @@ -633,11 +775,46 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY)); } + fitToContainer = async () => this.props.Document.fitToBox = !this.fitToBox; + + arrangeContents = async () => { + const docs = await DocListCastAsync(this.Document[this.props.fieldKey]); + UndoManager.RunInBatch(() => { + if (docs) { + let startX = this.Document.panX || 0; + let x = startX; + let y = this.Document.panY || 0; + let i = 0; + const width = Math.max(...docs.map(doc => NumCast(doc.width))); + const height = Math.max(...docs.map(doc => NumCast(doc.height))); + for (const doc of docs) { + doc.x = x; + doc.y = y; + x += width + 20; + if (++i === 6) { + i = 0; + x = startX; + y += height + 20; + } + } + } + }, "arrange contents"); + } + + analyzeStrokes = async () => { + let data = Cast(this.fieldExtensionDoc[this.inkKey], InkField); + if (!data) { + return; + } + let relevantKeys = ["inkAnalysis", "handwriting"]; + CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.fieldExtensionDoc, relevantKeys, data.inkData); + } + onContextMenu = (e: React.MouseEvent) => { let layoutItems: ContextMenuProps[] = []; layoutItems.push({ description: `${this.fitToBox ? "Unset" : "Set"} Fit To Container`, - event: async () => this.props.Document.fitToBox = !this.fitToBox, + event: this.fitToContainer, icon: !this.fitToBox ? "expand-arrows-alt" : "compress-arrows-alt" }); layoutItems.push({ @@ -662,41 +839,18 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { }); layoutItems.push({ description: "Arrange contents in grid", - icon: "table", - event: async () => { - const docs = await DocListCastAsync(this.Document[this.props.fieldKey]); - UndoManager.RunInBatch(() => { - if (docs) { - let startX = this.Document.panX || 0; - let x = startX; - let y = this.Document.panY || 0; - let i = 0; - const width = Math.max(...docs.map(doc => NumCast(doc.width))); - const height = Math.max(...docs.map(doc => NumCast(doc.height))); - for (const doc of docs) { - doc.x = x; - doc.y = y; - x += width + 20; - if (++i === 6) { - i = 0; - x = startX; - y += height + 20; - } - } - } - }, "arrange contents"); - } + event: this.arrangeContents, + icon: "table" }); - ContextMenu.Instance.addItem({ description: "Layout...", subitems: layoutItems, icon: "compass" }); ContextMenu.Instance.addItem({ - description: "Analyze Strokes", event: async () => { - let data = Cast(this.fieldExtensionDoc[this.inkKey], InkField); - if (!data) { - return; - } - let relevantKeys = ["inkAnalysis", "handwriting"]; - CognitiveServices.Inking.Manager.analyzer(this.fieldExtensionDoc, relevantKeys, data.inkData); - }, icon: "paint-brush" + description: "Layout...", + subitems: layoutItems, + icon: "compass" + }); + ContextMenu.Instance.addItem({ + description: "Analyze Strokes", + event: this.analyzeStrokes, + icon: "paint-brush" }); ContextMenu.Instance.addItem({ description: "Import document", icon: "upload", event: () => { diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index a7b4f33db..c8eab85c2 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -8,14 +8,13 @@ import { Copy, 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 { BoolCast, Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types"; +import { BoolCast, Cast, FieldValue, NumCast, StrCast, PromiseValue } from "../../../new_fields/Types"; import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; import { RouteStore } from '../../../server/RouteStore'; import { emptyFunction, returnTrue, Utils } from "../../../Utils"; import { DocServer } from "../../DocServer"; import { Docs, DocUtils } from "../../documents/Documents"; import { ClientUtils } from '../../util/ClientUtils'; -import DictationManager from '../../util/DictationManager'; import { DocumentManager } from "../../util/DocumentManager"; import { DragManager, dropActionType } from "../../util/DragManager"; import { LinkManager } from '../../util/LinkManager'; @@ -38,6 +37,9 @@ import { DocumentContentsView } from "./DocumentContentsView"; import "./DocumentView.scss"; import { FormattedTextBox } from './FormattedTextBox'; import React = require("react"); +import { DictationManager } from '../../util/DictationManager'; +import { MainView } from '../MainView'; +import requestPromise = require('request-promise'); const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? library.add(fa.faTrash); @@ -149,10 +151,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu set templates(templates: List<string>) { this.props.Document.templates = templates; } screenRect = (): ClientRect | DOMRect => this._mainCont.current ? this._mainCont.current.getBoundingClientRect() : new DOMRect(); - constructor(props: DocumentViewProps) { - super(props); - } - _animateToIconDisposer?: IReactionDisposer; _reactionDisposer?: IReactionDisposer; @action @@ -303,7 +301,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu fullScreenAlias.showCaption = true; this.props.addDocTab(fullScreenAlias, this.dataDoc, "inTab"); SelectionManager.DeselectAll(); - this.props.Document.libraryBrush = false; + Doc.UnBrushDoc(this.props.Document); } else if (CurrentUserUtils.MainDocId !== this.props.Document[Id] && (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && @@ -361,12 +359,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu if (!linkedFwdDocs.some(l => l instanceof Promise)) { let maxLocation = StrCast(linkedFwdDocs[0].maximizeLocation, "inTab"); let targetContext = !Doc.AreProtosEqual(linkedFwdContextDocs[altKey ? 1 : 0], this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document) ? linkedFwdContextDocs[altKey ? 1 : 0] : undefined; - DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], ctrlKey, false, document => { - this.props.focus(this.props.Document, true, 1); - setTimeout(() => - this.props.addDocTab(document, undefined, maxLocation), 1000); - } - , linkedFwdPage[altKey ? 1 : 0], targetContext); + DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], ctrlKey, false, + document => { // open up target if it's not already in view ... + this.props.focus(this.props.Document, true, 1); // by zooming into the button document first + setTimeout(() => this.props.addDocTab(document, undefined, maxLocation), 1000); // then after the 1sec animation, open up the target in a new tab + }, + linkedFwdPage[altKey ? 1 : 0], targetContext); } } } @@ -411,7 +409,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu deleteClicked = (): void => { SelectionManager.DeselectAll(); this.props.removeDocument && this.props.removeDocument(this.props.Document); } @undoBatch - fieldsClicked = (): void => { let kvp = Docs.Create.KVPDocument(this.props.Document, { width: 300, height: 300 }); this.props.addDocTab(kvp, this.dataDoc, "onRight"); } + fieldsClicked = (): void => { + let kvp = Docs.Create.KVPDocument(this.props.Document, { width: 300, height: 300 }); + this.props.addDocTab(kvp, this.dataDoc, "onRight"); + } @undoBatch makeBtnClicked = (): void => { @@ -444,15 +445,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu let targetDoc = this.props.Document; targetDoc.targetContext = de.data.targetContext; let annotations = await DocListCastAsync(annotationDoc.annotations); - if (annotations) { - annotations.forEach(anno => { - anno.target = targetDoc; - }); - } - let pdfDoc = await Cast(annotationDoc.pdfDoc, Doc); - if (pdfDoc) { - DocUtils.MakeLink(annotationDoc, targetDoc, this.props.ContainingCollectionView!.props.Document, `Annotation from ${StrCast(pdfDoc.title)}`, "", StrCast(pdfDoc.title)); - } + annotations && annotations.forEach(anno => anno.target = targetDoc); + + DocUtils.MakeLink(annotationDoc, targetDoc, this.props.ContainingCollectionView!.props.Document, `Link from ${StrCast(annotationDoc.title)}`); } if (de.data instanceof DragManager.LinkDragData) { let sourceDoc = de.data.linkSourceDocument; @@ -537,8 +532,15 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } listen = async () => { - let transcript = await DictationManager.Instance.listen(); - transcript && (Doc.GetProto(this.props.Document).transcript = transcript); + Doc.GetProto(this.props.Document).transcript = await DictationManager.Controls.listen({ + continuous: { indefinite: true }, + interimHandler: (results: string) => { + let main = MainView.Instance; + main.dictationSuccess = true; + main.dictatedPhrase = results; + main.isListening = { interim: true }; + } + }); } @action @@ -647,8 +649,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu }); } - onPointerEnter = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = true; }; - onPointerLeave = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = false; }; + onPointerEnter = (e: React.PointerEvent): void => { Doc.BrushDoc(this.props.Document); }; + onPointerLeave = (e: React.PointerEvent): void => { Doc.UnBrushDoc(this.props.Document); }; isSelected = () => SelectionManager.IsSelected(this); @action select = (ctrlPressed: boolean) => { SelectionManager.SelectDoc(this, ctrlPressed); }; @@ -695,22 +697,23 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu }); } let showTextTitle = showTitle && StrCast(this.layoutDoc.layout).startsWith("<FormattedTextBox") ? showTitle : undefined; + let brushDegree = Doc.IsBrushedDegree(this.layoutDoc); return ( <div className={`documentView-node${this.topMost ? "-topmost" : ""}`} ref={this._mainCont} style={{ pointerEvents: this.layoutDoc.isBackground && !this.isSelected() ? "none" : "all", color: foregroundColor, - outlineColor: "maroon", - outlineStyle: "dashed", - outlineWidth: BoolCast(this.layoutDoc.libraryBrush) && !StrCast(Doc.GetProto(this.props.Document).borderRounding) ? - `${this.props.ScreenToLocalTransform().Scale}px` : "0px", - marginLeft: BoolCast(this.layoutDoc.libraryBrush) && StrCast(Doc.GetProto(this.props.Document).borderRounding) ? - `${-1 * this.props.ScreenToLocalTransform().Scale}px` : undefined, - marginTop: BoolCast(this.layoutDoc.libraryBrush) && StrCast(Doc.GetProto(this.props.Document).borderRounding) ? - `${-1 * this.props.ScreenToLocalTransform().Scale}px` : undefined, - border: BoolCast(this.layoutDoc.libraryBrush) && StrCast(Doc.GetProto(this.props.Document).borderRounding) ? - `dashed maroon ${this.props.ScreenToLocalTransform().Scale}px` : undefined, + outlineColor: ["transparent", "maroon", "maroon"][brushDegree], + outlineStyle: ["none", "dashed", "solid"][brushDegree], + outlineWidth: brushDegree && !StrCast(Doc.GetProto(this.props.Document).borderRounding) ? + `${brushDegree * this.props.ScreenToLocalTransform().Scale}px` : "0px", + marginLeft: brushDegree && StrCast(Doc.GetProto(this.props.Document).borderRounding) ? + `${-brushDegree * this.props.ScreenToLocalTransform().Scale}px` : undefined, + marginTop: brushDegree && StrCast(Doc.GetProto(this.props.Document).borderRounding) ? + `${-brushDegree * this.props.ScreenToLocalTransform().Scale}px` : undefined, + border: brushDegree && StrCast(Doc.GetProto(this.props.Document).borderRounding) ? + `${["none", "dashed", "solid"][brushDegree]} ${["transparent", "maroon", "maroon"][brushDegree]} ${this.props.ScreenToLocalTransform().Scale}px` : undefined, borderRadius: "inherit", background: backgroundColor, width: nativeWidth, diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 9dd0643df..44b5d2c21 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -674,7 +674,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe style={{ height: this.props.height ? this.props.height : undefined, background: this.props.hideOnLeave ? "rgba(0,0,0,0.4)" : undefined, - opacity: this.props.hideOnLeave ? (this._entered || this.props.isSelected() || this.props.Document.libraryBrush ? 1 : 0.1) : 1, + opacity: this.props.hideOnLeave ? (this._entered || this.props.isSelected() || Doc.IsBrushed(this.props.Document) ? 1 : 0.1) : 1, color: this.props.color ? this.props.color : this.props.hideOnLeave ? "white" : "inherit", pointerEvents: interactive, fontSize: "13px" diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 0d9c2bb8a..ca0f637eb 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -234,7 +234,9 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD results.map((face: CognitiveServices.Image.Face) => faceDocs.push(Docs.Get.DocumentHierarchyFromJson(face, `Face: ${face.faceId}`)!)); return faceDocs; }; - CognitiveServices.Image.Manager.analyzer(this.extensionDoc, ["faces"], this.url, Service.Face, converter); + if (this.url) { + CognitiveServices.Image.Appliers.ProcessImage(this.extensionDoc, ["faces"], this.url, Service.Face, converter); + } } generateMetadata = (threshold: Confidence = Confidence.Excellent) => { @@ -253,7 +255,9 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD tagDoc.confidence = threshold; return tagDoc; }; - CognitiveServices.Image.Manager.analyzer(this.extensionDoc, ["generatedTagsDoc"], this.url, Service.ComputerVision, converter); + if (this.url) { + CognitiveServices.Image.Appliers.ProcessImage(this.extensionDoc, ["generatedTagsDoc"], this.url, Service.ComputerVision, converter); + } } @action diff --git a/src/client/views/nodes/LinkMenuItem.tsx b/src/client/views/nodes/LinkMenuItem.tsx index 1d4fcad69..a119eb39b 100644 --- a/src/client/views/nodes/LinkMenuItem.tsx +++ b/src/client/views/nodes/LinkMenuItem.tsx @@ -6,7 +6,7 @@ import { DocumentManager } from "../../util/DocumentManager"; import { undoBatch } from "../../util/UndoManager"; import './LinkMenu.scss'; import React = require("react"); -import { Doc } from '../../../new_fields/Doc'; +import { Doc, DocListCastAsync } from '../../../new_fields/Doc'; import { StrCast, Cast, FieldValue, NumCast } from '../../../new_fields/Types'; import { observable, action } from 'mobx'; import { LinkManager } from '../../util/LinkManager'; @@ -52,7 +52,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { } if (this.props.destinationDoc === self.props.linkDoc.anchor2 && targetContext) { - DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, false, document => dockingFunc(targetContext!)); + DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, false, async document => dockingFunc(document), undefined, targetContext!); } else if (this.props.destinationDoc === self.props.linkDoc.anchor1 && sourceContext) { DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, false, document => dockingFunc(sourceContext!)); diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index e9207404e..6450cb826 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -194,10 +194,11 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen onScroll={this.onScroll} style={{ marginTop: `${this.containingCollectionDocument ? NumCast(this.containingCollectionDocument.panY) : 0}px` }} ref={this._mainCont}> - <div className="pdfBox-scrollHack" style={{ height: NumCast(this.props.Document.scrollHeight) + (NumCast(this.props.Document.nativeHeight) - NumCast(this.props.Document.nativeHeight) / NumCast(this.props.Document.scale)), width: "100%" }} /> + <div className="pdfBox-scrollHack" style={{ height: NumCast(this.props.Document.scrollHeight) + (NumCast(this.props.Document.nativeHeight) - NumCast(this.props.Document.nativeHeight) / NumCast(this.props.Document.scale, 1)), width: "100%" }} /> <PDFViewer pdf={this._pdf} url={pdfUrl.url.pathname} active={this.props.active} scrollTo={this.scrollTo} loaded={this.loaded} panY={NumCast(this.props.Document.panY)} Document={this.props.Document} DataDoc={this.props.DataDoc} addDocTab={this.props.addDocTab} setPanY={this.setPanY} + addDocument={this.props.addDocument} fieldKey={this.props.fieldKey} fieldExtensionDoc={this.fieldExtensionDoc} /> {this.settingsPanel()} </div>); diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 162ac1d98..c8749b7cd 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -8,6 +8,7 @@ import "./WebBox.scss"; import React = require("react"); import { InkTool } from "../../../new_fields/InkField"; import { Cast, FieldValue, NumCast } from "../../../new_fields/Types"; +import { Utils } from "../../../Utils"; @observer export class WebBox extends React.Component<FieldViewProps> { @@ -52,7 +53,7 @@ export class WebBox extends React.Component<FieldViewProps> { if (field instanceof HtmlField) { view = <span id="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: field.html }} />; } else if (field instanceof WebField) { - view = <iframe src={field.url.href} style={{ position: "absolute", width: "100%", height: "100%" }} />; + view = <iframe src={Utils.CorsProxy(field.url.href)} style={{ position: "absolute", width: "100%", height: "100%" }} />; } else { view = <iframe src={"https://crossorigin.me/https://cs.brown.edu"} style={{ position: "absolute", width: "100%", height: "100%" }} />; } diff --git a/src/client/views/pdf/Annotation.scss b/src/client/views/pdf/Annotation.scss index 817115690..0c6df74f0 100644 --- a/src/client/views/pdf/Annotation.scss +++ b/src/client/views/pdf/Annotation.scss @@ -2,6 +2,6 @@ pointer-events: all; user-select: none; position: absolute; - background-color: pink; + background-color: red; opacity: 0.1; }
\ No newline at end of file diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx index 2610f6c6e..7ba7b6d14 100644 --- a/src/client/views/pdf/Annotation.tsx +++ b/src/client/views/pdf/Annotation.tsx @@ -120,10 +120,10 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> { render() { return (<div className="pdfAnnotation" onPointerDown={this.onPointerDown} ref={this._mainCont} style={{ - top: this.props.y * scale, - left: this.props.x * scale, - width: this.props.width * scale, - height: this.props.height * scale, + top: this.props.y, + left: this.props.x, + width: this.props.width, + height: this.props.height, backgroundColor: this.props.ParentIndex() === this.props.index ? "green" : StrCast(this.props.document.color) }} />); } diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index f3281047a..08674720d 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -36,6 +36,7 @@ interface IViewerProps { active: () => boolean; setPanY?: (n: number) => void; addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void; + addDocument?: (doc: Doc, allowDuplicates?: boolean) => boolean; } /** @@ -124,7 +125,7 @@ export class PDFViewer extends React.Component<IViewerProps> { if (this.props.active() && e.clipboardData) { e.clipboardData.setData("text/plain", this._selectionText); e.clipboardData.setData("dash/pdfOrigin", this.props.Document[Id]); - e.clipboardData.setData("dash/pdfRegion", this.makeAnnotationDocument(undefined, 0, "#0390fc")[Id]); + e.clipboardData.setData("dash/pdfRegion", this.makeAnnotationDocument(undefined, "#0390fc")[Id]); e.preventDefault(); } } @@ -133,7 +134,7 @@ export class PDFViewer extends React.Component<IViewerProps> { if (e.clipboardData && e.clipboardData.getData("dash/pdfOrigin") === this.props.Document[Id]) { let linkDocId = e.clipboardData.getData("dash/linkDoc"); linkDocId && DocServer.GetRefField(linkDocId).then(async (link) => - (link instanceof Doc) && (Doc.GetProto(link).anchor2 = this.makeAnnotationDocument(await Cast(Doc.GetProto(link), Doc), 0, "#0390fc", false))); + (link instanceof Doc) && (Doc.GetProto(link).anchor2 = this.makeAnnotationDocument(await Cast(Doc.GetProto(link), Doc), "#0390fc", false))); } } @@ -166,34 +167,51 @@ export class PDFViewer extends React.Component<IViewerProps> { } @action - makeAnnotationDocument = (sourceDoc: Doc | undefined, s: number, color: string, createLink: boolean = true): Doc => { + makeAnnotationDocument = (sourceDoc: Doc | undefined, color: string, createLink: boolean = true): Doc => { let mainAnnoDoc = Docs.Create.InstanceFromProto(new Doc(), "", {}); + let mainAnnoDocProto = Doc.GetProto(mainAnnoDoc); let annoDocs: Doc[] = []; let minY = Number.MAX_VALUE; - this._savedAnnotations.forEach((key: number, value: HTMLDivElement[]) => value.map(anno => { - let annoDoc = new Doc(); - if (anno.style.left) annoDoc.x = parseInt(anno.style.left) / scale; - if (anno.style.top) annoDoc.y = parseInt(anno.style.top) / scale; - if (anno.style.height) annoDoc.height = parseInt(anno.style.height) / scale; - if (anno.style.width) annoDoc.width = parseInt(anno.style.width) / scale; - annoDoc.page = key; + if (this._savedAnnotations.size() === 1 && this._savedAnnotations.values()[0].length === 1 && !createLink) { + let anno = this._savedAnnotations.values()[0][0]; + let annoDoc = Docs.Create.FreeformDocument([], { backgroundColor: "rgba(255, 0, 0, 0.1)", title: "Annotation on " + StrCast(this.props.Document.title) }); + if (anno.style.left) annoDoc.x = parseInt(anno.style.left); + if (anno.style.top) annoDoc.y = parseInt(anno.style.top); + if (anno.style.height) annoDoc.height = parseInt(anno.style.height); + if (anno.style.width) annoDoc.width = parseInt(anno.style.width); annoDoc.target = sourceDoc; annoDoc.group = mainAnnoDoc; annoDoc.color = color; annoDoc.type = AnnotationTypes.Region; annoDocs.push(annoDoc); + annoDoc.isButton = true; anno.remove(); - (annoDoc.y !== undefined) && (minY = Math.min(NumCast(annoDoc.y), minY)); - })); - - let mainAnnoDocProto = Doc.GetProto(mainAnnoDoc); + this.props.addDocument && this.props.addDocument(annoDoc, false); + mainAnnoDoc = annoDoc; + mainAnnoDocProto = Doc.GetProto(annoDoc); + } else { + this._savedAnnotations.forEach((key: number, value: HTMLDivElement[]) => value.map(anno => { + let annoDoc = new Doc(); + if (anno.style.left) annoDoc.x = parseInt(anno.style.left); + if (anno.style.top) annoDoc.y = parseInt(anno.style.top); + if (anno.style.height) annoDoc.height = parseInt(anno.style.height); + if (anno.style.width) annoDoc.width = parseInt(anno.style.width); + annoDoc.target = sourceDoc; + annoDoc.group = mainAnnoDoc; + annoDoc.color = color; + annoDoc.type = AnnotationTypes.Region; + annoDocs.push(annoDoc); + anno.remove(); + (annoDoc.y !== undefined) && (minY = Math.min(NumCast(annoDoc.y), minY)); + })); + + mainAnnoDocProto.y = Math.max(minY, 0); + mainAnnoDocProto.annotations = new List<Doc>(annoDocs); + } mainAnnoDocProto.title = "Annotation on " + StrCast(this.props.Document.title); - mainAnnoDocProto.pdfDoc = this.props.Document; mainAnnoDocProto.annotationOn = this.props.Document; - mainAnnoDocProto.y = Math.max(minY, 0); - mainAnnoDocProto.annotations = new List<Doc>(annoDocs); if (sourceDoc && createLink) { - DocUtils.MakeLink(sourceDoc, mainAnnoDocProto, undefined, `Annotation from ${StrCast(this.props.Document.title)}`, "", StrCast(this.props.Document.title)); + DocUtils.MakeLink(sourceDoc, mainAnnoDocProto, undefined, `Annotation from ${StrCast(this.props.Document.title)}`); } this._savedAnnotations.clear(); this.Index = -1; @@ -346,48 +364,6 @@ export class PDFViewer extends React.Component<IViewerProps> { this._mainCont.current.addEventListener("pagesloaded", executeFind); this._mainCont.current.addEventListener("pagerendered", executeFind); } - - // let viewer = this._viewer.current; - - // if (!this._pdfFindController) { - // if (container && viewer) { - // let simpleLinkService = new SimpleLinkService(); - // let pdfViewer = new PDFJSViewer.PDFViewer({ - // container: container, - // viewer: viewer, - // linkService: simpleLinkService - // }); - // simpleLinkService.setPdf(this.props.pdf); - // container.addEventListener("pagesinit", () => { - // pdfViewer.currentScaleValue = 1; - // }); - // container.addEventListener("pagerendered", () => { - // console.log("rendered"); - // this._pdfFindController.executeCommand('find', - // { - // caseSensitive: false, - // findPrevious: undefined, - // highlightAll: true, - // phraseSearch: true, - // query: searchString - // }); - // }); - // pdfViewer.setDocument(this.props.pdf); - // this._pdfFindController = new PDFJSViewer.PDFFindController(pdfViewer); - // // this._pdfFindController._linkService = pdfLinkService; - // pdfViewer.findController = this._pdfFindController; - // } - // } - // else { - // this._pdfFindController.executeCommand('find', - // { - // caseSensitive: false, - // findPrevious: undefined, - // highlightAll: true, - // phraseSearch: true, - // query: searchString - // }); - // } } diff --git a/src/client/views/pdf/Page.tsx b/src/client/views/pdf/Page.tsx index 6de2db427..7ca9d2d7d 100644 --- a/src/client/views/pdf/Page.tsx +++ b/src/client/views/pdf/Page.tsx @@ -24,7 +24,7 @@ interface IPageProps { renderAnnotations: (annotations: Doc[], removeOld: boolean) => void; sendAnnotations: (annotations: HTMLDivElement[], page: number) => void; createAnnotation: (div: HTMLDivElement, page: number) => void; - makeAnnotationDocuments: (doc: Doc | undefined, scale: number, color: string, linkTo: boolean) => Doc; + makeAnnotationDocuments: (doc: Doc | undefined, color: string, linkTo: boolean) => Doc; getScrollFromPage: (page: number) => number; setSelectionText: (text: string) => void; } @@ -87,7 +87,7 @@ export default class Page extends React.Component<IPageProps> { @action highlight = (targetDoc: Doc | undefined, color: string) => { // creates annotation documents for current highlights - let annotationDoc = this.props.makeAnnotationDocuments(targetDoc, scale, color, false); + let annotationDoc = this.props.makeAnnotationDocuments(targetDoc, color, false); Doc.AddDocToList(this.props.fieldExtensionDoc, "annotations", annotationDoc); return annotationDoc; } @@ -112,8 +112,7 @@ export default class Page extends React.Component<IPageProps> { if (!BoolCast(annotationDoc.linkedToDoc)) { let annotations = await DocListCastAsync(annotationDoc.annotations); annotations && annotations.forEach(anno => anno.target = targetDoc); - let pdfDoc = await Cast(annotationDoc.pdfDoc, Doc); - pdfDoc && DocUtils.MakeLink(annotationDoc, targetDoc, dragData.targetContext, `Annotation from ${StrCast(pdfDoc.title)}`, "", StrCast(pdfDoc.title)) + DocUtils.MakeLink(annotationDoc, targetDoc, dragData.targetContext, `Annotation from ${StrCast(this.props.Document.title)}`) } } }, @@ -143,11 +142,8 @@ export default class Page extends React.Component<IPageProps> { @action onPointerDown = (e: React.PointerEvent): void => { // if alt+left click, drag and annotate - if (this.props.Document.scale !== 1) return; - if (e.altKey && e.button === 0) { - e.stopPropagation(); - } - else if (e.button === 0) { + if (NumCast(this.props.Document.scale, 1) !== 1) return; + if (!e.altKey && e.button === 0) { PDFMenu.Instance.StartDrag = this.startDrag; PDFMenu.Instance.Highlight = this.highlight; PDFMenu.Instance.Snippet = this.createSnippet; diff --git a/src/client/views/presentationview/PresentationElement.tsx b/src/client/views/presentationview/PresentationElement.tsx index e2d8daea9..d98b66324 100644 --- a/src/client/views/presentationview/PresentationElement.tsx +++ b/src/client/views/presentationview/PresentationElement.tsx @@ -706,7 +706,7 @@ export default class PresentationElement extends React.Component<PresentationEle * It makes it possible to show dropping lines on drop targets. */ onDragMove = (e: PointerEvent): void => { - this.props.document.libraryBrush = false; + Doc.UnBrushDoc(this.props.document); let x = this.ScreenToLocalListTransform(e.clientX, e.clientY); let rect = this.header!.getBoundingClientRect(); let bounds = this.ScreenToLocalListTransform(rect.left, rect.top + rect.height / 2); @@ -889,7 +889,7 @@ export default class PresentationElement extends React.Component<PresentationEle style={{ outlineColor: "maroon", outlineStyle: "dashed", - outlineWidth: BoolCast(p.document.libraryBrush) ? `1px` : "0px", + outlineWidth: Doc.IsBrushed(p.document) ? `1px` : "0px", }} onClick={e => { p.gotoDocument(p.index, NumCast(this.props.mainDocument.selectedDoc)); e.stopPropagation(); }}> <strong className="presentationView-name"> diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx index c4af30f5c..8201aa374 100644 --- a/src/client/views/search/SearchItem.tsx +++ b/src/client/views/search/SearchItem.tsx @@ -105,23 +105,11 @@ export interface LinkMenuProps { @observer export class LinkContextMenu extends React.Component<LinkMenuProps> { - highlightDoc = (doc: Doc) => { - return () => { - doc.libraryBrush = true; - }; - } + highlightDoc = (doc: Doc) => () => Doc.BrushDoc(doc); - unHighlightDoc = (doc: Doc) => { - return () => { - doc.libraryBrush = false; - }; - } + unHighlightDoc = (doc: Doc) => () => Doc.UnBrushDoc(doc); - getOnClick(col: Doc) { - return () => { - CollectionDockingView.Instance.AddRightSplit(col, undefined); - }; - } + getOnClick = (col: Doc) => () => CollectionDockingView.Instance.AddRightSplit(col, undefined); render() { return ( @@ -286,14 +274,12 @@ export class SearchItem extends React.Component<SearchItemProps> { let doc1 = Cast(this.props.doc.anchor1, Doc, null); let doc2 = Cast(this.props.doc.anchor2, Doc, null); - doc1 && (doc1.libraryBrush = true); - doc2 && (doc2.libraryBrush = true); + Doc.BrushDoc(doc1); + Doc.BrushDoc(doc2); } } else { - let docViews: DocumentView[] = DocumentManager.Instance.getAllDocumentViews(this.props.doc); - docViews.forEach(element => { - element.props.Document.libraryBrush = true; - }); + DocumentManager.Instance.getAllDocumentViews(this.props.doc).forEach(element => + Doc.BrushDoc(element.props.Document)); } } @@ -303,14 +289,12 @@ export class SearchItem extends React.Component<SearchItemProps> { let doc1 = Cast(this.props.doc.anchor1, Doc, null); let doc2 = Cast(this.props.doc.anchor2, Doc, null); - doc1 && (doc1.libraryBrush = false); - doc2 && (doc2.libraryBrush = false); + Doc.UnBrushDoc(doc1); + Doc.UnBrushDoc(doc2); } } else { - let docViews: DocumentView[] = DocumentManager.Instance.getAllDocumentViews(this.props.doc); - docViews.forEach(element => { - element.props.Document.libraryBrush = false; - }); + DocumentManager.Instance.getAllDocumentViews(this.props.doc). + forEach(element => Doc.UnBrushDoc(element.props.Document)); } } diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index c01f4e8cf..87e048140 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -8,10 +8,10 @@ import { listSpec } from "./Schema"; import { ObjectField } from "./ObjectField"; import { RefField, FieldId } from "./RefField"; import { ToScriptString, SelfProxy, Parent, OnUpdate, Self, HandleUpdate, Update, Id } from "./FieldSymbols"; -import { scriptingGlobal } from "../client/util/Scripting"; +import { scriptingGlobal, CompileScript, Scripting } from "../client/util/Scripting"; import { List } from "./List"; import { DocumentType } from "../client/documents/Documents"; -import { ComputedField } from "./ScriptField"; +import { ComputedField, ScriptField } from "./ScriptField"; import { PrefetchProxy, ProxyField } from "./Proxy"; export namespace Field { @@ -68,6 +68,7 @@ export function DocListCast(field: FieldResult): Doc[] { export const WidthSym = Symbol("Width"); export const HeightSym = Symbol("Height"); +const CachedUpdates = Symbol("Cached updates"); function fetchProto(doc: Doc) { const proto = doc.proto; @@ -147,6 +148,8 @@ export class Doc extends RefField { return "invalid"; } + private [CachedUpdates]: { [key: string]: () => void | Promise<any> } = {}; + public async [HandleUpdate](diff: any) { const set = diff.$set; if (set) { @@ -154,11 +157,18 @@ export class Doc extends RefField { if (!key.startsWith("fields.")) { continue; } - const value = await SerializationHelper.Deserialize(set[key]); const fKey = key.substring(7); - updatingFromServer = true; - this[fKey] = value; - updatingFromServer = false; + const fn = async () => { + const value = await SerializationHelper.Deserialize(set[key]); + updatingFromServer = true; + this[fKey] = value; + updatingFromServer = false; + }; + if (DocServer.getFieldWriteMode(fKey)) { + this[CachedUpdates][fKey] = fn; + } else { + await fn(); + } } } const unset = diff.$unset; @@ -168,9 +178,16 @@ export class Doc extends RefField { continue; } const fKey = key.substring(7); - updatingFromServer = true; - delete this[fKey]; - updatingFromServer = false; + const fn = () => { + updatingFromServer = true; + delete this[fKey]; + updatingFromServer = false; + }; + if (DocServer.getFieldWriteMode(fKey)) { + this[CachedUpdates][fKey] = fn; + } else { + await fn(); + } } } } @@ -187,6 +204,21 @@ export namespace Doc { // return Cast(field, ctor); // }); // } + export function RunCachedUpdate(doc: Doc, field: string) { + const update = doc[CachedUpdates][field]; + if (update) { + update(); + delete doc[CachedUpdates][field]; + } + } + export function AddCachedUpdate(doc: Doc, field: string, oldValue: any) { + const val = oldValue; + doc[CachedUpdates][field] = () => { + updatingFromServer = true; + doc[field] = val; + updatingFromServer = false; + }; + } export function MakeReadOnly(): { end(): void } { makeReadOnly(); return { @@ -366,10 +398,15 @@ export namespace Doc { } } export function MakeAlias(doc: Doc) { - if (!GetT(doc, "isPrototype", "boolean", true)) { - return Doc.MakeCopy(doc); + let alias = !GetT(doc, "isPrototype", "boolean", true) ? Doc.MakeCopy(doc) : Doc.MakeDelegate(doc); + let aliasNumber = Doc.GetProto(doc).aliasNumber = NumCast(Doc.GetProto(doc).aliasNumber) + 1; + let script = `return renameAlias(self, ${aliasNumber})`; + //let script = "StrCast(self.title).replace(/\\([0-9]*\\)/, \"\") + `(${n})`"; + let compiled = CompileScript(script, { params: { this: "Doc" }, capturedVariables: { self: doc }, typecheck: false }); + if (compiled.compiled) { + alias.title = new ComputedField(compiled); } - return Doc.MakeDelegate(doc); // bcz? + return alias; } // @@ -542,4 +579,25 @@ export namespace Doc { } }); } -}
\ No newline at end of file + + export class DocBrush { + @observable BrushedDoc: Doc[] = []; + } + const manager = new DocBrush(); + export function IsBrushed(doc: Doc) { + return manager.BrushedDoc.some(d => Doc.AreProtosEqual(d, doc)); + } + export function IsBrushedDegree(doc: Doc) { + return manager.BrushedDoc.some(d => d === doc) ? 2 : Doc.IsBrushed(doc) ? 1 : 0; + } + export function BrushDoc(doc: Doc) { + if (manager.BrushedDoc.indexOf(doc) === -1) runInAction(() => manager.BrushedDoc.push(doc)); + } + export function UnBrushDoc(doc: Doc) { + let index = manager.BrushedDoc.indexOf(doc); + if (index !== -1) runInAction(() => manager.BrushedDoc.splice(index, 1)); + } +} +Scripting.addGlobal(function renameAlias(doc: any, n: any) { + return StrCast(doc.title).replace(/\([0-9]*\)/, "") + `(${n})`; +});
\ No newline at end of file diff --git a/src/new_fields/Types.ts b/src/new_fields/Types.ts index 565ae2ee3..09cbff25e 100644 --- a/src/new_fields/Types.ts +++ b/src/new_fields/Types.ts @@ -48,9 +48,11 @@ export interface Interface { } export type WithoutRefField<T extends Field> = T extends RefField ? never : T; -export function Cast<T extends ToConstructor<Field> | ListSpec<Field>>(field: FieldResult, ctor: T): FieldResult<ToType<T>>; -export function Cast<T extends ToConstructor<Field> | ListSpec<Field>>(field: FieldResult, ctor: T, defaultVal: WithoutList<WithoutRefField<ToType<T>>> | null): WithoutList<ToType<T>>; -export function Cast<T extends ToConstructor<Field> | ListSpec<Field>>(field: FieldResult, ctor: T, defaultVal?: ToType<T> | null): FieldResult<ToType<T>> | undefined { +export type CastCtor = ToConstructor<Field> | ListSpec<Field>; + +export function Cast<T extends CastCtor>(field: FieldResult, ctor: T): FieldResult<ToType<T>>; +export function Cast<T extends CastCtor>(field: FieldResult, ctor: T, defaultVal: WithoutList<WithoutRefField<ToType<T>>> | null): WithoutList<ToType<T>>; +export function Cast<T extends CastCtor>(field: FieldResult, ctor: T, defaultVal?: ToType<T> | null): FieldResult<ToType<T>> | undefined { if (field instanceof Promise) { return defaultVal === undefined ? field.then(f => Cast(f, ctor) as any) as any : defaultVal === null ? undefined : defaultVal; } diff --git a/src/new_fields/util.ts b/src/new_fields/util.ts index c6f693f7f..6c05da507 100644 --- a/src/new_fields/util.ts +++ b/src/new_fields/util.ts @@ -6,7 +6,8 @@ import { RefField } from "./RefField"; import { ObjectField } from "./ObjectField"; import { action } from "mobx"; import { Parent, OnUpdate, Update, Id, SelfProxy, Self } from "./FieldSymbols"; -import { ComputedField } from "./ScriptField"; +import { DocServer } from "../client/DocServer"; +import { CurrentUserUtils } from "../server/authentication/models/current_user_utils"; function _readOnlySetter(): never { throw new Error("Documents can't be modified in read-only mode"); @@ -63,9 +64,14 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number } else { target.__fields[prop] = value; } - if (value === undefined) target[Update]({ '$unset': { ["fields." + prop]: "" } }); + const writeMode = DocServer.getFieldWriteMode(prop as string); if (typeof value === "object" && !(value instanceof ObjectField)) debugger; - else target[Update]({ '$set': { ["fields." + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) : (value === undefined ? null : value) } }); + if (!writeMode || (writeMode === DocServer.WriteMode.SameUser && receiver.author === CurrentUserUtils.email)) { + if (value === undefined) target[Update]({ '$unset': { ["fields." + prop]: "" } }); + else target[Update]({ '$set': { ["fields." + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) : (value === undefined ? null : value) } }); + } else { + DocServer.registerDocWithCachedUpdate(receiver, prop as string, curValue); + } UndoManager.AddEvent({ redo: () => receiver[prop] = value, undo: () => receiver[prop] = curValue diff --git a/src/scraping/buxton/scraper.py b/src/scraping/buxton/scraper.py index f0f45d8f9..807216ef1 100644 --- a/src/scraping/buxton/scraper.py +++ b/src/scraping/buxton/scraper.py @@ -17,6 +17,7 @@ dist = "../../server/public/files" db = MongoClient("localhost", 27017)["Dash"] target_collection = db.newDocuments +target_doc_title = "Workspace 1" schema_guids = [] common_proto_id = "" @@ -69,7 +70,7 @@ def text_doc_map(string_list): return listify(proxify_guids(list(map(guid_map, string_list)))) -def write_schema(parse_results, display_fields, storage_key): +def write_collection(parse_results, display_fields, storage_key, viewType=2): view_guids = parse_results["child_guids"] data_doc = parse_results["schema"] @@ -90,7 +91,7 @@ def write_schema(parse_results, display_fields, storage_key): "zoomBasis": 1, "zIndex": 2, "libraryBrush": False, - "viewType": 2 + "viewType": viewType }, "__type": "Doc" } @@ -130,8 +131,7 @@ def write_text_doc(content): "x": 10, "y": 10, "width": 400, - "zIndex": 2, - "libraryBrush": False + "zIndex": 2 }, "__type": "Doc" } @@ -183,8 +183,7 @@ def write_image(folder, name): "x": 10, "y": 10, "width": min(800, native_width), - "zIndex": 2, - "libraryBrush": False + "zIndex": 2 }, "__type": "Doc" } @@ -237,7 +236,7 @@ def parse_document(file_name: str): copyfile(dir_path + "/" + image, dir_path + "/" + image.replace(".", "_o.", 1)) copyfile(dir_path + "/" + image, dir_path + - "/" + image.replace(".", "_m.", 1)) + "/" + image.replace(".", "_m.", 1)) print(f"extracted {count} images...") def sanitize(line): return re.sub("[\n\t]+", "", line).replace(u"\u00A0", " ").replace( @@ -381,22 +380,22 @@ candidates = 0 for file_name in os.listdir(source): if file_name.endswith('.docx'): candidates += 1 - schema_guids.append(write_schema( + schema_guids.append(write_collection( parse_document(file_name), ["title", "data"], "image_data")) print("writing parent schema...") -parent_guid = write_schema({ +parent_guid = write_collection({ "schema": { "_id": guid(), "fields": {}, "__type": "Doc" }, "child_guids": schema_guids -}, ["title", "short_description", "original_price"], "data") +}, ["title", "short_description", "original_price"], "data", 1) print("appending parent schema to main workspace...\n") target_collection.update_one( - {"fields.title": "WS collection 1"}, + {"fields.title": target_doc_title}, {"$push": {"fields.data.fields": {"fieldId": parent_guid, "__type": "proxy"}}} ) diff --git a/src/scraping/buxton/source/Bill_Notes_Apple_Mac_Portable.docx b/src/scraping/buxton/source/Bill_Notes_Apple_Mac_Portable.docx Binary files differnew file mode 100644 index 000000000..df1aafe9c --- /dev/null +++ b/src/scraping/buxton/source/Bill_Notes_Apple_Mac_Portable.docx diff --git a/src/scraping/buxton/source/Bill_Notes_Bill_Notes_CyKey.docx b/src/scraping/buxton/source/Bill_Notes_Bill_Notes_CyKey.docx Binary files differindex 649d636e3..06094b4d3 100644 --- a/src/scraping/buxton/source/Bill_Notes_Bill_Notes_CyKey.docx +++ b/src/scraping/buxton/source/Bill_Notes_Bill_Notes_CyKey.docx diff --git a/src/scraping/buxton/source/Bill_Notes_FrogPad.docx b/src/scraping/buxton/source/Bill_Notes_FrogPad.docx Binary files differindex ba80c1959..d01e1bf5c 100644 --- a/src/scraping/buxton/source/Bill_Notes_FrogPad.docx +++ b/src/scraping/buxton/source/Bill_Notes_FrogPad.docx diff --git a/src/scraping/buxton/source/Bill_Notes_Gavilan_SC.docx b/src/scraping/buxton/source/Bill_Notes_Gavilan_SC.docx Binary files differindex 8558a4e13..b9a30c8a9 100644 --- a/src/scraping/buxton/source/Bill_Notes_Gavilan_SC.docx +++ b/src/scraping/buxton/source/Bill_Notes_Gavilan_SC.docx diff --git a/src/scraping/buxton/source/Bill_Notes_Grandjean_Stenotype.docx b/src/scraping/buxton/source/Bill_Notes_Grandjean_Stenotype.docx Binary files differindex 09e17f971..0615c4953 100644 --- a/src/scraping/buxton/source/Bill_Notes_Grandjean_Stenotype.docx +++ b/src/scraping/buxton/source/Bill_Notes_Grandjean_Stenotype.docx diff --git a/src/scraping/buxton/source/Bill_Notes_NewO.docx b/src/scraping/buxton/source/Bill_Notes_NewO.docx Binary files differindex 2f4a04e81..a514926d2 100644 --- a/src/scraping/buxton/source/Bill_Notes_NewO.docx +++ b/src/scraping/buxton/source/Bill_Notes_NewO.docx diff --git a/src/scraping/buxton/source/Bill_Notes_PARCkbd.docx b/src/scraping/buxton/source/Bill_Notes_PARCkbd.docx Binary files differindex 3038de363..c0cf6ba9a 100644 --- a/src/scraping/buxton/source/Bill_Notes_PARCkbd.docx +++ b/src/scraping/buxton/source/Bill_Notes_PARCkbd.docx diff --git a/src/scraping/buxton/source/Bill_Notes_PARCtab.doc b/src/scraping/buxton/source/Bill_Notes_PARCtab.doc Binary files differnew file mode 100644 index 000000000..3cdc2d21b --- /dev/null +++ b/src/scraping/buxton/source/Bill_Notes_PARCtab.doc diff --git a/src/server/GarbageCollector.ts b/src/server/GarbageCollector.ts index ea5388004..09b52eadf 100644 --- a/src/server/GarbageCollector.ts +++ b/src/server/GarbageCollector.ts @@ -13,7 +13,7 @@ function addDoc(doc: any, ids: string[], files: { [name: string]: string[] }) { if (field === undefined || field === null) { continue; } - if (field.__type === "proxy") { + if (field.__type === "proxy" || field.__type === "prefetch_proxy") { ids.push(field.fieldId); } else if (field.__type === "list") { addDoc(field.fields, ids, files); diff --git a/src/server/index.ts b/src/server/index.ts index 10a84c823..29b44713c 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,6 +1,6 @@ require('dotenv').config(); import * as bodyParser from 'body-parser'; -import { exec } from 'child_process'; +import { exec, ExecOptions } from 'child_process'; import * as cookieParser from 'cookie-parser'; import * as express from 'express'; import * as session from 'express-session'; @@ -148,6 +148,33 @@ app.get("/pull", (req, res) => res.redirect("/"); })); +app.get("/buxton", (req, res) => { + let cwd = '../scraping/buxton'; + + let onResolved = (stdout: string) => { console.log(stdout); res.redirect("/"); }; + let onRejected = (err: any) => { console.error(err.message); res.send(err); }; + let tryPython3 = () => command_line('python3 scraper.py', cwd).then(onResolved, onRejected); + + command_line('python scraper.py', cwd).then(onResolved, tryPython3); +}); + +const command_line = (command: string, fromDirectory?: string) => { + return new Promise<string>((resolve, reject) => { + let options: ExecOptions = {}; + if (fromDirectory) { + options.cwd = path.join(__dirname, fromDirectory); + } + exec(command, options, (err, stdout) => err ? reject(err) : resolve(stdout)); + }); +}; + +const read_text_file = (relativePath: string) => { + let target = path.join(__dirname, relativePath); + return new Promise<string>((resolve, reject) => { + fs.readFile(target, (err, data) => err ? reject(err) : resolve(data.toString())); + }); +}; + app.get("/version", (req, res) => { exec('"C:\\Program Files\\Git\\bin\\git.exe" rev-parse HEAD', (err, stdout, stderr) => { if (err) { diff --git a/src/server/remapUrl.ts b/src/server/remapUrl.ts index 69c766d56..5218a239a 100644 --- a/src/server/remapUrl.ts +++ b/src/server/remapUrl.ts @@ -2,6 +2,8 @@ import { Database } from "./database"; import { Search } from "./Search"; import * as path from 'path'; +//npx ts-node src/server/remapUrl.ts + const suffixMap: { [type: string]: true } = { "video": true, "pdf": true, |