diff options
Diffstat (limited to 'src/client')
40 files changed, 1639 insertions, 1544 deletions
| diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index 87a87be92..bf5168c22 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,42 @@ export namespace DocServer {      let GUID: string;      // indicates whether or not a document is currently being udpated, and, if so, its id +    export enum WriteMode { +        Default = 0, //Anything goes +        Playground = 1, +        LiveReadonly = 2, +        LivePlayground = 3, +    } + +    const fieldWriteModes: { [field: string]: WriteMode } = {}; +    const docsWithUpdates: { [field: string]: Set<Doc> } = {}; + +    export function setFieldWriteMode(field: string, writeMode: WriteMode) { +        fieldWriteModes[field] = writeMode; +        if (writeMode !== WriteMode.Playground) { +            const docs = docsWithUpdates[field]; +            if (docs) { +                docs.forEach(doc => Doc.RunCachedUpdate(doc, field)); +                delete docsWithUpdates[field]; +            } +        } +    } + +    export function getFieldWriteMode(field: string) { +        return fieldWriteModes[field] || WriteMode.Default; +    } + +    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/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 15471371a..6616d5d58 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -562,7 +562,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>                  } else {                      dW && (doc.width = actualdW);                      dH && (doc.height = actualdH); -                    dH && Doc.SetInPlace(element.props.Document, "autoHeight", undefined, true); +                    dH && element.props.Document.autoHeight && Doc.SetInPlace(element.props.Document, "autoHeight", false, true);                  }              }          }); 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 1cf13aa74..0e687737d 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -39,5 +39,10 @@ let swapDocs = async () => {      (await Cast(CurrentUserUtils.UserDocument.sidebar, Doc))!.chromeStatus = "disabled";      CurrentUserUtils.UserDocument.chromeStatus = "disabled";      await swapDocs(); +    document.getElementById('root')!.addEventListener('wheel', event => { +        if (event.ctrlKey) { +            event.preventDefault(); +        } +    }, true);      ReactDOM.render(<MainView />, document.getElementById('root'));  })();
\ No newline at end of file diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 3941c9c20..8f68f7c0d 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'; @@ -40,6 +40,7 @@ import { CollectionTreeView } from './collections/CollectionTreeView';  import { ClientUtils } from '../util/ClientUtils';  import { SchemaHeaderField, RandomPastel } from '../../new_fields/SchemaHeaderField';  import { TimelineMenu } from './animationtimeline/TimelineMenu'; +import { DictationManager } from '../util/DictationManager';  @observer  export class MainView extends React.Component { @@ -48,6 +49,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));      } @@ -65,6 +90,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'); @@ -409,6 +466,20 @@ export class MainView extends React.Component {          ];          if (!ClientUtils.RELEASE) btns.unshift([React.createRef<HTMLDivElement>(), "cat", "Add Cat Image", addImageNode]); +        const setWriteMode = (mode: DocServer.WriteMode) => { +            console.log(DocServer.WriteMode[mode]); +            const mode1 = mode; +            const mode2 = mode === DocServer.WriteMode.Default ? mode : DocServer.WriteMode.Playground; +            DocServer.setFieldWriteMode("x", mode1); +            DocServer.setFieldWriteMode("y", mode1); +            DocServer.setFieldWriteMode("width", mode1); +            DocServer.setFieldWriteMode("height", mode1); + +            DocServer.setFieldWriteMode("panX", mode2); +            DocServer.setFieldWriteMode("panY", mode2); +            DocServer.setFieldWriteMode("scale", mode2); +            DocServer.setFieldWriteMode("viewType", mode2); +        };          return < div id="add-nodes-menu" style={{ left: this.flyoutWidth + 20, bottom: 20 }} >              <input type="checkbox" id="add-menu-toggle" ref={this.addMenuToggle} />              <label htmlFor="add-menu-toggle" style={{ marginTop: 2 }} title="Add Node"><p>+</p></label> @@ -427,6 +498,12 @@ 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> +                    {ClientUtils.RELEASE ? [] : [ +                        <li key="test"><button className="add-button round-button" title="Default" onClick={() => setWriteMode(DocServer.WriteMode.Default)}><FontAwesomeIcon icon="exclamation" size="sm" /></button></li>, +                        <li key="test1"><button className="add-button round-button" title="Playground" onClick={() => setWriteMode(DocServer.WriteMode.Playground)}><FontAwesomeIcon icon="exclamation" size="sm" /></button></li>, +                        <li key="test2"><button className="add-button round-button" title="Live Playground" onClick={() => setWriteMode(DocServer.WriteMode.LivePlayground)}><FontAwesomeIcon icon="exclamation" size="sm" /></button></li>, +                        <li key="test3"><button className="add-button round-button" title="Live Readonly" onClick={() => setWriteMode(DocServer.WriteMode.LiveReadonly)}><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} /> @@ -469,9 +546,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/CollectionPDFView.tsx b/src/client/views/collections/CollectionPDFView.tsx index 70010819a..8eda4d9ee 100644 --- a/src/client/views/collections/CollectionPDFView.tsx +++ b/src/client/views/collections/CollectionPDFView.tsx @@ -1,66 +1,31 @@ -import { action, IReactionDisposer, observable, reaction } from "mobx"; +import { computed } from "mobx";  import { observer } from "mobx-react"; -import { WidthSym, HeightSym } from "../../../new_fields/Doc";  import { Id } from "../../../new_fields/FieldSymbols"; -import { NumCast } from "../../../new_fields/Types";  import { emptyFunction } from "../../../Utils";  import { ContextMenu } from "../ContextMenu";  import { FieldView, FieldViewProps } from "../nodes/FieldView"; +import { PDFBox } from "../nodes/PDFBox";  import { CollectionBaseView, CollectionRenderProps, CollectionViewType } from "./CollectionBaseView";  import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView";  import "./CollectionPDFView.scss";  import React = require("react"); -import { PDFBox } from "../nodes/PDFBox";  @observer  export class CollectionPDFView extends React.Component<FieldViewProps> { -    private _pdfBox?: PDFBox; -    private _reactionDisposer?: IReactionDisposer; -    private _buttonTray: React.RefObject<HTMLDivElement>; - -    constructor(props: FieldViewProps) { -        super(props); - -        this._buttonTray = React.createRef(); -    } - -    componentDidMount() { -        this._reactionDisposer = reaction( -            () => NumCast(this.props.Document.scrollY), -            () => { -                this.props.Document.panY = NumCast(this.props.Document.scrollY); -            }, -            { fireImmediately: true } -        ); -    } - -    componentWillUnmount() { -        this._reactionDisposer && this._reactionDisposer(); -    } -      public static LayoutString(fieldKey: string = "data", fieldExt: string = "annotations") {          return FieldView.LayoutString(CollectionPDFView, fieldKey, fieldExt);      } -    @observable _inThumb = false; -    private set curPage(value: number) { this._pdfBox && this._pdfBox.GotoPage(value); } -    private get curPage() { return NumCast(this.props.Document.curPage, -1); } -    private get numPages() { return NumCast(this.props.Document.numPages); } -    @action onPageBack = () => this._pdfBox && this._pdfBox.BackPage(); -    @action onPageForward = () => this._pdfBox && this._pdfBox.ForwardPage(); +    private _pdfBox?: PDFBox; +    private _buttonTray: React.RefObject<HTMLDivElement> = React.createRef(); -    nativeWidth = () => NumCast(this.props.Document.nativeWidth); -    nativeHeight = () => NumCast(this.props.Document.nativeHeight); -    private get uIButtons() { -        let ratio = (this.curPage - 1) / this.numPages * 100; +    @computed +    get uIButtons() {          return (              <div className="collectionPdfView-buttonTray" ref={this._buttonTray} key="tray" style={{ height: "100%" }}> -                <button className="collectionPdfView-backward" onClick={this.onPageBack}>{"<"}</button> -                <button className="collectionPdfView-forward" onClick={this.onPageForward}>{">"}</button> -                {/* <div className="collectionPdfView-slider" onPointerDown={this.onThumbDown} style={{ top: 60, left: -20, width: 50, height: `calc(100% - 80px)` }} > -                    <div className="collectionPdfView-thumb" onPointerDown={this.onThumbDown} style={{ top: `${ratio}%`, width: 50, height: 50 }} /> -                </div> */} +                <button className="collectionPdfView-backward" onClick={() => this._pdfBox && this._pdfBox.BackPage()}>{"<"}</button> +                <button className="collectionPdfView-forward" onClick={() => this._pdfBox && this._pdfBox.ForwardPage()}>{">"}</button>              </div>          );      } @@ -73,20 +38,16 @@ export class CollectionPDFView extends React.Component<FieldViewProps> {      setPdfBox = (pdfBox: PDFBox) => { this._pdfBox = pdfBox; }; - -    private subView = (_type: CollectionViewType, renderProps: CollectionRenderProps) => { -        let props = { ...this.props, ...renderProps }; -        return ( -            <> -                <CollectionFreeFormView {...props} setPdfBox={this.setPdfBox} CollectionView={this} chromeCollapsed={true} /> -                {renderProps.active() ? this.uIButtons : (null)} -            </> -        ); +    subView = (_type: CollectionViewType, renderProps: CollectionRenderProps) => { +        return (<> +            <CollectionFreeFormView {...this.props} {...renderProps} setPdfBox={this.setPdfBox} CollectionView={this} chromeCollapsed={true} /> +            {renderProps.active() ? this.uIButtons : (null)} +        </>);      }      render() {          return ( -            <CollectionBaseView {...this.props} className={`collectionPdfView-cont${this._inThumb ? "-dragging" : ""}`} onContextMenu={this.onContextMenu}> +            <CollectionBaseView {...this.props} className={"collectionPdfView-cont"} onContextMenu={this.onContextMenu}>                  {this.subView}              </CollectionBaseView>          ); diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 75787c0a8..897796174 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 @@ -303,13 +303,13 @@ export class SchemaTable extends React.Component<SchemaTableProps> {              return resized;          }, [] as { "id": string, "value": number }[]);      } -    @computed get sorted(): { "id": string, "desc"?: true }[] { +    @computed get sorted(): { id: string, desc: boolean }[] {          return this.columns.reduce((sorted, shf) => {              if (shf.desc) {                  sorted.push({ "id": shf.heading, "desc": shf.desc });              }              return sorted; -        }, [] as { "id": string, "desc"?: true }[]); +        }, [] as { id: string, desc: boolean }[]);      }      @computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); } diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 4a751c84c..b87be5d68 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -76,7 +76,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {      componentDidMount() {          // is there any reason this needs to exist? -syip -        this._heightDisposer = reaction(() => [this.yMargin, this.props.Document[WidthSym](), this.gridGap, this.columnWidth, this.childDocs.map(d => [d.height, d.width, d.zoomBasis, d.nativeHeight, d.nativeWidth, d.isMinimized])], +        this._heightDisposer = reaction(() => [this.props.Document.autoHeight, this.yMargin, this.props.Document[WidthSym](), this.gridGap, this.columnWidth, this.childDocs.map(d => [d.height, d.width, d.zoomBasis, d.nativeHeight, d.nativeWidth, d.isMinimized])],              () => {                  if (this.singleColumn && BoolCast(this.props.Document.autoHeight)) {                      let hgt = this.Sections.size * 50 + this.filteredChildren.reduce((height, d, i) => { diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 02b2583cd..24bd24d11 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -27,7 +27,6 @@ import "./CollectionTreeView.scss";  import React = require("react");  import { ComputedField } from '../../../new_fields/ScriptField';  import { KeyValueBox } from '../nodes/KeyValueBox'; -import { exportNamedDeclaration } from 'babel-types';  export interface TreeViewProps { @@ -71,8 +70,9 @@ class TreeView extends React.Component<TreeViewProps> {      private _header?: React.RefObject<HTMLDivElement> = React.createRef();      private _treedropDisposer?: DragManager.DragDropDisposer;      private _dref = React.createRef<HTMLDivElement>(); +    get defaultExpandedView() { return this.childDocs ? this.fieldKey : "fields"; }      @observable _collapsed: boolean = true; -    @computed get treeViewExpandedView() { return StrCast(this.props.document.treeViewExpandedView, "fields"); } +    @computed get treeViewExpandedView() { return StrCast(this.props.document.treeViewExpandedView, this.defaultExpandedView); }      @computed get MAX_EMBED_HEIGHT() { return NumCast(this.props.document.maxEmbedHeight, 300); }      @computed get dataDoc() { return this.resolvedDataDoc ? this.resolvedDataDoc : this.props.document; }      @computed get fieldKey() { @@ -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); @@ -341,10 +341,12 @@ class TreeView extends React.Component<TreeViewProps> {          let headerElements = (              <span className="collectionTreeView-keyHeader" key={this.treeViewExpandedView}                  onPointerDown={action(() => { -                    this.props.document.treeViewExpandedView = this.treeViewExpandedView === this.fieldKey ? "fields" : -                        this.treeViewExpandedView === "fields" && this.props.document.layout ? "layout" : -                            this.treeViewExpandedView === "layout" && this.props.document.links ? "links" : -                                this.childDocs ? this.fieldKey : "fields"; +                    if (!this._collapsed) { +                        this.props.document.treeViewExpandedView = this.treeViewExpandedView === this.fieldKey ? "fields" : +                            this.treeViewExpandedView === "fields" && this.props.document.layout ? "layout" : +                                this.treeViewExpandedView === "layout" && this.props.document.links ? "links" : +                                    this.childDocs ? this.fieldKey : "fields"; +                    }                      this._collapsed = false;                  })}>                  {this.treeViewExpandedView} @@ -357,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 25146a886..694bfbe4f 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"; @@ -187,6 +187,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 }}> @@ -219,6 +249,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 764d066cb..32c181557 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,8 +29,8 @@ 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"; @@ -40,6 +40,8 @@ import v5 = require("uuid/v5");  import { Timeline } from "../../animationtimeline/Timeline";  import { number } from "prop-types";  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); @@ -51,6 +53,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); @@ -355,12 +490,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {      @action      onPointerWheel = (e: React.WheelEvent): void => {          if (BoolCast(this.props.Document.lockedPosition)) return; -        // if (!this.props.active()) { -        //     return; -        // } -        if (this.props.Document.type === "pdf") { +        if (!e.ctrlKey && this.props.Document.scrollHeight !== undefined) { // things that can scroll vertically should do that instead of zooming +            e.stopPropagation();              return;          } +          let childSelected = this.childDocs.some(doc => {              var dv = DocumentManager.Instance.getDocumentView(doc);              return dv && SelectionManager.IsSelected(dv) ? true : false; @@ -369,21 +503,20 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {              return;          }          e.stopPropagation(); -        const coefficient = 1000; - -        if (e.ctrlKey) { -            let deltaScale = (1 - (e.deltaY / coefficient)); -            let nw = this.nativeWidth * deltaScale; -            let nh = this.nativeHeight * deltaScale; -            if (nw && nh) { -                this.props.Document.nativeWidth = nw; -                this.props.Document.nativeHeight = nh; -            } -            e.stopPropagation(); -            e.preventDefault(); -        } else { -            // if (modes[e.deltaMode] === 'pixels') coefficient = 50; -            // else if (modes[e.deltaMode] === 'lines') coefficient = 1000; // This should correspond to line-height?? + +        // bcz: this changes the nativewidth/height, but ImageBox will just revert it back to its defaults.  need more logic to fix. +        // if (e.ctrlKey && this.props.Document.scrollHeight === undefined) { +        //     let deltaScale = (1 - (e.deltaY / coefficient)); +        //     let nw = this.nativeWidth * deltaScale; +        //     let nh = this.nativeHeight * deltaScale; +        //     if (nw && nh) { +        //         this.props.Document.nativeWidth = nw; +        //         this.props.Document.nativeHeight = nh; +        //     } +        //     e.preventDefault(); +        // }  +        // else  +        {              let deltaScale = e.deltaY > 0 ? (1 / 1.1) : 1.1;              if (deltaScale * this.zoomScaling() < 1 && this.isAnnotationOverlay) {                  deltaScale = 1 / this.zoomScaling(); @@ -395,21 +528,19 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {              let safeScale = Math.min(Math.max(0.15, localTransform.Scale), 40);              this.props.Document.scale = Math.abs(safeScale);              this.setPan(-localTransform.TranslateX / safeScale, -localTransform.TranslateY / safeScale); -            e.stopPropagation(); +            e.preventDefault();          }      }      @action      setPan(panX: number, panY: number) { -        if (BoolCast(this.props.Document.lockedPosition)) return; -        this.props.Document.panTransformType = "None"; -        var scale = this.getLocalTransform().inverse().Scale; -        const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX)); -        const newPanY = Math.min((1 - 1 / scale) * this.nativeHeight, Math.max(0, panY)); -        this.props.Document.panX = this.isAnnotationOverlay ? newPanX : panX; -        this.props.Document.panY = this.isAnnotationOverlay && StrCast(this.props.Document.backgroundLayout).indexOf("PDFBox") === -1 ? newPanY : panY; -        if (this.props.Document.scrollY) { -            this.props.Document.scrollY = panY - scale * this.props.Document[HeightSym](); +        if (!BoolCast(this.props.Document.lockedPosition)) { +            this.props.Document.panTransformType = "None"; +            var scale = this.getLocalTransform().inverse().Scale; +            const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX)); +            const newPanY = Math.min((this.props.Document.scrollHeight !== undefined ? NumCast(this.props.Document.scrollHeight) : (1 - 1 / scale) * this.nativeHeight), Math.max(0, panY)); +            this.props.Document.panX = this.isAnnotationOverlay ? newPanX : panX; +            this.props.Document.panY = this.isAnnotationOverlay ? newPanY : panY;          }      } @@ -493,16 +624,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {          this.Document.scale = scale;      } -    getScale = () => { -        if (this.Document.scale) { -            return this.Document.scale; -        } -        return 1; -    } - +    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, @@ -561,7 +685,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"); @@ -590,20 +726,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) => { @@ -625,14 +755,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() { @@ -645,11 +778,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({ @@ -674,41 +842,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 95970cb81..5a7e96522 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 @@ -648,8 +650,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); }; @@ -696,22 +698,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 eb8a7a1c3..c5f4490a0 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/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index f10079169..0d4b377dd 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -73,6 +73,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {      public static ApplyKVPScript(doc: Doc, key: string, kvpScript: KVPScript): boolean {          const { script, type, onDelegate } = kvpScript; +        //const target = onDelegate ? (doc.layout instanceof Doc ? doc.layout : doc) : Doc.GetProto(doc); // bcz: need to be able to set fields on layout templates          const target = onDelegate ? doc : Doc.GetProto(doc);          let field: Field;          if (type === "computed") { 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.scss b/src/client/views/nodes/PDFBox.scss index e7655d598..c88a94c28 100644 --- a/src/client/views/nodes/PDFBox.scss +++ b/src/client/views/nodes/PDFBox.scss @@ -1,37 +1,3 @@ -.react-pdf__Page { -    transform-origin: left top; -    position: absolute; -    top: 0; -    left: 0; -} - -.react-pdf__Page__textContent span { -    user-select: text; -} - -.react-pdf__Document { -    position: absolute; -} - -.pdfBox-buttonTray { -    position: absolute; -    top: 0; -    left: 0; -    z-index: 25; -    pointer-events: all; -} - -.pdfBox-thumbnail { -    position: absolute; -    width: 100%; -} - -.pdfButton { -    pointer-events: all; -    width: 100px; -    height: 100px; -} -  .pdfBox-cont,  .pdfBox-cont-interactive {      display: flex; @@ -39,30 +5,24 @@      height: 100%;      overflow-y: scroll;      overflow-x: hidden; +    .pdfBox-scrollHack { +        pointer-events: none; +    }  }  .pdfBox-cont {      pointer-events: none; - -    .textlayer { -        pointer-events: none; - +    .pdfPage-textlayer {          span {              pointer-events: none !important; +            user-select: none;           }      } - -    .page-cont { -        pointer-events: none; -    }  }  .pdfBox-cont-interactive {      pointer-events: all; -    display: flex; -    flex-direction: row; - -    .textlayer { +    .pdfPage-textlayer {          span {              pointer-events: all !important;              user-select: text; @@ -70,11 +30,22 @@      }  } -.pdfBox-contentContainer { -    position: absolute; +.react-pdf__Page {      transform-origin: left top; +    position: absolute; +    top: 0; +    left: 0;  } +.react-pdf__Page__textContent span { +    user-select: text; +} + +.react-pdf__Document { +    position: absolute; +} + +  .pdfBox-settingsCont {      position: absolute;      right: 0; diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index a49709e83..6450cb826 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -1,31 +1,24 @@ -import { action, IReactionDisposer, observable, reaction, trace, untracked, computed } from 'mobx'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';  import { observer } from "mobx-react"; +import * as Pdfjs from "pdfjs-dist"; +import "pdfjs-dist/web/pdf_viewer.css";  import 'react-image-lightbox/style.css'; -import { WidthSym, Doc } from "../../../new_fields/Doc"; +import { Doc, WidthSym, Opt } from "../../../new_fields/Doc";  import { makeInterface } from "../../../new_fields/Schema"; -import { Cast, NumCast, BoolCast } from "../../../new_fields/Types"; +import { ScriptField } from '../../../new_fields/ScriptField'; +import { BoolCast, Cast, NumCast } from "../../../new_fields/Types";  import { PdfField } from "../../../new_fields/URLField"; -//@ts-ignore -// import { Document, Page } from "react-pdf"; -// import 'react-pdf/dist/Page/AnnotationLayer.css'; -import { RouteStore } from "../../../server/RouteStore"; +import { KeyCodes } from '../../northstar/utils/KeyCodes'; +import { CompileScript } from '../../util/Scripting';  import { DocComponent } from "../DocComponent";  import { InkingControl } from "../InkingControl"; -import { FilterBox } from "../search/FilterBox"; -import { Annotation } from './Annotation';  import { PDFViewer } from "../pdf/PDFViewer";  import { positionSchema } from "./DocumentView";  import { FieldView, FieldViewProps } from './FieldView';  import { pageSchema } from "./ImageBox";  import "./PDFBox.scss";  import React = require("react"); -import { CompileScript } from '../../util/Scripting'; -import { Flyout, anchorPoints } from '../DocumentDecorations'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { ScriptField } from '../../../new_fields/ScriptField'; -import { KeyCodes } from '../../northstar/utils/KeyCodes'; -import { Utils } from '../../../Utils'; -import { Id } from '../../../new_fields/FieldSymbols';  type PdfDocument = makeInterface<[typeof positionSchema, typeof pageSchema]>;  const PdfDocument = makeInterface(positionSchema, pageSchema); @@ -35,40 +28,34 @@ export const handleBackspace = (e: React.KeyboardEvent) => { if (e.keyCode === K  export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocument) {      public static LayoutString() { return FieldView.LayoutString(PDFBox); } +    @observable private _flyout: boolean = false;      @observable private _alt = false; -    @observable private _scrollY: number = 0; +    @observable private _pdf: Opt<Pdfjs.PDFDocumentProxy>; + +    @computed get containingCollectionDocument() { return this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document; }      @computed get dataDoc() { return BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document; } +    @computed get fieldExtensionDoc() { return Doc.resolvedFieldDataDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, "true"); } -    @observable private _flyout: boolean = false; -    private _mainCont: React.RefObject<HTMLDivElement>; +    private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();      private _reactionDisposer?: IReactionDisposer;      private _keyValue: string = "";      private _valueValue: string = "";      private _scriptValue: string = ""; -    private _keyRef: React.RefObject<HTMLInputElement>; -    private _valueRef: React.RefObject<HTMLInputElement>; -    private _scriptRef: React.RefObject<HTMLInputElement>; +    private _keyRef: React.RefObject<HTMLInputElement> = React.createRef(); +    private _valueRef: React.RefObject<HTMLInputElement> = React.createRef(); +    private _scriptRef: React.RefObject<HTMLInputElement> = React.createRef(); -    constructor(props: FieldViewProps) { -        super(props); +    componentDidMount() { +        this.props.setPdfBox && this.props.setPdfBox(this); -        this._mainCont = React.createRef(); +        const pdfUrl = Cast(this.props.Document.data, PdfField); +        if (pdfUrl instanceof PdfField) { +            Pdfjs.getDocument(pdfUrl.url.pathname).promise.then(pdf => runInAction(() => this._pdf = pdf)); +        }          this._reactionDisposer = reaction( -            () => this.props.Document.scrollY, -            () => { -                if (this._mainCont.current) { -                    this._mainCont.current && this._mainCont.current.scrollTo({ top: NumCast(this.Document.scrollY), behavior: "auto" }); -                } -            } +            () => this.props.Document.panY, +            () => this._mainCont.current && this._mainCont.current.scrollTo({ top: NumCast(this.Document.panY), behavior: "auto" })          ); - -        this._keyRef = React.createRef(); -        this._valueRef = React.createRef(); -        this._scriptRef = React.createRef(); -    } - -    componentDidMount() { -        if (this.props.setPdfBox) this.props.setPdfBox(this);      }      componentWillUnmount() { @@ -76,184 +63,144 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen      }      public GetPage() { -        return Math.floor(NumCast(this.props.Document.scrollY) / NumCast(this.dataDoc.pdfHeight)) + 1; +        return Math.floor(NumCast(this.props.Document.panY) / NumCast(this.dataDoc.nativeHeight)) + 1;      } + +    @action      public BackPage() { -        let cp = Math.ceil(NumCast(this.props.Document.scrollY) / NumCast(this.dataDoc.pdfHeight)) + 1; +        let cp = Math.ceil(NumCast(this.props.Document.panY) / NumCast(this.dataDoc.nativeHeight)) + 1;          cp = cp - 1;          if (cp > 0) {              this.props.Document.curPage = cp; -            this.props.Document.scrollY = (cp - 1) * NumCast(this.dataDoc.pdfHeight); +            this.props.Document.panY = (cp - 1) * NumCast(this.dataDoc.nativeHeight);          }      } + +    @action      public GotoPage(p: number) {          if (p > 0 && p <= NumCast(this.props.Document.numPages)) {              this.props.Document.curPage = p; -            this.props.Document.scrollY = (p - 1) * NumCast(this.dataDoc.pdfHeight); +            this.props.Document.panY = (p - 1) * NumCast(this.dataDoc.nativeHeight);          }      } +    @action      public ForwardPage() {          let cp = this.GetPage() + 1;          if (cp <= NumCast(this.props.Document.numPages)) {              this.props.Document.curPage = cp; -            this.props.Document.scrollY = (cp - 1) * NumCast(this.dataDoc.pdfHeight); +            this.props.Document.panY = (cp - 1) * NumCast(this.dataDoc.nativeHeight);          }      } -    private newKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => { -        this._keyValue = e.currentTarget.value; -    } - -    private newValueChange = (e: React.ChangeEvent<HTMLInputElement>) => { -        this._valueValue = e.currentTarget.value; -    } -      @action -    private newScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => { -        this._scriptValue = e.currentTarget.value; +    setPanY = (y: number) => { +        this.containingCollectionDocument && (this.containingCollectionDocument.panY = y);      } +    @action      private applyFilter = () => { -        let scriptText = ""; -        if (this._scriptValue.length > 0) { -            scriptText = this._scriptValue; -        } else if (this._keyValue.length > 0 && this._valueValue.length > 0) { -            scriptText = `return this.${this._keyValue} === ${this._valueValue}`; -        } -        else { -            scriptText = "return true"; -        } +        let scriptText = this._scriptValue.length > 0 ? this._scriptValue : +            this._keyValue.length > 0 && this._valueValue.length > 0 ? +                `return this.${this._keyValue} === ${this._valueValue}` : "return true";          let script = CompileScript(scriptText, { params: { this: Doc.name } }); -        if (script.compiled) { -            this.props.Document.filterScript = new ScriptField(script); -        } +        script.compiled && (this.props.Document.filterScript = new ScriptField(script));      } -    @action -    private toggleFlyout = () => { -        this._flyout = !this._flyout; +    scrollTo = (y: number) => { +        this._mainCont.current && this._mainCont.current.scrollTo({ top: Math.max(y - (this._mainCont.current.offsetHeight / 2), 0), behavior: "auto" });      } -    @action      private resetFilters = () => {          this._keyValue = this._valueValue = "";          this._scriptValue = "return true"; -        if (this._keyRef.current) { -            this._keyRef.current.value = ""; -        } -        if (this._valueRef.current) { -            this._valueRef.current.value = ""; -        } -        if (this._scriptRef.current) { -            this._scriptRef.current.value = ""; -        } +        this._keyRef.current && (this._keyRef.current.value = ""); +        this._valueRef.current && (this._valueRef.current.value = ""); +        this._scriptRef.current && (this._scriptRef.current.value = "");          this.applyFilter();      } - -    scrollTo(y: number) { -        this._mainCont.current && this._mainCont.current.scrollTo({ top: Math.max(y - (this._mainCont.current.offsetHeight / 2), 0), behavior: "auto" }); -    } +    private newKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => this._keyValue = e.currentTarget.value; +    private newValueChange = (e: React.ChangeEvent<HTMLInputElement>) => this._valueValue = e.currentTarget.value; +    private newScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => this._scriptValue = e.currentTarget.value;      settingsPanel() {          return !this.props.active() ? (null) : -            ( -                <div className="pdfBox-settingsCont" onPointerDown={(e) => e.stopPropagation()}> -                    <button className="pdfBox-settingsButton" onClick={this.toggleFlyout} title="Open Annotation Settings" -                        style={{ marginTop: `${NumCast(this.props.ContainingCollectionView!.props.Document.panY)}px` }}> -                        <div className="pdfBox-settingsButton-arrow" -                            style={{ -                                borderTop: `25px solid ${this._flyout ? "#121721" : "transparent"}`, -                                borderBottom: `25px solid ${this._flyout ? "#121721" : "transparent"}`, -                                borderRight: `25px solid ${this._flyout ? "transparent" : "#121721"}`, -                                transform: `scaleX(${this._flyout ? -1 : 1})` -                            }}></div> -                        <div className="pdfBox-settingsButton-iconCont"> -                            <FontAwesomeIcon style={{ color: "white" }} icon="cog" size="3x" /> -                        </div> -                    </button> -                    <div className="pdfBox-settingsFlyout" style={{ left: `${this._flyout ? -600 : 100}px` }} > -                        <div className="pdfBox-settingsFlyout-title"> -                            Annotation View Settings -                        </div> -                        <div className="pdfBox-settingsFlyout-kvpInput"> -                            <input placeholder="Key" className="pdfBox-settingsFlyout-input" onKeyDown={handleBackspace} onChange={this.newKeyChange} -                                style={{ gridColumn: 1 }} ref={this._keyRef} /> -                            <input placeholder="Value" className="pdfBox-settingsFlyout-input" onKeyDown={handleBackspace} onChange={this.newValueChange} -                                style={{ gridColumn: 3 }} ref={this._valueRef} /> -                        </div> -                        <div className="pdfBox-settingsFlyout-kvpInput"> -                            <input placeholder="Custom Script" onChange={this.newScriptChange} onKeyDown={handleBackspace} style={{ gridColumn: "1 / 4" }} ref={this._scriptRef} /> -                        </div> -                        <div className="pdfBox-settingsFlyout-kvpInput"> -                            <button style={{ gridColumn: 1 }} onClick={this.resetFilters}> -                                <FontAwesomeIcon style={{ color: "white" }} icon="trash" size="lg" /> -                                  Reset Filters -                            </button> -                            <button style={{ gridColumn: 3 }} onClick={this.applyFilter}> -                                <FontAwesomeIcon style={{ color: "white" }} icon="check" size="lg" /> -                                  Apply -                            </button> -                        </div> +            (<div className="pdfBox-settingsCont" onPointerDown={(e) => e.stopPropagation()}> +                <button className="pdfBox-settingsButton" onClick={action(() => this._flyout = !this._flyout)} title="Open Annotation Settings" +                    style={{ marginTop: `${this.containingCollectionDocument ? NumCast(this.containingCollectionDocument.panY) : 0}px` }}> +                    <div className="pdfBox-settingsButton-arrow" +                        style={{ +                            borderTop: `25px solid ${this._flyout ? "#121721" : "transparent"}`, +                            borderBottom: `25px solid ${this._flyout ? "#121721" : "transparent"}`, +                            borderRight: `25px solid ${this._flyout ? "transparent" : "#121721"}`, +                            transform: `scaleX(${this._flyout ? -1 : 1})` +                        }} /> +                    <div className="pdfBox-settingsButton-iconCont"> +                        <FontAwesomeIcon style={{ color: "white" }} icon="cog" size="3x" /> +                    </div> +                </button> +                <div className="pdfBox-settingsFlyout" style={{ left: `${this._flyout ? -600 : 100}px` }} > +                    <div className="pdfBox-settingsFlyout-title"> +                        Annotation View Settings +                    </div> +                    <div className="pdfBox-settingsFlyout-kvpInput"> +                        <input placeholder="Key" className="pdfBox-settingsFlyout-input" onKeyDown={handleBackspace} onChange={this.newKeyChange} +                            style={{ gridColumn: 1 }} ref={this._keyRef} /> +                        <input placeholder="Value" className="pdfBox-settingsFlyout-input" onKeyDown={handleBackspace} onChange={this.newValueChange} +                            style={{ gridColumn: 3 }} ref={this._valueRef} /> +                    </div> +                    <div className="pdfBox-settingsFlyout-kvpInput"> +                        <input placeholder="Custom Script" onChange={this.newScriptChange} onKeyDown={handleBackspace} style={{ gridColumn: "1 / 4" }} ref={this._scriptRef} /> +                    </div> +                    <div className="pdfBox-settingsFlyout-kvpInput"> +                        <button style={{ gridColumn: 1 }} onClick={this.resetFilters}> +                            <FontAwesomeIcon style={{ color: "white" }} icon="trash" size="lg" /> +                              Reset Filters +                        </button> +                        <button style={{ gridColumn: 3 }} onClick={this.applyFilter}> +                            <FontAwesomeIcon style={{ color: "white" }} icon="check" size="lg" /> +                              Apply +                        </button>                      </div>                  </div> -            ); +            </div>);      }      loaded = (nw: number, nh: number, np: number) => { -        if (this.props.Document) { -            let doc = this.dataDoc; -            doc.numPages = np; -            if (doc.nativeWidth && doc.nativeHeight) return; -            let oldaspect = NumCast(doc.nativeHeight) / NumCast(doc.nativeWidth, 1); -            doc.nativeWidth = nw; -            if (doc.nativeHeight) doc.nativeHeight = nw * oldaspect; -            else doc.nativeHeight = nh; -            let ccv = this.props.ContainingCollectionView; -            if (ccv) { -                ccv.props.Document.pdfHeight = nh; -            } -            doc.height = nh * (doc[WidthSym]() / nw); +        this.dataDoc.numPages = np; +        if (!this.dataDoc.nativeWidth || !this.dataDoc.nativeHeight || !this.dataDoc.scrollHeight) { +            let oldaspect = NumCast(this.dataDoc.nativeHeight) / NumCast(this.dataDoc.nativeWidth, 1); +            this.dataDoc.nativeWidth = nw; +            this.dataDoc.nativeHeight = this.dataDoc.nativeHeight ? nw * oldaspect : nh; +            this.dataDoc.height = this.dataDoc[WidthSym]() * (nh / nw); +            this.dataDoc.scrollHeight = np * this.dataDoc.nativeHeight;          }      }      @action      onScroll = (e: React.UIEvent<HTMLDivElement>) => { - -        if (e.currentTarget) { -            this._scrollY = e.currentTarget.scrollTop; -            let ccv = this.props.ContainingCollectionView; -            if (ccv) { -                ccv.props.Document.panTransformType = "None"; -                ccv.props.Document.scrollY = this._scrollY; -            } +        if (e.currentTarget && this.containingCollectionDocument) { +            this.containingCollectionDocument.panTransformType = "None"; +            this.containingCollectionDocument.panY = e.currentTarget.scrollTop;          }      } - -    @computed get fieldExtensionDoc() { -        return Doc.resolvedFieldDataDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, "true"); -    }      render() { -        // uses mozilla pdf as default          const pdfUrl = Cast(this.props.Document.data, PdfField); -        if (!(pdfUrl instanceof PdfField)) return <div>{`pdf, ${this.props.Document.data}, not found`}</div>;          let classname = "pdfBox-cont" + (this.props.active() && !InkingControl.Instance.selectedTool && !this._alt ? "-interactive" : ""); -        return ( +        return (!(pdfUrl instanceof PdfField) || !this._pdf ? +            <div>{`pdf, ${this.props.Document.data}, not found`}</div> :              <div className={classname}                  onScroll={this.onScroll} -                style={{ -                    marginTop: `${NumCast(this.props.ContainingCollectionView!.props.Document.panY)}px` -                }} -                ref={this._mainCont} -                onWheel={(e: React.WheelEvent) => { -                    e.stopPropagation(); -                }}> -                <PDFViewer url={pdfUrl.url.pathname} loaded={this.loaded} scrollY={this._scrollY} parent={this} /> -                {/* <div style={{ width: "100px", height: "300px" }}></div> */} +                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, 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> -        ); +            </div>);      } -  }
\ No newline at end of file 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 0ea85d522..0c6df74f0 100644 --- a/src/client/views/pdf/Annotation.scss +++ b/src/client/views/pdf/Annotation.scss @@ -1,4 +1,7 @@ -.pdfViewer-annotationBox { +.pdfAnnotation {      pointer-events: all;      user-select: none; +    position: absolute; +    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 a08ff5969..7ba7b6d14 100644 --- a/src/client/views/pdf/Annotation.tsx +++ b/src/client/views/pdf/Annotation.tsx @@ -4,34 +4,26 @@ import { observer } from "mobx-react";  import { Doc, DocListCast, HeightSym, WidthSym } from "../../../new_fields/Doc";  import { Id } from "../../../new_fields/FieldSymbols";  import { List } from "../../../new_fields/List"; -import { BoolCast, Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types"; +import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types";  import { DocumentManager } from "../../util/DocumentManager";  import { PresentationView } from "../presentationview/PresentationView";  import PDFMenu from "./PDFMenu";  import "./Annotation.scss"; -import { AnnotationTypes, scale, Viewer } from "./PDFViewer"; +import { scale } from "./PDFViewer";  interface IAnnotationProps {      anno: Doc;      index: number; -    parent: Viewer; +    ParentIndex: () => number; +    fieldExtensionDoc: Doc; +    scrollTo?: (n: number) => void; +    addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;  }  export default class Annotation extends React.Component<IAnnotationProps> {      render() { -        let annotationDocs = DocListCast(this.props.anno.annotations); -        let res = annotationDocs.map(a => { -            let type = NumCast(a.type); -            switch (type) { -                // case AnnotationTypes.Pin: -                //     return <PinAnnotation parent={this} document={a} x={NumCast(a.x)} y={NumCast(a.y)} width={a[WidthSym]()} height={a[HeightSym]()} key={a[Id]} />; -                case AnnotationTypes.Region: -                    return <RegionAnnotation parent={this.props.parent} document={a} index={this.props.index} x={NumCast(a.x)} y={NumCast(a.y)} width={a[WidthSym]()} height={a[HeightSym]()} key={a[Id]} />; -                default: -                    return <div></div>; -            } -        }); -        return res; +        return DocListCast(this.props.anno.annotations).map(a => ( +            <RegionAnnotation {...this.props} document={a} x={NumCast(a.x)} y={NumCast(a.y)} width={a[WidthSym]()} height={a[HeightSym]()} key={a[Id]} />));      }  } @@ -41,44 +33,29 @@ interface IRegionAnnotationProps {      width: number;      height: number;      index: number; -    parent: Viewer; +    ParentIndex: () => number; +    fieldExtensionDoc: Doc; +    scrollTo?: (n: number) => void; +    addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;      document: Doc;  }  @observer  class RegionAnnotation extends React.Component<IRegionAnnotationProps> { -    @observable private _backgroundColor: string = "red"; -      private _reactionDisposer?: IReactionDisposer;      private _scrollDisposer?: IReactionDisposer; -    private _mainCont: React.RefObject<HTMLDivElement>; - -    constructor(props: IRegionAnnotationProps) { -        super(props); - -        this._mainCont = React.createRef(); -    } +    private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();      componentDidMount() {          this._reactionDisposer = reaction( -            () => BoolCast(this.props.document.delete), -            () => { -                if (BoolCast(this.props.document.delete)) { -                    if (this._mainCont.current) { -                        this._mainCont.current.style.display = "none"; -                    } -                } -            }, +            () => this.props.document.delete, +            (del) => del && this._mainCont.current && (this._mainCont.current.style.display = "none"),              { fireImmediately: true }          );          this._scrollDisposer = reaction( -            () => this.props.parent.Index, -            () => { -                if (this.props.parent.Index === this.props.index) { -                    this.props.parent.scrollTo(this.props.y * scale); -                } -            } +            () => this.props.ParentIndex(), +            (ind) => ind === this.props.index && this.props.scrollTo && this.props.scrollTo(this.props.y * scale)          );      } @@ -88,16 +65,15 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {      }      deleteAnnotation = () => { -        let annotation = DocListCast(this.props.parent.props.parent.fieldExtensionDoc.annotations); +        let annotation = DocListCast(this.props.fieldExtensionDoc.annotations);          let group = FieldValue(Cast(this.props.document.group, Doc)); -        if (group && annotation.indexOf(group) !== -1) { -            let newAnnotations = annotation.filter(a => a !== FieldValue(Cast(this.props.document.group, Doc))); -            this.props.parent.props.parent.fieldExtensionDoc.annotations = new List<Doc>(newAnnotations); -        } -          if (group) { -            let groupAnnotations = DocListCast(group.annotations); -            groupAnnotations.forEach(anno => anno.delete = true); +            if (annotation.indexOf(group) !== -1) { +                let newAnnotations = annotation.filter(a => a !== FieldValue(Cast(this.props.document.group, Doc))); +                this.props.fieldExtensionDoc.annotations = new List<Doc>(newAnnotations); +            } + +            DocListCast(group.annotations).forEach(anno => anno.delete = true);          }          PDFMenu.Instance.fadeOut(true); @@ -105,9 +81,7 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {      pinToPres = () => {          let group = FieldValue(Cast(this.props.document.group, Doc)); -        if (group) { -            PresentationView.Instance.PinDoc(group); -        } +        group && PresentationView.Instance.PinDoc(group);      }      @action @@ -118,7 +92,7 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {                  let context = await Cast(targetDoc.targetContext, Doc);                  if (context) {                      DocumentManager.Instance.jumpToDocument(targetDoc, false, false, -                        ((doc) => this.props.parent.props.parent.props.addDocTab(targetDoc!, undefined, e.ctrlKey ? "onRight" : "inTab")), +                        ((doc) => this.props.addDocTab(targetDoc!, undefined, e.ctrlKey ? "onRight" : "inTab")),                          undefined, undefined);                  }              } @@ -144,15 +118,13 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {      }      render() { -        return ( -            <div className="pdfViewer-annotationBox" 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, -                    backgroundColor: this.props.parent.Index === this.props.index ? "green" : StrCast(this.props.document.color) -                }}></div> -        ); +        return (<div className="pdfAnnotation" onPointerDown={this.onPointerDown} ref={this._mainCont} +            style={{ +                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) +            }} />);      }  }
\ No newline at end of file diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx index 27c2a8f1a..3ed81faef 100644 --- a/src/client/views/pdf/PDFMenu.tsx +++ b/src/client/views/pdf/PDFMenu.tsx @@ -11,36 +11,34 @@ import { handleBackspace } from "../nodes/PDFBox";  export default class PDFMenu extends React.Component {      static Instance: PDFMenu; +    private _offsetY: number = 0; +    private _offsetX: number = 0; +    private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); +    private _commentCont = React.createRef<HTMLButtonElement>(); +    private _snippetButton: React.RefObject<HTMLButtonElement> = React.createRef(); +    private _dragging: boolean = false; +      @observable private _top: number = -300;      @observable private _left: number = -300;      @observable private _opacity: number = 1;      @observable private _transition: string = "opacity 0.5s";      @observable private _transitionDelay: string = ""; - - -    StartDrag: (e: PointerEvent, ele: HTMLElement) => void = emptyFunction; -    Highlight: (d: Doc | undefined, color: string | undefined) => void = emptyFunction; -    Delete: () => void = emptyFunction; -    Snippet: (marquee: { left: number, top: number, width: number, height: number }) => void = emptyFunction; -    AddTag: (key: string, value: string) => boolean = returnFalse; -    PinToPres: () => void = emptyFunction; +    @observable private _keyValue: string = ""; +    @observable private _valueValue: string = ""; +    @observable private _added: boolean = false;      @observable public Highlighting: boolean = false;      @observable public Status: "pdf" | "annotation" | "snippet" | "" = "";      @observable public Pinned: boolean = false; +    public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = emptyFunction; +    public Highlight: (d: Doc | undefined, color: string) => void = emptyFunction; +    public Delete: () => void = emptyFunction; +    public Snippet: (marquee: { left: number, top: number, width: number, height: number }) => void = emptyFunction; +    public AddTag: (key: string, value: string) => boolean = returnFalse; +    public PinToPres: () => void = emptyFunction;      public Marquee: { left: number; top: number; width: number; height: number; } | undefined; -    private _offsetY: number = 0; -    private _offsetX: number = 0; -    private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); -    private _commentCont = React.createRef<HTMLButtonElement>(); -    private _snippetButton: React.RefObject<HTMLButtonElement> = React.createRef(); -    private _dragging: boolean = false; -    @observable private _keyValue: string = ""; -    @observable private _valueValue: string = ""; -    @observable private _added: boolean = false; -      constructor(props: Readonly<{}>) {          super(props); @@ -61,12 +59,10 @@ export default class PDFMenu extends React.Component {          e.stopPropagation();          e.preventDefault(); -        if (this._dragging) { -            return; +        if (!this._dragging) { +            this.StartDrag(e, this._commentCont.current!); +            this._dragging = true;          } - -        this.StartDrag(e, this._commentCont.current!); -        this._dragging = true;      }      pointerUp = (e: PointerEvent) => { @@ -126,9 +122,20 @@ export default class PDFMenu extends React.Component {      @action      togglePin = (e: React.MouseEvent) => {          this.Pinned = !this.Pinned; -        if (!this.Pinned) { -            this.Highlighting = false; -        } +        !this.Pinned && (this.Highlighting = false); +    } + +    dragStart = (e: React.PointerEvent) => { +        document.removeEventListener("pointermove", this.dragging); +        document.addEventListener("pointermove", this.dragging); +        document.removeEventListener("pointerup", this.dragEnd); +        document.addEventListener("pointerup", this.dragEnd); + +        this._offsetX = this._mainCont.current!.getBoundingClientRect().width - e.nativeEvent.offsetX; +        this._offsetY = e.nativeEvent.offsetY; + +        e.stopPropagation(); +        e.preventDefault();      }      @action @@ -147,19 +154,6 @@ export default class PDFMenu extends React.Component {          e.preventDefault();      } -    dragStart = (e: React.PointerEvent) => { -        document.removeEventListener("pointermove", this.dragging); -        document.addEventListener("pointermove", this.dragging); -        document.removeEventListener("pointerup", this.dragEnd); -        document.addEventListener("pointerup", this.dragEnd); - -        this._offsetX = this._mainCont.current!.getBoundingClientRect().width - e.nativeEvent.offsetX; -        this._offsetY = e.nativeEvent.offsetY; - -        e.stopPropagation(); -        e.preventDefault(); -    } -      @action      highlightClicked = (e: React.MouseEvent) => {          if (!this.Pinned) { @@ -193,13 +187,10 @@ export default class PDFMenu extends React.Component {      snippetDrag = (e: PointerEvent) => {          e.stopPropagation();          e.preventDefault(); -        if (this._dragging) { -            return; -        } -        this._dragging = true; +        if (!this._dragging) { +            this._dragging = true; -        if (this.Marquee) { -            this.Snippet(this.Marquee); +            this.Marquee && this.Snippet(this.Marquee);          }      } @@ -226,36 +217,32 @@ export default class PDFMenu extends React.Component {          if (this._keyValue.length > 0 && this._valueValue.length > 0) {              this._added = this.AddTag(this._keyValue, this._valueValue); -            setTimeout( -                () => { -                    runInAction(() => { -                        this._added = false; -                    }); -                }, 1000 -            ); +            setTimeout(action(() => this._added = false), 1000);          }      }      render() { -        let buttons = this.Status === "pdf" || this.Status === "snippet" ? [ -            <button key="1" className="pdfMenu-button" title="Click to Highlight" onClick={this.highlightClicked} -                style={this.Highlighting ? { backgroundColor: "#121212" } : {}}> -                <FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: "transform 0.1s", transform: this.Highlighting ? "" : "rotate(-45deg)" }} /> -            </button>, -            <button className="pdfMenu-button" title="Drag to Annotate" ref={this._commentCont} onPointerDown={this.pointerDown}><FontAwesomeIcon icon="comment-alt" size="lg" key="2" /></button>, -            this.Status === "snippet" ? <button className="pdfMenu-button" title="Drag to Snippetize Selection" onPointerDown={this.snippetStart} ref={this._snippetButton}><FontAwesomeIcon icon="cut" size="lg" /></button> : undefined, -            <button key="3" className="pdfMenu-button" title="Pin Menu" onClick={this.togglePin} -                style={this.Pinned ? { backgroundColor: "#121212" } : {}}> -                <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transition: "transform 0.1s", transform: this.Pinned ? "rotate(45deg)" : "" }} /> -            </button> -        ] : [ -                <button key="4" className="pdfMenu-button" title="Delete Anchor" onPointerDown={this.deleteClicked}><FontAwesomeIcon icon="trash-alt" size="lg" key="1" /></button>, -                <button key="5" className="pdfMenu-button" title="Pin to Presentation" onPointerDown={this.PinToPres}><FontAwesomeIcon icon="map-pin" size="lg" key="2" /></button>, -                <div className="pdfMenu-addTag" key="3"> +        let buttons = this.Status === "pdf" || this.Status === "snippet" ? +            [ +                <button key="1" className="pdfMenu-button" title="Click to Highlight" onClick={this.highlightClicked} style={this.Highlighting ? { backgroundColor: "#121212" } : {}}> +                    <FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: "transform 0.1s", transform: this.Highlighting ? "" : "rotate(-45deg)" }} /></button>, +                <button key="2" className="pdfMenu-button" title="Drag to Annotate" ref={this._commentCont} onPointerDown={this.pointerDown}> +                    <FontAwesomeIcon icon="comment-alt" size="lg" /></button>, +                <button key="3" className="pdfMenu-button" title="Drag to Snippetize Selection" style={{ display: this.Status === "snippet" ? "" : "none" }} onPointerDown={this.snippetStart} ref={this._snippetButton}> +                    <FontAwesomeIcon icon="cut" size="lg" /></button>, +                <button key="4" className="pdfMenu-button" title="Pin Menu" onClick={this.togglePin} style={this.Pinned ? { backgroundColor: "#121212" } : {}}> +                    <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transition: "transform 0.1s", transform: this.Pinned ? "rotate(45deg)" : "" }} /> </button> +            ] : [ +                <button key="5" className="pdfMenu-button" title="Delete Anchor" onPointerDown={this.deleteClicked}> +                    <FontAwesomeIcon icon="trash-alt" size="lg" /></button>, +                <button key="6" className="pdfMenu-button" title="Pin to Presentation" onPointerDown={this.PinToPres}> +                    <FontAwesomeIcon icon="map-pin" size="lg" /></button>, +                <div key="7" className="pdfMenu-addTag" >                      <input onKeyDown={handleBackspace} onChange={this.keyChanged} placeholder="Key" style={{ gridColumn: 1 }} />                      <input onKeyDown={handleBackspace} onChange={this.valueChanged} placeholder="Value" style={{ gridColumn: 3 }} />                  </div>, -                <button key="6" className="pdfMenu-button" title={`Add tag: ${this._keyValue} with value: ${this._valueValue}`} onPointerDown={this.addTag}><FontAwesomeIcon style={{ transition: "all .2s" }} color={this._added ? "#42f560" : "white"} icon="check" size="lg" key="4" /></button>, +                <button key="8" className="pdfMenu-button" title={`Add tag: ${this._keyValue} with value: ${this._valueValue}`} onPointerDown={this.addTag}> +                    <FontAwesomeIcon style={{ transition: "all .2s" }} color={this._added ? "#42f560" : "white"} icon="check" size="lg" /></button>,              ];          return ( diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss index 7158aaffa..a2f3911c5 100644 --- a/src/client/views/pdf/PDFViewer.scss +++ b/src/client/views/pdf/PDFViewer.scss @@ -1,136 +1,93 @@ -.textLayer { -    div { -        user-select: text; -    } -} -.viewer-button-cont { -    position: absolute; -    display: flex; -    justify-content: space-evenly; -    align-items: center; -} - -.viewer-previousPage, -.viewer-nextPage { -    background: grey; -    font-weight: bold; -    opacity: 0.5; -    padding: 0 10px; -    border-radius: 5px; -} - -.textLayer { -    user-select: auto; -} -.viewer { -    // position: absolute; -    // top: 0; -} -.pdfViewere-viewer { +.pdfViewer-viewer {      pointer-events:inherit; -} -.pdfViewer-text { -    transform: scale(1.5); -    transform-origin: top left; -    .page { -        .canvasWrapper { -            display: none; -        } - -        .textLayer { -            position: relative; -            user-select: none; +    width: 100%; +    .pdfViewer-visibleElements { +        .pdfPage-cont { +            .pdfPage-textLayer { +                div { +                    user-select: text; +                } +                span { +                    color: transparent; +                    position: absolute; +                    white-space: pre; +                    cursor: text; +                    -webkit-transform-origin: 0% 0%; +                    transform-origin: 0% 0%; +                } +            }          }      } -} -.pdfViewer-viewerCont { -    width:100%; -} - -.page-cont { -    .textLayer { -        user-select: auto; - -        div { -            user-select: text; -        } +    .pdfViewer-text { +        transform: scale(1.5); +        transform-origin: top left;      } -} - -.pdfViewer-overlayCont { -    position: absolute; -    width: 100%; -    height: 100px; -    background: #121721; -    bottom: 0; -    display: flex; -    justify-content: center; -    align-items: center; -    padding: 20px; -    overflow: hidden; -    transition: left .5s; -} - -.pdfViewer-overlaySearchBar { -    width: 20%; -    height: 100%; -    font-size: 30px; -    padding: 5px; -} -.pdfViewer-overlayButton { -    border-bottom-left-radius: 50%; -    display: flex; -    justify-content: space-evenly; -    align-items: center; -    height: 70px; -    background: none; -    padding: 0; -    position: absolute; - -    .pdfViewer-overlayButton-arrow { -        width: 0; -        height: 0; -        border-top: 25px solid transparent; -        border-bottom: 25px solid transparent; -        border-right: 25px solid #121721; -        transition: all 0.5s; +    .pdfViewer-annotationLayer { +        position: absolute; +        top: 0; +        width: 100%; +        pointer-events: none; +        .pdfPage-annotationBox { +            position: absolute; +            background-color: red; +            opacity: 0.1; +        }      } -    .pdfViewer-overlayButton-iconCont { +    .pdfViewer-overlayCont { +        position: absolute; +        width: 100%; +        height: 100px;          background: #121721; -        height: 50px; -        width: 70px; +        bottom: 0;          display: flex;          justify-content: center;          align-items: center; -        margin-left: -2px; -        border-radius: 3px; +        padding: 20px; +        overflow: hidden; +        transition: left .5s; +        .pdfViewer-overlaySearchBar { +            width: 20%; +            height: 100%; +            font-size: 30px; +            padding: 5px; +        }      } -} -.pdfViewer-overlayButton:hover { -    background: none; -} +    .pdfViewer-overlayButton { +        border-bottom-left-radius: 50%; +        display: flex; +        justify-content: space-evenly; +        align-items: center; +        height: 70px; +        background: none; +        padding: 0; +        position: absolute; + +        .pdfViewer-overlayButton-arrow { +            width: 0; +            height: 0; +            border-top: 25px solid transparent; +            border-bottom: 25px solid transparent; +            border-right: 25px solid #121721; +            transition: all 0.5s; +        } -.pdfViewer-annotationBox { -    position: absolute; -    background-color: red; -    opacity: 0.1; -} +        .pdfViewer-overlayButton-iconCont { +            background: #121721; +            height: 50px; +            width: 70px; +            display: flex; +            justify-content: center; +            align-items: center; +            margin-left: -2px; +            border-radius: 3px; +        } +    } -.pdfViewer-annotationLayer { -    position: absolute; -    top: 0; -         width: 100%; -        pointer-events: none; +    .pdfViewer-overlayButton:hover { +        background: none; +    }  } - - - -.pdfViewer-pinAnnotation { -    background-color: red; -    position: absolute; -    border-radius: 100%; -}
\ No newline at end of file diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 6a99cec59..08674720d 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -1,167 +1,114 @@ +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";  import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";  import { observer } from "mobx-react";  import * as Pdfjs from "pdfjs-dist";  import "pdfjs-dist/web/pdf_viewer.css";  import * as rp from "request-promise";  import { Dictionary } from "typescript-collections"; -import { Doc, DocListCast, Opt } from "../../../new_fields/Doc"; +import { Doc, DocListCast, FieldResult } from "../../../new_fields/Doc";  import { Id } from "../../../new_fields/FieldSymbols";  import { List } from "../../../new_fields/List"; +import { ScriptField } from "../../../new_fields/ScriptField";  import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; -import { emptyFunction, Utils } from "../../../Utils"; +import { Utils, numberRange } from "../../../Utils"; +import { DocServer } from "../../DocServer";  import { Docs, DocUtils } from "../../documents/Documents"; -import { DragManager } from "../../util/DragManager"; -import { PDFBox } from "../nodes/PDFBox"; +import { KeyCodes } from "../../northstar/utils/KeyCodes"; +import { CompileScript, CompiledScript } from "../../util/Scripting"; +import Annotation from "./Annotation";  import Page from "./Page";  import "./PDFViewer.scss";  import React = require("react"); -import { CompileScript, CompileResult } from "../../util/Scripting"; -import { ScriptField } from "../../../new_fields/ScriptField"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import Annotation from "./Annotation"; -import { KeyCodes } from "../../northstar/utils/KeyCodes"; -import { DocServer } from "../../DocServer";  const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer");  export const scale = 2; -interface IPDFViewerProps { -    url: string; -    loaded: (nw: number, nh: number, np: number) => void; -    scrollY: number; -    parent: PDFBox; -} - -/** - * Wrapper that loads the PDF and cascades the pdf down - */ -@observer -export class PDFViewer extends React.Component<IPDFViewerProps> { -    @observable _pdf: Opt<Pdfjs.PDFDocumentProxy>; -    private _mainDiv = React.createRef<HTMLDivElement>(); - -    @action -    componentDidMount() { -        Pdfjs.getDocument(this.props.url).promise.then(pdf => runInAction(() => this._pdf = pdf)); -    } - -    render() { -        return ( -            <div className="pdfViewer-viewerCont" ref={this._mainDiv}> -                {!this._pdf ? (null) : -                    <Viewer pdf={this._pdf} loaded={this.props.loaded} scrollY={this.props.scrollY} parent={this.props.parent} mainCont={this._mainDiv} url={this.props.url} />} -            </div> -        ); -    } -}  interface IViewerProps {      pdf: Pdfjs.PDFDocumentProxy; -    loaded: (nw: number, nh: number, np: number) => void; -    scrollY: number; -    parent: PDFBox; -    mainCont: React.RefObject<HTMLDivElement>;      url: string; +    Document: Doc; +    DataDoc?: Doc; +    fieldExtensionDoc: Doc; +    fieldKey: string; +    loaded: (nw: number, nh: number, np: number) => void; +    panY: number; +    scrollTo: (y: number) => void; +    active: () => boolean; +    setPanY?: (n: number) => void; +    addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void; +    addDocument?: (doc: Doc, allowDuplicates?: boolean) => boolean;  }  /**   * Handles rendering and virtualization of the pdf   */  @observer -export class Viewer extends React.Component<IViewerProps> { -    // _visibleElements is the array of JSX elements that gets rendered -    @observable.shallow private _visibleElements: JSX.Element[] = []; -    // _isPage is an array that tells us whether or not an index is rendered as a page or as a placeholder -    @observable private _isPage: string[] = []; +export class PDFViewer extends React.Component<IViewerProps> { +    @observable.shallow private _visibleElements: JSX.Element[] = []; // _visibleElements is the array of JSX elements that gets rendered +    @observable private _isPage: string[] = [];// _isPage is an array that tells us whether or not an index is rendered as a page or as a placeholder      @observable private _pageSizes: { width: number, height: number }[] = [];      @observable private _annotations: Doc[] = [];      @observable private _savedAnnotations: Dictionary<number, HTMLDivElement[]> = new Dictionary<number, HTMLDivElement[]>(); -    @observable private _script: CompileResult | undefined; +    @observable private _script: CompiledScript = CompileScript("return true") as CompiledScript;      @observable private _searching: boolean = false; - -    @observable public Index: number = -1; +    @observable private Index: number = -1;      private _pageBuffer: number = 1;      private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();      private _reactionDisposer?: IReactionDisposer;      private _annotationReactionDisposer?: IReactionDisposer; -    private _dropDisposer?: DragManager.DragDropDisposer;      private _filterReactionDisposer?: IReactionDisposer; -    private _viewer: React.RefObject<HTMLDivElement>; -    private _mainCont: React.RefObject<HTMLDivElement>; +    private _viewer: React.RefObject<HTMLDivElement> = React.createRef(); +    private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();      private _pdfViewer: any; -    // private _textContent: Pdfjs.TextContent[] = [];      private _pdfFindController: any;      private _searchString: string = "";      private _selectionText: string = ""; -    constructor(props: IViewerProps) { -        super(props); +    @computed get panY(): number { return this.props.panY; } -        let scriptfield = Cast(this.props.parent.Document.filterScript, ScriptField); -        this._script = scriptfield ? scriptfield.script : CompileScript("return true"); -        this._viewer = React.createRef(); -        this._mainCont = React.createRef(); -    } +    // startIndex: where to start rendering pages +    @computed get startIndex(): number { return Math.max(0, this.getPageFromScroll(this.panY) - this._pageBuffer); } -    setSelectionText = (text: string) => { -        this._selectionText = text; +    // endIndex: where to end rendering pages +    @computed get endIndex(): number { +        return Math.min(this.props.pdf.numPages - 1, this.getPageFromScroll(this.panY + (this._pageSizes[0] ? this._pageSizes[0].height : 0)) + this._pageBuffer);      } -    componentDidUpdate = (prevProps: IViewerProps) => { -        if (this.scrollY !== prevProps.scrollY) { -            this.renderPages(); -        } +    @computed get filteredAnnotations() { +        return this._annotations.filter(anno => { +            let run = this._script.run({ this: anno }); +            return run.success ? run.result : true; +        });      } -    @action -    componentDidMount = () => { -        this._reactionDisposer = reaction( +    componentDidUpdate = (prevProps: IViewerProps) => this.panY !== prevProps.panY && this.renderPages(); -            () => [this.props.parent.props.active(), this.startIndex, this._pageSizes.length ? this.endIndex : 0], -            async () => { -                await this.initialLoad(); -                this.renderPages(); -            }, { fireImmediately: true }); +    componentDidMount = async () => { +        await this.initialLoad(); -        this._annotationReactionDisposer = reaction( -            () => { -                return this.props.parent && this.props.parent.fieldExtensionDoc && DocListCast(this.props.parent.fieldExtensionDoc.annotations); -            }, -            (annotations: Doc[]) => { -                annotations && annotations.length && this.renderAnnotations(annotations, true); -            }, +        this._reactionDisposer = reaction( +            () => [this.props.active(), this.startIndex, this._pageSizes.length ? this.endIndex : 0], +            () => this.renderPages(),              { fireImmediately: true }); +        this._annotationReactionDisposer = reaction( +            () => this.props.fieldExtensionDoc && DocListCast(this.props.fieldExtensionDoc.annotations), +            annotations => annotations && annotations.length && this.renderAnnotations(annotations, true), +            { fireImmediately: true }); -        if (this.props.parent.props.ContainingCollectionView) { -            this._filterReactionDisposer = reaction( -                () => this.props.parent.Document.filterScript, -                () => { -                    runInAction(() => { -                        let scriptfield = Cast(this.props.parent.Document.filterScript, ScriptField); -                        this._script = scriptfield ? scriptfield.script : CompileScript("return true"); -                        if (this.props.parent.props.ContainingCollectionView) { -                            let fieldDoc = Doc.resolvedFieldDataDoc(this.props.parent.props.ContainingCollectionView.props.DataDoc ? -                                this.props.parent.props.ContainingCollectionView.props.DataDoc : this.props.parent.props.ContainingCollectionView.props.Document, this.props.parent.props.ContainingCollectionView.props.fieldKey, "true"); -                            let ccvAnnos = DocListCast(fieldDoc.annotations); -                            ccvAnnos.forEach(d => { -                                if (this._script && this._script.compiled) { -                                    let run = this._script.run(d); -                                    if (run.success) { -                                        d.opacity = run.result ? 1 : 0; -                                    } -                                } -                            }); -                        } -                        this.Index = -1; -                    }); -                } -            ); -        } - -        if (this._mainCont.current) { -            this._dropDisposer = this._mainCont.current && DragManager.MakeDropTarget(this._mainCont.current, { handlers: { drop: this.drop.bind(this) } }); -        } +        this._filterReactionDisposer = reaction( +            () => ({ scriptField: Cast(this.props.Document.filterScript, ScriptField), annos: this._annotations.slice() }), +            action(({ scriptField, annos }: { scriptField: FieldResult<ScriptField>, annos: Doc[] }) => { +                this._script = scriptField && scriptField.script.compiled ? scriptField.script : CompileScript("return true") as CompiledScript; +                annos.forEach(d => { +                    let run = this._script.run(d); +                    d.opacity = !run.success || run.result ? 1 : 0; +                }); +                this.Index = -1; +            }), +            { fireImmediately: true } +        );          document.removeEventListener("copy", this.copy);          document.addEventListener("copy", this.copy); @@ -171,162 +118,115 @@ export class Viewer extends React.Component<IViewerProps> {          this._reactionDisposer && this._reactionDisposer();          this._annotationReactionDisposer && this._annotationReactionDisposer();          this._filterReactionDisposer && this._filterReactionDisposer(); -        this._dropDisposer && this._dropDisposer();          document.removeEventListener("copy", this.copy);      } -    private copy = (e: ClipboardEvent) => { -        if (this.props.parent.props.active()) { -            let text = this._selectionText; -            if (e.clipboardData) { -                e.clipboardData.setData("text/plain", text); -                e.clipboardData.setData("dash/pdfOrigin", this.props.parent.props.Document[Id]); -                let annoDoc = this.makeAnnotationDocument(undefined, 0, "#0390fc"); -                e.clipboardData.setData("dash/pdfRegion", annoDoc[Id]); -                e.preventDefault(); -            } +    copy = (e: ClipboardEvent) => { +        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, "#0390fc")[Id]); +            e.preventDefault();          } -        // let targetAnnotations = DocListCast(this.props.parent.fieldExtensionDoc.annotations); -        // if (targetAnnotations) { -        //     targetAnnotations.push(destDoc); -        // }      }      paste = (e: ClipboardEvent) => { -        if (e.clipboardData) { -            if (e.clipboardData.getData("dash/pdfOrigin") === this.props.parent.props.Document[Id]) { -                let linkDocId = e.clipboardData.getData("dash/linkDoc"); -                if (linkDocId) { -                    DocServer.GetRefField(linkDocId).then(async (link) => { -                        if (!(link instanceof Doc)) { -                            return; -                        } -                        let proto = Doc.GetProto(link); -                        let source = await Cast(proto.anchor1, Doc); -                        proto.anchor2 = this.makeAnnotationDocument(source, 0, "#0390fc", false); -                    }); -                } -            } +        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), "#0390fc", false)));          }      } -    scrollTo(y: number) { -        if (this.props.mainCont.current) { -            this.props.parent.scrollTo(y); -        } -    } +    searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => this._searchString = e.currentTarget.value; + +    pageLoaded = (page: Pdfjs.PDFPageViewport): void => this.props.loaded(page.width, page.height, this.props.pdf.numPages); + +    setSelectionText = (text: string) => this._selectionText = text; + +    getIndex = () => this.Index;      @action      initialLoad = async () => {          if (this._pageSizes.length === 0) { -            let pageSizes = Array<{ width: number, height: number }>(this.props.pdf.numPages);              this._isPage = Array<string>(this.props.pdf.numPages); -            // this._textContent = Array<Pdfjs.TextContent>(this.props.pdf.numPages); -            const proms: Pdfjs.PDFPromise<any>[] = []; -            for (let i = 0; i < this.props.pdf.numPages; i++) { -                proms.push(this.props.pdf.getPage(i + 1).then(page => runInAction(() => { -                    pageSizes[i] = { +            this._pageSizes = Array<{ width: number, height: number }>(this.props.pdf.numPages); +            await Promise.all(this._pageSizes.map<Pdfjs.PDFPromise<any>>((val, i) => +                this.props.pdf.getPage(i + 1).then(action((page: Pdfjs.PDFPageProxy) => { +                    this._pageSizes.splice(i, 1, {                          width: (page.view[page.rotate === 0 || page.rotate === 180 ? 2 : 3] - page.view[page.rotate === 0 || page.rotate === 180 ? 0 : 1]) * scale,                          height: (page.view[page.rotate === 0 || page.rotate === 180 ? 3 : 2] - page.view[page.rotate === 0 || page.rotate === 180 ? 1 : 0]) * scale -                    }; -                    // let x = page.getViewport(scale); -                    // page.getTextContent().then((text: Pdfjs.TextContent) => { -                    //     // let tc = new Pdfjs.TextContentItem() -                    //     // let tc = {str: } -                    //     this._textContent[i] = text; -                    //     // text.items.forEach(t => { -                    //     //     tcStr += t.str; -                    //     // }) -                    // }); -                    // pageSizes[i] = { width: x.width, height: x.height }; -                }))); -            } -            await Promise.all(proms); -            runInAction(() => -                Array.from(Array((this._pageSizes = pageSizes).length).keys()).map(this.getPlaceholderPage)); -            this.props.loaded(Math.max(...pageSizes.map(i => i.width)), pageSizes[0].height, this.props.pdf.numPages); -            // this.props.loaded(Math.max(...pageSizes.map(i => i.width)), pageSizes[0].height, this.props.pdf.numPages); - -            let startY = NumCast(this.props.parent.Document.startY); -            let ccv = this.props.parent.props.ContainingCollectionView; -            if (ccv) { -                ccv.props.Document.panY = startY; -            } -            this.props.parent.Document.scrollY = 0; -            this.props.parent.Document.scrollY = startY + 1; +                    }); +                    this.getPlaceholderPage(i); +                })))); +            this.props.loaded(Math.max(...this._pageSizes.map(i => i.width)), this._pageSizes[0].height, this.props.pdf.numPages); + +            let startY = NumCast(this.props.Document.startY, NumCast(this.props.Document.panY)); +            this.props.setPanY && this.props.setPanY(startY);          }      }      @action -    makeAnnotationDocument = (sourceDoc: Doc | undefined, s: number, color: string, createLink: boolean = true): Doc => { -        let annoDocs: Doc[] = []; +    makeAnnotationDocument = (sourceDoc: Doc | undefined, color: string, createLink: boolean = true): Doc => {          let mainAnnoDoc = Docs.Create.InstanceFromProto(new Doc(), "", {}); - -        mainAnnoDoc.title = "Annotation on " + StrCast(this.props.parent.Document.title); -        mainAnnoDoc.pdfDoc = this.props.parent.props.Document; +        let mainAnnoDocProto = Doc.GetProto(mainAnnoDoc); +        let annoDocs: Doc[] = [];          let minY = Number.MAX_VALUE; -        this._savedAnnotations.forEach((key: number, value: HTMLDivElement[]) => { -            for (let anno of value) { +        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(); +            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) / scale; -                if (anno.style.top) { -                    annoDoc.y = parseInt(anno.style.top) / scale; -                    minY = Math.min(parseInt(anno.style.top), minY); -                } -                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 (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)); +            })); -        mainAnnoDoc.y = Math.max(minY, 0); -        mainAnnoDoc.annotations = new List<Doc>(annoDocs); +            mainAnnoDocProto.y = Math.max(minY, 0); +            mainAnnoDocProto.annotations = new List<Doc>(annoDocs); +        } +        mainAnnoDocProto.title = "Annotation on " + StrCast(this.props.Document.title); +        mainAnnoDocProto.annotationOn = this.props.Document;          if (sourceDoc && createLink) { -            DocUtils.MakeLink(sourceDoc, mainAnnoDoc, undefined, `Annotation from ${StrCast(this.props.parent.Document.title)}`, "", StrCast(this.props.parent.Document.title)); +            DocUtils.MakeLink(sourceDoc, mainAnnoDocProto, undefined, `Annotation from ${StrCast(this.props.Document.title)}`);          }          this._savedAnnotations.clear();          this.Index = -1;          return mainAnnoDoc;      } -    drop = async (e: Event, de: DragManager.DropEvent) => { -        // if (de.data instanceof DragManager.LinkDragData) { -        //     let sourceDoc = de.data.linkSourceDocument; -        //     let destDoc = this.makeAnnotationDocument(sourceDoc, 1, "red"); -        //     de.data.droppedDocuments.push(destDoc); -        //     let targetAnnotations = DocListCast(this.props.parent.fieldExtensionDoc.annotations); -        //     if (targetAnnotations) { -        //         targetAnnotations.push(destDoc); -        //     } -        //     else { -        //         this.props.parent.fieldExtensionDoc.annotations = new List<Doc>([destDoc]); -        //     } -        //     e.stopPropagation(); -        // } -    } -    /** -     * Called by the Page class when it gets rendered, initializes the lists and -     * puts a placeholder with all of the correct page sizes when all of the pages have been loaded. -     */ -    @action -    pageLoaded = (index: number, page: Pdfjs.PDFPageViewport): void => { -        this.props.loaded(page.width, page.height, this.props.pdf.numPages); -    } -      @action      getPlaceholderPage = (page: number) => {          if (this._isPage[page] !== "none") {              this._isPage[page] = "none";              this._visibleElements[page] = (                  <div key={`${this.props.url}-placeholder-${page + 1}`} className="pdfviewer-placeholder" -                    style={{ width: this._pageSizes[page].width, height: this._pageSizes[page].height }} /> -            ); +                    style={{ width: this._pageSizes[page].width, height: this._pageSizes[page].height }}> +                    "PAGE IS LOADING... " +                </div>);          }      } @@ -334,25 +234,19 @@ export class Viewer extends React.Component<IViewerProps> {      getRenderedPage = (page: number) => {          if (this._isPage[page] !== "page") {              this._isPage[page] = "page"; -            this._visibleElements[page] = ( -                <Page -                    setSelectionText={this.setSelectionText} -                    size={this._pageSizes[page]} -                    pdf={this.props.pdf} -                    page={page} -                    numPages={this.props.pdf.numPages} -                    key={`${this.props.url}-rendered-${page + 1}`} -                    name={`${this.props.pdf.fingerprint + `-page${page + 1}`}`} -                    pageLoaded={this.pageLoaded} -                    parent={this.props.parent} -                    makePin={emptyFunction} -                    renderAnnotations={this.renderAnnotations} -                    createAnnotation={this.createAnnotation} -                    sendAnnotations={this.receiveAnnotations} -                    makeAnnotationDocuments={this.makeAnnotationDocument} -                    getScrollFromPage={this.getScrollFromPage} -                    {...this.props} /> -            ); +            this._visibleElements[page] = (<Page {...this.props} +                size={this._pageSizes[page]} +                numPages={this.props.pdf.numPages} +                setSelectionText={this.setSelectionText} +                page={page} +                key={`${this.props.url}-rendered-${page + 1}`} +                name={`${this.props.pdf.fingerprint + `-page${page + 1}`}`} +                pageLoaded={this.pageLoaded} +                renderAnnotations={this.renderAnnotations} +                createAnnotation={this.createAnnotation} +                sendAnnotations={this.receiveAnnotations} +                makeAnnotationDocuments={this.makeAnnotationDocument} +                getScrollFromPage={this.getScrollFromPage} />);          }      } @@ -360,14 +254,12 @@ export class Viewer extends React.Component<IViewerProps> {      // file address of the pdf      @action      getPageImage = async (page: number) => { -        let handleError = () => this.getRenderedPage(page);          if (this._isPage[page] !== "image") {              this._isPage[page] = "image"; -            const address = this.props.url;              try { -                let res = JSON.parse(await rp.get(Utils.prepend(`/thumbnail${address.substring("files/".length, address.length - ".pdf".length)}-${page + 1}.PNG`))); +                let res = JSON.parse(await rp.get(Utils.prepend(`/thumbnail${this.props.url.substring("files/".length, this.props.url.length - ".pdf".length)}-${page + 1}.PNG`)));                  runInAction(() => this._visibleElements[page] = -                    <img key={res.path} src={res.path} onError={handleError} +                    <img key={res.path} src={res.path} onError={() => this.getRenderedPage(page)}                          style={{ width: `${parseInt(res.width) * scale}px`, height: `${parseInt(res.height) * scale}px` }} />);              } catch (e) {                  console.log(e); @@ -375,33 +267,14 @@ export class Viewer extends React.Component<IViewerProps> {          }      } -    @computed get scrollY(): number { return this.props.scrollY; } - -    // startIndex: where to start rendering pages -    @computed get startIndex(): number { return Math.max(0, this.getPageFromScroll(this.scrollY) - this._pageBuffer); } - -    // endIndex: where to end rendering pages -    @computed get endIndex(): number { -        return Math.min(this.props.pdf.numPages - 1, this.getPageFromScroll(this.scrollY + this._pageSizes[0].height) + this._pageBuffer); -    } - -    @action      renderPages = () => { -        for (let i = 0; i < this.props.pdf.numPages; i++) { -            if (i < this.startIndex || i > this.endIndex) { -                this.getPlaceholderPage(i);  // pages outside of the pdf use empty stand-in divs -            } else { -                if (this.props.parent.props.active()) { -                    this.getRenderedPage(i); -                } else { -                    this.getPageImage(i); -                } -            } -        } +        numberRange(this.props.pdf.numPages).filter(p => this._isPage[p] !== undefined).map(i => +            (i < this.startIndex || i > this.endIndex) ? this.getPlaceholderPage(i) : // pages outside of the pdf use empty stand-in divs +                this.props.active() ? this.getRenderedPage(i) : this.getPageImage(i));      }      @action -    private renderAnnotations = (annotations: Doc[], removeOldAnnotations: boolean): void => { +    renderAnnotations = (annotations: Doc[], removeOldAnnotations: boolean): void => {          if (removeOldAnnotations) {              this._annotations = annotations;          } @@ -412,6 +285,21 @@ export class Viewer extends React.Component<IViewerProps> {      }      @action +    prevAnnotation = (e: React.MouseEvent) => { +        e.stopPropagation(); +        this.Index = Math.max(this.Index - 1, 0); +    } + +    @action +    nextAnnotation = (e: React.MouseEvent) => { +        e.stopPropagation(); +        this.Index = Math.min(this.Index + 1, this.filteredAnnotations.length - 1); +    } + +    sendAnnotations = (page: number) => { +        return this._savedAnnotations.getValue(page); +    } +      receiveAnnotations = (annotations: HTMLDivElement[], page: number) => {          if (page === -1) {              this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove())); @@ -422,28 +310,21 @@ export class Viewer extends React.Component<IViewerProps> {          }      } -    sendAnnotations = (page: number): HTMLDivElement[] | undefined => { -        return this._savedAnnotations.getValue(page); -    } -      // get the page index that the vertical offset passed in is on      getPageFromScroll = (vOffset: number) => {          let index = 0;          let currOffset = vOffset; -        while (index < this._pageSizes.length && currOffset - this._pageSizes[index].height > 0) { +        while (index < this._pageSizes.length && this._pageSizes[index] && currOffset - this._pageSizes[index].height > 0) {              currOffset -= this._pageSizes[index++].height;          }          return index;      }      getScrollFromPage = (index: number): number => { -        let counter = 0; -        for (let i = 0; i < Math.min(this.props.pdf.numPages, index); i++) { -            counter += this._pageSizes[i].height; -        } -        return counter; +        return numberRange(Math.min(this.props.pdf.numPages, index)).reduce((counter, i) => counter + this._pageSizes[i].height, 0);      } +    @action      createAnnotation = (div: HTMLDivElement, page: number) => {          if (this._annotationLayer.current) {              if (div.style.top) { @@ -461,101 +342,30 @@ export class Viewer extends React.Component<IViewerProps> {          }      } -    renderAnnotation = (anno: Doc, index: number): JSX.Element => { -        return <Annotation anno={anno} index={index} parent={this} key={`${anno[Id]}-annotation`} />; -    } - -    @action -    pointerDown = () => { -        // this._searching = false; -    } -      @action      search = (searchString: string) => {          if (this._pdfViewer._pageViewsReady) { -            this._pdfFindController.executeCommand('find', -                { -                    caseSensitive: false, -                    findPrevious: undefined, -                    highlightAll: true, -                    phraseSearch: true, -                    query: searchString -                }); +            this._pdfFindController.executeCommand('find', { +                caseSensitive: false, +                findPrevious: undefined, +                highlightAll: true, +                phraseSearch: true, +                query: searchString +            });          } -        else { -            let container = this._mainCont.current; -            if (container) { -                container.addEventListener("pagesloaded", () => { -                    console.log("rendered"); -                    this._pdfFindController.executeCommand('find', -                        { -                            caseSensitive: false, -                            findPrevious: undefined, -                            highlightAll: true, -                            phraseSearch: true, -                            query: searchString -                        }); -                }); -                container.addEventListener("pagerendered", () => { -                    console.log("rendered"); -                    this._pdfFindController.executeCommand('find', -                        { -                            caseSensitive: false, -                            findPrevious: undefined, -                            highlightAll: true, -                            phraseSearch: true, -                            query: searchString -                        }); -                }); -            } +        else if (this._mainCont.current) { +            let executeFind = () => this._pdfFindController.executeCommand('find', { +                caseSensitive: false, +                findPrevious: undefined, +                highlightAll: true, +                phraseSearch: true, +                query: searchString +            }); +            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 -        //         }); -        // }      } -    searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => { -        this._searchString = e.currentTarget.value; -    }      @action      toggleSearch = (e: React.MouseEvent) => { @@ -563,29 +373,19 @@ export class Viewer extends React.Component<IViewerProps> {          this._searching = !this._searching;          if (this._searching) { -            let container = this._mainCont.current; -            let viewer = this._viewer.current; - -            if (!this._pdfFindController) { -                if (container && viewer) { -                    let simpleLinkService = new SimpleLinkService(); -                    this._pdfViewer = new PDFJSViewer.PDFViewer({ -                        container: container, -                        viewer: viewer, -                        linkService: simpleLinkService -                    }); -                    simpleLinkService.setPdf(this.props.pdf); -                    container.addEventListener("pagesinit", () => { -                        this._pdfViewer.currentScaleValue = 1; -                    }); -                    container.addEventListener("pagerendered", () => { -                        console.log("rendered"); -                    }); -                    this._pdfViewer.setDocument(this.props.pdf); -                    this._pdfFindController = new PDFJSViewer.PDFFindController(this._pdfViewer); -                    // this._pdfFindController._linkService = pdfLinkService; -                    this._pdfViewer.findController = this._pdfFindController; -                } +            if (!this._pdfFindController && this._mainCont.current && this._viewer.current) { +                let simpleLinkService = new SimpleLinkService(); +                this._pdfViewer = new PDFJSViewer.PDFViewer({ +                    container: this._mainCont.current, +                    viewer: this._viewer.current, +                    linkService: simpleLinkService +                }); +                simpleLinkService.setPdf(this.props.pdf); +                this._mainCont.current.addEventListener("pagesinit", () => this._pdfViewer.currentScaleValue = 1); +                this._mainCont.current.addEventListener("pagerendered", () => console.log("rendered")); +                this._pdfViewer.setDocument(this.props.pdf); +                this._pdfFindController = new PDFJSViewer.PDFFindController(this._pdfViewer); +                this._pdfViewer.findController = this._pdfFindController;              }          }          else { @@ -599,116 +399,45 @@ export class Viewer extends React.Component<IViewerProps> {          }      } -    @action -    prevAnnotation = (e: React.MouseEvent) => { -        e.stopPropagation(); - -        // if (this.Index > 0) { -        //     this.Index--; -        // } -        this.Index = Math.max(this.Index - 1, 0); -    } - -    @action -    nextAnnotation = (e: React.MouseEvent) => { -        e.stopPropagation(); - -        let compiled = this._script; -        let filtered = this._annotations.filter(anno => { -            if (compiled && compiled.compiled) { -                let run = compiled.run({ this: anno }); -                if (run.success) { -                    return run.result; -                } -            } -            return true; -        }); -        this.Index = Math.min(this.Index + 1, filtered.length - 1); -    } - -    nextResult = () => { -        // if (this._viewer.current) { -        //     let results = this._pdfFindController.pageMatches; -        //     if (results && results.length) { -        //         if (this._pageIndex === this.props.pdf.numPages && this._matchIndex === results[this._pageIndex].length - 1) { -        //             return; -        //         } -        //         if (this._pageIndex === -1 || this._matchIndex === results[this._pageIndex].length - 1) { -        //             this._matchIndex = 0; -        //             this._pageIndex++; -        //         } -        //         else { -        //             this._matchIndex++; -        //         } -        //         this._pdfFindController._nextMatch() -        // let nextMatch = this._viewer.current.children[this._pageIndex].children[1].children[results[this._pageIndex][this._matchIndex]]; -        // rconsole.log(nextMatch); -        // this.props.parent.scrollTo(nextMatch.getBoundingClientRect().top); -        // nextMatch.setAttribute("style", nextMatch.getAttribute("style") ? nextMatch.getAttribute("style") + ", background-color: green" : "background-color: green"); -        // } -        // } -    } -      render() { -        let compiled = this._script; -        return ( -            <div className="pdfViewer-viewer" ref={this._mainCont} onPointerDown={this.pointerDown}> -                <div className="viewer" style={this._searching ? { position: "absolute", top: 0 } : {}}> -                    {this._visibleElements} -                </div> -                <div className="pdfViewer-text" ref={this._viewer} /> -                <div className="pdfViewer-annotationLayer" style={{ height: this.props.parent.Document.nativeHeight }}> -                    <div className="pdfViewer-annotationLayer-subCont" ref={this._annotationLayer}> -                        {this._annotations.filter(anno => { -                            if (compiled && compiled.compiled) { -                                let run = compiled.run({ this: anno }); -                                if (run.success) { -                                    return run.result; -                                } -                            } -                            return true; -                        }).sort((a: Doc, b: Doc) => NumCast(a.y) - NumCast(b.y)) -                            .map((anno: Doc, index: number) => this.renderAnnotation(anno, index))} -                    </div> -                </div> -                <div className="pdfViewer-overlayCont" onPointerDown={(e) => e.stopPropagation()} -                    style={{ -                        bottom: -this.props.scrollY, -                        left: `${this._searching ? 0 : 100}%` -                    }}> -                    <button className="pdfViewer-overlayButton" title="Open Search Bar"></button> -                    {/* <button title="Previous Result" onClick={() => this.search(this._searchString)}><FontAwesomeIcon icon="arrow-up" size="3x" color="white" /></button> -                    <button title="Next Result" onClick={this.nextResult}><FontAwesomeIcon icon="arrow-down" size="3x" color="white" /></button> */} -                    <input onKeyDown={(e: React.KeyboardEvent) => e.keyCode === KeyCodes.ENTER ? this.search(this._searchString) : e.keyCode === KeyCodes.BACKSPACE ? e.stopPropagation() : true} placeholder="Search" className="pdfViewer-overlaySearchBar" onChange={this.searchStringChanged} /> -                    <button title="Search" onClick={() => this.search(this._searchString)}><FontAwesomeIcon icon="search" size="3x" color="white" /></button> -                </div> -                <button className="pdfViewer-overlayButton" onClick={this.prevAnnotation} title="Previous Annotation" -                    style={{ bottom: -this.props.scrollY + 280, right: 10, display: this.props.parent.props.active() ? "flex" : "none" }}> -                    <div className="pdfViewer-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}> -                        <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-up"} size="3x" /> -                    </div> -                </button> -                <button className="pdfViewer-overlayButton" onClick={this.nextAnnotation} title="Next Annotation" -                    style={{ bottom: -this.props.scrollY + 200, right: 10, display: this.props.parent.props.active() ? "flex" : "none" }}> -                    <div className="pdfViewer-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}> -                        <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-down"} size="3x" /> -                    </div> -                </button> -                <button className="pdfViewer-overlayButton" onClick={this.toggleSearch} title="Open Search Bar" -                    style={{ bottom: -this.props.scrollY + 10, right: 0, display: this.props.parent.props.active() ? "flex" : "none" }}> -                    <div className="pdfViewer-overlayButton-arrow" onPointerDown={(e) => e.stopPropagation()}></div> -                    <div className="pdfViewer-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}> -                        <FontAwesomeIcon style={{ color: "white" }} icon={this._searching ? "times" : "search"} size="3x" /> -                    </div> -                </button> -            </div > -        ); +        return (<div className="pdfViewer-viewer" ref={this._mainCont} > +            <div className="pdfViewer-visibleElements" style={this._searching ? { position: "absolute", top: 0 } : {}}> +                {this._visibleElements} +            </div> +            <div className="pdfViewer-text" ref={this._viewer} /> +            <div className="pdfViewer-annotationLayer" style={{ height: NumCast(this.props.Document.nativeHeight) }} ref={this._annotationLayer}> +                {this.filteredAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map((anno, index) => +                    <Annotation {...this.props} ParentIndex={this.getIndex} anno={anno} index={index} key={`${anno[Id]}-annotation`} />)} +            </div> +            <div className="pdfViewer-overlayCont" onPointerDown={(e) => e.stopPropagation()} +                style={{ bottom: -this.props.panY, left: `${this._searching ? 0 : 100}%` }}> +                <button className="pdfViewer-overlayButton" title="Open Search Bar" /> +                <input className="pdfViewer-overlaySearchBar" placeholder="Search" onChange={this.searchStringChanged} +                    onKeyDown={(e: React.KeyboardEvent) => e.keyCode === KeyCodes.ENTER ? this.search(this._searchString) : e.keyCode === KeyCodes.BACKSPACE ? e.stopPropagation() : true} /> +                <button title="Search" onClick={() => this.search(this._searchString)}> +                    <FontAwesomeIcon icon="search" size="3x" color="white" /></button> +            </div> +            <button className="pdfViewer-overlayButton" onClick={this.prevAnnotation} title="Previous Annotation" +                style={{ bottom: -this.props.panY + 280, right: 10, display: this.props.active() ? "flex" : "none" }}> +                <div className="pdfViewer-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}> +                    <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-up"} size="3x" /></div> +            </button> +            <button className="pdfViewer-overlayButton" onClick={this.nextAnnotation} title="Next Annotation" +                style={{ bottom: -this.props.panY + 200, right: 10, display: this.props.active() ? "flex" : "none" }}> +                <div className="pdfViewer-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}> +                    <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-down"} size="3x" /></div> +            </button> +            <button className="pdfViewer-overlayButton" onClick={this.toggleSearch} title="Open Search Bar" +                style={{ bottom: -this.props.panY + 10, right: 0, display: this.props.active() ? "flex" : "none" }}> +                <div className="pdfViewer-overlayButton-arrow" onPointerDown={(e) => e.stopPropagation()}></div> +                <div className="pdfViewer-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}> +                    <FontAwesomeIcon style={{ color: "white" }} icon={this._searching ? "times" : "search"} size="3x" /></div> +            </button> +        </div >);      }  } -export enum AnnotationTypes { -    Region -} +export enum AnnotationTypes { Region }  class SimpleLinkService {      externalLinkTarget: any = null; diff --git a/src/client/views/pdf/Page.scss b/src/client/views/pdf/Page.scss new file mode 100644 index 000000000..af1628a6f --- /dev/null +++ b/src/client/views/pdf/Page.scss @@ -0,0 +1,31 @@ + +.pdfPage-cont { +    position: relative; + +    .pdfPage-canvasContainer { +        position: absolute; +    } + +    .pdfPage-dragAnnotationBox { +        position: absolute; +        background-color: transparent; +        opacity: 0.1; +    } + +    .pdfPage-textLayer { +        position: absolute; +        width: 100%; +        height: 100%; +        div { +            user-select: text; +        } +        span { +            color: transparent; +            position: absolute; +            white-space: pre; +            cursor: text; +            -webkit-transform-origin: 0% 0%; +            transform-origin: 0% 0%; +        } +    } +}
\ No newline at end of file diff --git a/src/client/views/pdf/Page.tsx b/src/client/views/pdf/Page.tsx index dea4e0da1..7ca9d2d7d 100644 --- a/src/client/views/pdf/Page.tsx +++ b/src/client/views/pdf/Page.tsx @@ -3,38 +3,35 @@ import { observer } from "mobx-react";  import * as Pdfjs from "pdfjs-dist";  import "pdfjs-dist/web/pdf_viewer.css";  import { Doc, DocListCastAsync, Opt, WidthSym } from "../../../new_fields/Doc"; -import { List } from "../../../new_fields/List"; -import { listSpec } from "../../../new_fields/Schema"; -import { Cast, NumCast, StrCast, BoolCast } from "../../../new_fields/Types"; +import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types";  import { Docs, DocUtils } from "../../documents/Documents";  import { DragManager } from "../../util/DragManager"; -import { PDFBox } from "../nodes/PDFBox";  import PDFMenu from "./PDFMenu";  import { scale } from "./PDFViewer"; -import "./PDFViewer.scss"; +import "./Page.scss";  import React = require("react");  interface IPageProps {      size: { width: number, height: number }; -    pdf: Opt<Pdfjs.PDFDocumentProxy>; +    pdf: Pdfjs.PDFDocumentProxy;      name: string;      numPages: number;      page: number; -    pageLoaded: (index: number, page: Pdfjs.PDFPageViewport) => void; -    parent: PDFBox; +    pageLoaded: (page: Pdfjs.PDFPageViewport) => void; +    fieldExtensionDoc: Doc, +    Document: Doc,      renderAnnotations: (annotations: Doc[], removeOld: boolean) => void; -    makePin: (x: number, y: number, page: number) => 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;  }  @observer  export default class Page extends React.Component<IPageProps> { -    @observable private _state: string = "N/A"; +    @observable private _state: "N/A" | "rendering" = "N/A";      @observable private _width: number = this.props.size.width;      @observable private _height: number = this.props.size.height;      @observable private _page: Opt<Pdfjs.PDFPageProxy>; @@ -43,90 +40,44 @@ export default class Page extends React.Component<IPageProps> {      @observable private _marqueeY: number = 0;      @observable private _marqueeWidth: number = 0;      @observable private _marqueeHeight: number = 0; -    @observable private _rotate: string = ""; -    private _canvas: React.RefObject<HTMLCanvasElement>; -    private _textLayer: React.RefObject<HTMLDivElement>; -    private _annotationLayer: React.RefObject<HTMLDivElement>; -    private _marquee: React.RefObject<HTMLDivElement>; -    // private _curly: React.RefObject<HTMLImageElement>; +    private _canvas: React.RefObject<HTMLCanvasElement> = React.createRef(); +    private _textLayer: React.RefObject<HTMLDivElement> = React.createRef(); +    private _marquee: React.RefObject<HTMLDivElement> = React.createRef();      private _marqueeing: boolean = false;      private _reactionDisposer?: IReactionDisposer;      private _startY: number = 0;      private _startX: number = 0; -    constructor(props: IPageProps) { -        super(props); -        this._canvas = React.createRef(); -        this._textLayer = React.createRef(); -        this._annotationLayer = React.createRef(); -        this._marquee = React.createRef(); -        // this._curly = React.createRef(); -    } +    componentDidMount = (): void => this.loadPage(this.props.pdf); -    componentDidMount = (): void => { -        if (this.props.pdf) { -            this.update(this.props.pdf); -        } -    } +    componentDidUpdate = (): void => this.loadPage(this.props.pdf); -    componentWillUnmount = (): void => { -        if (this._reactionDisposer) { -            this._reactionDisposer(); -        } -    } +    componentWillUnmount = (): void => this._reactionDisposer && this._reactionDisposer(); -    componentDidUpdate = (): void => { -        if (this.props.pdf) { -            this.update(this.props.pdf); -        } -    } - -    private update = (pdf: Pdfjs.PDFDocumentProxy): void => { -        if (pdf) { -            this.loadPage(pdf); -        } -        else { -            this._state = "loading"; -        } -    } - -    private loadPage = (pdf: Pdfjs.PDFDocumentProxy): void => { -        if (this._state === "rendering" || this._page) return; - -        pdf.getPage(this._currPage).then( -            (page: Pdfjs.PDFPageProxy): void => { -                this._state = "rendering"; -                this.renderPage(page); -            } -        ); +    loadPage = (pdf: Pdfjs.PDFDocumentProxy): void => { +        pdf.getPage(this._currPage).then(page => this.renderPage(page));      }      @action -    private renderPage = (page: Pdfjs.PDFPageProxy): void => { +    renderPage = (page: Pdfjs.PDFPageProxy): void => {          // lower scale = easier to read at small sizes, higher scale = easier to read at large sizes -        let viewport = page.getViewport(scale); -        let canvas = this._canvas.current; -        let textLayer = this._textLayer.current; -        if (canvas && textLayer) { -            let ctx = canvas.getContext("2d"); -            canvas.width = viewport.width; -            this._width = viewport.width; -            canvas.height = viewport.height; -            this._height = viewport.height; -            this.props.pageLoaded(this._currPage, viewport); +        if (this._state !== "rendering" && !this._page && this._canvas.current && this._textLayer.current) { +            this._state = "rendering"; +            let viewport = page.getViewport(scale); +            this._canvas.current.width = this._width = viewport.width; +            this._canvas.current.height = this._height = viewport.height; +            this.props.pageLoaded(viewport); +            let ctx = this._canvas.current.getContext("2d");              if (ctx) { -                // renders the page onto the canvas context -                page.render({ canvasContext: ctx, viewport: viewport }); -                // renders text onto the text container -                page.getTextContent().then((res: Pdfjs.TextContent): void => { +                page.render({ canvasContext: ctx, viewport: viewport }); // renders the page onto the canvas context +                page.getTextContent().then(res =>                   // renders text onto the text container                      //@ts-ignore                      Pdfjs.renderTextLayer({                          textContent: res, -                        container: textLayer, +                        container: this._textLayer.current,                          viewport: viewport -                    }); -                }); +                    }));                  this._page = page;              } @@ -134,15 +85,10 @@ export default class Page extends React.Component<IPageProps> {      }      @action -    highlight = (targetDoc?: Doc, color: string = "red") => { +    highlight = (targetDoc: Doc | undefined, color: string) => {          // creates annotation documents for current highlights -        let annotationDoc = this.props.makeAnnotationDocuments(targetDoc, scale, color, false); -        let targetAnnotations = Cast(this.props.parent.fieldExtensionDoc.annotations, listSpec(Doc)); -        if (targetAnnotations === undefined) { -            Doc.GetProto(this.props.parent.fieldExtensionDoc).annotations = new List([annotationDoc]); -        } else { -            targetAnnotations.push(annotationDoc); -        } +        let annotationDoc = this.props.makeAnnotationDocuments(targetDoc, color, false); +        Doc.AddDocToList(this.props.fieldExtensionDoc, "annotations", annotationDoc);          return annotationDoc;      } @@ -154,26 +100,19 @@ export default class Page extends React.Component<IPageProps> {      startDrag = (e: PointerEvent, ele: HTMLElement): void => {          e.preventDefault();          e.stopPropagation(); -        let thisDoc = this.props.parent.Document; -        // document that this annotation is linked to -        let targetDoc = Docs.Create.TextDocument({ width: 200, height: 200, title: "New Annotation" }); -        targetDoc.targetPage = this.props.page; -        let annotationDoc = this.highlight(undefined, "red"); -        Doc.GetProto(annotationDoc).annotationOn = thisDoc; -        annotationDoc.linkedToDoc = false; -        // create dragData and star tdrag -        let dragData = new DragManager.AnnotationDragData(thisDoc, annotationDoc, targetDoc);          if (this._textLayer.current) { +            let targetDoc = Docs.Create.TextDocument({ width: 200, height: 200, title: "New Annotation" }); +            targetDoc.targetPage = this.props.page; +            let annotationDoc = this.highlight(undefined, "red"); +            annotationDoc.linkedToDoc = false; +            let dragData = new DragManager.AnnotationDragData(this.props.Document, annotationDoc, targetDoc);              DragManager.StartAnnotationDrag([ele], dragData, e.pageX, e.pageY, {                  handlers: {                      dragComplete: async () => {                          if (!BoolCast(annotationDoc.linkedToDoc)) {                              let annotations = await DocListCastAsync(annotationDoc.annotations);                              annotations && annotations.forEach(anno => anno.target = targetDoc); -                            let pdfDoc = await Cast(annotationDoc.pdfDoc, Doc); -                            if (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)}`)                          }                      }                  }, @@ -184,57 +123,44 @@ export default class Page extends React.Component<IPageProps> {      // cleans up events and boolean      endDrag = (e: PointerEvent): void => { -        // document.removeEventListener("pointermove", this.startDrag); -        // document.removeEventListener("pointerup", this.endDrag);          e.stopPropagation();      }      createSnippet = (marquee: { left: number, top: number, width: number, height: number }): void => { -        let doc = this.props.parent.Document; -        let view = Doc.MakeAlias(doc); -        let data = Doc.MakeDelegate(doc.proto!); +        let view = Doc.MakeAlias(this.props.Document); +        let data = Doc.MakeDelegate(Doc.GetProto(this.props.Document));          data.title = StrCast(data.title) + "_snippet";          view.proto = data;          view.nativeHeight = marquee.height; -        view.height = (doc[WidthSym]() / NumCast(doc.nativeWidth)) * marquee.height; -        view.nativeWidth = doc.nativeWidth; +        view.height = (this.props.Document[WidthSym]() / NumCast(this.props.Document.nativeWidth)) * marquee.height; +        view.nativeWidth = this.props.Document.nativeWidth;          view.startY = marquee.top + this.props.getScrollFromPage(this.props.page); -        view.width = doc[WidthSym](); -        let dragData = new DragManager.DocumentDragData([view], [undefined]); -        DragManager.StartDocumentDrag([], dragData, 0, 0); +        view.width = this.props.Document[WidthSym](); +        DragManager.StartDocumentDrag([], new DragManager.DocumentDragData([view], [undefined]), 0, 0);      }      @action      onPointerDown = (e: React.PointerEvent): void => {          // if alt+left click, drag and annotate -        if (e.altKey && e.button === 0) { -            e.stopPropagation(); - -            // document.removeEventListener("pointermove", this.startDrag); -            // document.addEventListener("pointermove", this.startDrag); -            // document.removeEventListener("pointerup", this.endDrag); -            // document.addEventListener("pointerup", this.endDrag); -        } -        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;              PDFMenu.Instance.Status = "pdf";              PDFMenu.Instance.fadeOut(true); -            let target: any = e.target; -            if (target && target.parentElement === this._textLayer.current) { +            if (e.target && (e.target as any).parentElement === this._textLayer.current) {                  e.stopPropagation();              }              else {                  // set marquee x and y positions to the spatially transformed position -                let current = this._textLayer.current; -                if (current) { -                    let boundingRect = current.getBoundingClientRect(); -                    this._startX = this._marqueeX = (e.clientX - boundingRect.left) * (current.offsetWidth / boundingRect.width); -                    this._startY = this._marqueeY = (e.clientY - boundingRect.top) * (current.offsetHeight / boundingRect.height); +                if (this._textLayer.current) { +                    let boundingRect = this._textLayer.current.getBoundingClientRect(); +                    this._startX = this._marqueeX = (e.clientX - boundingRect.left) * (this._textLayer.current.offsetWidth / boundingRect.width); +                    this._startY = this._marqueeY = (e.clientY - boundingRect.top) * (this._textLayer.current.offsetHeight / boundingRect.height);                  }                  this._marqueeing = true; -                if (this._marquee.current) this._marquee.current.style.opacity = "0.2"; +                this._marquee.current && (this._marquee.current.style.opacity = "0.2");              }              document.removeEventListener("pointermove", this.onSelectStart);              document.addEventListener("pointermove", this.onSelectStart); @@ -248,97 +174,41 @@ export default class Page extends React.Component<IPageProps> {      @action      onSelectStart = (e: PointerEvent): void => { -        let target: any = e.target; -        if (this._marqueeing) { -            let current = this._textLayer.current; -            if (current) { -                // transform positions and find the width and height to set the marquee to -                let boundingRect = current.getBoundingClientRect(); -                this._marqueeWidth = ((e.clientX - boundingRect.left) * (current.offsetWidth / boundingRect.width)) - this._startX; -                this._marqueeHeight = ((e.clientY - boundingRect.top) * (current.offsetHeight / boundingRect.height)) - this._startY; -                this._marqueeX = Math.min(this._startX, this._startX + this._marqueeWidth); -                this._marqueeY = Math.min(this._startY, this._startY + this._marqueeHeight); -                this._marqueeWidth = Math.abs(this._marqueeWidth); -                this._marqueeHeight = Math.abs(this._marqueeHeight); -                let { background, opacity, transform: transform } = this.getCurlyTransform(); -                if (this._marquee.current /*&& this._curly.current*/) { -                    this._marquee.current.style.background = background; -                    // this._curly.current.style.opacity = opacity; -                    this._rotate = transform; -                } -            } +        if (this._marqueeing && this._textLayer.current) { +            // transform positions and find the width and height to set the marquee to +            let boundingRect = this._textLayer.current.getBoundingClientRect(); +            this._marqueeWidth = ((e.clientX - boundingRect.left) * (this._textLayer.current.offsetWidth / boundingRect.width)) - this._startX; +            this._marqueeHeight = ((e.clientY - boundingRect.top) * (this._textLayer.current.offsetHeight / boundingRect.height)) - this._startY; +            this._marqueeX = Math.min(this._startX, this._startX + this._marqueeWidth); +            this._marqueeY = Math.min(this._startY, this._startY + this._marqueeHeight); +            this._marqueeWidth = Math.abs(this._marqueeWidth);              e.stopPropagation();              e.preventDefault();          } -        else if (target && target.parentElement === this._textLayer.current) { +        else if (e.target && (e.target as any).parentElement === this._textLayer.current) {              e.stopPropagation();          }      } -    getCurlyTransform = (): { background: string, opacity: string, transform: string } => { -        // let background = "", opacity = "", transform = ""; -        // if (this._marquee.current && this._curly.current) { -        //     if (this._marqueeWidth > 100 && this._marqueeHeight > 100) { -        //         background = "red"; -        //         opacity = "0"; -        //     } -        //     else { -        //         background = "transparent"; -        //         opacity = "1"; -        //     } - -        //     // split up for simplicity, could be done in a nested ternary. please do not. -syip2 -        //     let ratio = this._marqueeWidth / this._marqueeHeight; -        //     if (ratio > 1.5) { -        //         // vertical -        //         transform = "rotate(90deg) scale(1, 5)"; -        //     } -        //     else if (ratio < 0.5) { -        //         // horizontal -        //         transform = "scale(2, 1)"; -        //     } -        //     else { -        //         // diagonal -        //         transform = "rotate(45deg) scale(1.5, 1.5)"; -        //     } -        // } -        return { background: "red", opacity: "0.5", transform: "" }; -    } -      @action      onSelectEnd = (e: PointerEvent): void => {          if (this._marqueeing) {              this._marqueeing = false; -            if (this._marquee.current) { -                let copy = document.createElement("div"); -                // make a copy of the marquee -                let style = this._marquee.current.style; -                copy.style.left = style.left; -                copy.style.top = style.top; -                copy.style.width = style.width; -                copy.style.height = style.height; - -                // apply the appropriate background, opacity, and transform -                let { background, opacity, transform } = this.getCurlyTransform(); -                copy.style.background = background; -                // if curly bracing, add a curly brace -                // if (opacity === "1" && this._curly.current) { -                //     copy.style.opacity = opacity; -                //     let img = this._curly.current.cloneNode(); -                //     (img as any).style.opacity = opacity; -                //     (img as any).style.transform = transform; -                //     copy.appendChild(img); -                // } -                // else { -                copy.style.border = style.border; -                copy.style.opacity = style.opacity; -                // } -                copy.className = this._marquee.current.className; -                this.props.createAnnotation(copy, this.props.page); -                this._marquee.current.style.opacity = "0"; -            } -              if (this._marqueeWidth > 10 || this._marqueeHeight > 10) { +                if (this._marquee.current) { // make a copy of the marquee +                    let copy = document.createElement("div"); +                    let style = this._marquee.current.style; +                    copy.style.left = style.left; +                    copy.style.top = style.top; +                    copy.style.width = style.width; +                    copy.style.height = style.height; +                    copy.style.border = style.border; +                    copy.style.opacity = style.opacity; +                    copy.className = "pdfPage-annotationBox"; +                    this.props.createAnnotation(copy, this.props.page); +                    this._marquee.current.style.opacity = "0"; +                } +                  if (!e.ctrlKey) {                      PDFMenu.Instance.Status = "snippet";                      PDFMenu.Instance.Marquee = { left: this._marqueeX, top: this._marqueeY, width: this._marqueeWidth, height: this._marqueeHeight }; @@ -357,7 +227,6 @@ export default class Page extends React.Component<IPageProps> {              }          } -          if (PDFMenu.Instance.Highlighting) {              this.highlight(undefined, "goldenrod");          } @@ -371,14 +240,14 @@ export default class Page extends React.Component<IPageProps> {      @action      createTextAnnotation = (sel: Selection, selRange: Range) => { -        let clientRects = selRange.getClientRects();          if (this._textLayer.current) {              let boundingRect = this._textLayer.current.getBoundingClientRect(); +            let clientRects = selRange.getClientRects();              for (let i = 0; i < clientRects.length; i++) {                  let rect = clientRects.item(i);                  if (rect && rect.width !== this._textLayer.current.getBoundingClientRect().width && rect.height !== this._textLayer.current.getBoundingClientRect().height) {                      let annoBox = document.createElement("div"); -                    annoBox.className = "pdfViewer-annotationBox"; +                    annoBox.className = "pdfPage-annotationBox";                      // transforms the positions from screen onto the pdf div                      annoBox.style.top = ((rect.top - boundingRect.top) * (this._textLayer.current.offsetHeight / boundingRect.height)).toString();                      annoBox.style.left = ((rect.left - boundingRect.left) * (this._textLayer.current.offsetWidth / boundingRect.width)).toString(); @@ -389,9 +258,8 @@ export default class Page extends React.Component<IPageProps> {              }          }          let text = selRange.extractContents().textContent; -        if (text) { -            this.props.setSelectionText(text); -        } +        text && this.props.setSelectionText(text); +          // clear selection          if (sel.empty) {  // Chrome              sel.empty(); @@ -401,35 +269,23 @@ export default class Page extends React.Component<IPageProps> {      }      doubleClick = (e: React.MouseEvent) => { -        let target: any = e.target; -        // if double clicking text -        if (target && target.parentElement === this._textLayer.current) { +        if (e.target && (e.target as any).parentElement === this._textLayer.current) {              // do something to select the paragraph ideally          } - -        let current = this._textLayer.current; -        if (current) { -            let boundingRect = current.getBoundingClientRect(); -            let x = (e.clientX - boundingRect.left) * (current.offsetWidth / boundingRect.width); -            let y = (e.clientY - boundingRect.top) * (current.offsetHeight / boundingRect.height); -            this.props.makePin(x, y, this.props.page); -        }      }      render() {          return ( -            <div onPointerDown={this.onPointerDown} onDoubleClick={this.doubleClick} className={"page-cont"} style={{ "width": this._width, "height": this._height }}> -                <div className="canvasContainer"> -                    <canvas ref={this._canvas} /> +            <div className={"pdfPage-cont"} onPointerDown={this.onPointerDown} onDoubleClick={this.doubleClick} style={{ "width": this._width, "height": this._height }}> +                <canvas className="PdfPage-canvasContainer" ref={this._canvas} /> +                <div className="pdfPage-dragAnnotationBox" ref={this._marquee} +                    style={{ +                        left: `${this._marqueeX}px`, top: `${this._marqueeY}px`, +                        width: `${this._marqueeWidth}px`, height: `${this._marqueeHeight}px`, +                        border: `${this._marqueeWidth === 0 ? "" : "10px dashed black"}` +                    }}>                  </div> -                <div className="pdfInkingLayer-cont" ref={this._annotationLayer} style={{ width: "100%", height: "100%", position: "relative", top: "-100%" }}> -                    <div className="pdfViewer-annotationBox" ref={this._marquee} -                        style={{ left: `${this._marqueeX}px`, top: `${this._marqueeY}px`, width: `${this._marqueeWidth}px`, height: `${this._marqueeHeight}px`, background: "red", border: `${this._marqueeWidth === 0 ? "" : "10px dashed black"}` }}> -                        {/* <img ref={this._curly} src="https://static.thenounproject.com/png/331760-200.png" style={{ width: "100%", height: "100%", transform: `${this._rotate}` }} /> */} -                    </div> -                </div> -                <div className="textlayer" ref={this._textLayer} style={{ "position": "relative", "top": `-${2 * this._height}px`, "height": `${this._height}px` }} /> -            </div> -        ); +                <div className="pdfPage-textlayer" ref={this._textLayer} /> +            </div>);      } -} +}
\ No newline at end of file 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 48eb87251..8201aa374 100644 --- a/src/client/views/search/SearchItem.tsx +++ b/src/client/views/search/SearchItem.tsx @@ -1,34 +1,30 @@  import React = require("react");  import { library } from '@fortawesome/fontawesome-svg-core'; -import { faCaretUp, faChartBar, faFilePdf, faFilm, faGlobeAsia, faImage, faLink, faMusic, faObjectGroup, faStickyNote, faFingerprint } from '@fortawesome/free-solid-svg-icons'; +import { faCaretUp, faChartBar, faFile, faFilePdf, faFilm, faFingerprint, faGlobeAsia, faImage, faLink, faMusic, faObjectGroup, faStickyNote } from '@fortawesome/free-solid-svg-icons';  import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";  import { action, computed, observable, runInAction } from "mobx";  import { observer } from "mobx-react";  import { Doc, DocListCast, HeightSym, WidthSym } from "../../../new_fields/Doc";  import { Id } from "../../../new_fields/FieldSymbols"; +import { ObjectField } from "../../../new_fields/ObjectField"; +import { RichTextField } from "../../../new_fields/RichTextField";  import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; -import { emptyFunction, returnFalse, returnOne, Utils, returnEmptyString } from "../../../Utils"; +import { emptyFunction, returnEmptyString, returnFalse, returnOne, Utils } from "../../../Utils"; +import { DocServer } from "../../DocServer";  import { DocumentType } from "../../documents/Documents";  import { DocumentManager } from "../../util/DocumentManager"; -import { SetupDrag, DragManager } from "../../util/DragManager"; +import { DragManager, SetupDrag } from "../../util/DragManager";  import { LinkManager } from "../../util/LinkManager";  import { SearchUtil } from "../../util/SearchUtil";  import { Transform } from "../../util/Transform";  import { SEARCH_THUMBNAIL_SIZE } from "../../views/globalCssVariables.scss";  import { CollectionViewType } from "../collections/CollectionBaseView";  import { CollectionDockingView } from "../collections/CollectionDockingView"; +import { ContextMenu } from "../ContextMenu";  import { DocumentView } from "../nodes/DocumentView";  import { SearchBox } from "./SearchBox";  import "./SearchItem.scss";  import "./SelectorContextMenu.scss"; -import { RichTextField } from "../../../new_fields/RichTextField"; -import { FormattedTextBox } from "../nodes/FormattedTextBox"; -import { MarqueeView } from "../collections/collectionFreeForm/MarqueeView"; -import { SelectionManager } from "../../util/SelectionManager"; -import { ObjectField } from "../../../new_fields/ObjectField"; -import { ContextMenu } from "../ContextMenu"; -import { faFile } from '@fortawesome/free-solid-svg-icons'; -import { DocServer } from "../../DocServer";  export interface SearchItemProps {      doc: Doc; @@ -109,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 ( @@ -290,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));          }      } @@ -307,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));          }      } @@ -355,7 +335,7 @@ export class SearchItem extends React.Component<SearchItemProps> {                          </div>                          <div className="search-info" style={{ width: this._useIcons ? "15%" : "400px" }}>                              <div className={`icon-${this._useIcons ? "icons" : "live"}`}> -                                <div className="search-type" title="Click to Preview">{this.DocumentIcon}</div> +                                <div className="search-type" title="Click to Preview">{this.DocumentIcon()}</div>                                  <div className="search-label">{this.props.doc.type ? this.props.doc.type : "Other"}</div>                              </div>                              <div className="link-container item"> @@ -366,8 +346,8 @@ export class SearchItem extends React.Component<SearchItemProps> {                      </div>                  </div>                  <div className="searchBox-instances"> -                    {(doc1 instanceof Doc && doc2 instanceof Doc) ? this.props.doc.type === DocumentType.LINK ? <LinkContextMenu doc1={doc1} doc2={doc2} /> : -                        <SelectorContextMenu {...this.props} /> : null} +                    {(doc1 instanceof Doc && doc2 instanceof Doc) && this.props.doc.type === DocumentType.LINK ? <LinkContextMenu doc1={doc1} doc2={doc2} /> : +                        <SelectorContextMenu {...this.props} />}                  </div>              </div>          ); | 
